@tinyweb_dev/oe-exam-sdk 0.1.27 → 0.1.29

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 (809) hide show
  1. package/README.md +79 -6
  2. package/dist/api/contest-rooms.js +54 -1
  3. package/dist/api/contest-rooms.types.js +1 -1
  4. package/dist/api/exam-create.js +179 -1
  5. package/dist/api/exam-create.types.js +1 -1
  6. package/dist/api/exam-entry.js +68 -1
  7. package/dist/api/exam-entry.types.js +1 -1
  8. package/dist/api/exam-questions.js +149 -1
  9. package/dist/api/exam-questions.types.js +1 -1
  10. package/dist/api/exam-taking.js +84 -1
  11. package/dist/api/exam-taking.types.d.ts +7 -0
  12. package/dist/api/exam-taking.types.js +1 -1
  13. package/dist/api/index.js +8 -1
  14. package/dist/api/result-review.js +62 -1
  15. package/dist/api/result-review.types.js +16 -1
  16. package/dist/components/CueCardEditor.js +58 -1
  17. package/dist/components/common/ResolvedImage.js +15 -1
  18. package/dist/components/exams/CompletionDialog.js +8 -1
  19. package/dist/components/exams/CreateExamInfoForm.js +84 -1
  20. package/dist/components/exams/CreateExamPageContainer.js +110 -1
  21. package/dist/components/exams/ExamCreator.js +287 -1
  22. package/dist/components/exams/ExamPartTabs.js +138 -1
  23. package/dist/components/exams/ExamPreviewDialog.d.ts +6 -7
  24. package/dist/components/exams/ExamPreviewDialog.js +121 -1
  25. package/dist/components/exams/ExamPreviewPartContent.d.ts +18 -0
  26. package/dist/components/exams/ExamPreviewPartContent.js +27 -0
  27. package/dist/components/exams/ExamPreviewSidebar.d.ts +17 -0
  28. package/dist/components/exams/ExamPreviewSidebar.js +37 -0
  29. package/dist/components/exams/ExamQuestionGrid.js +37 -1
  30. package/dist/components/exams/ExamQuestionsPageContainer.js +112 -1
  31. package/dist/components/exams/ExamViewDialog.js +61 -1
  32. package/dist/components/exams/PartAudioUploader.js +9 -1
  33. package/dist/components/exams/TemplateSelector.js +76 -1
  34. package/dist/components/exams/UnsavedChangesDialog.js +7 -1
  35. package/dist/components/exams/api.js +3 -1
  36. package/dist/components/exams/create-exam.texts.js +1 -1
  37. package/dist/components/exams/create-exam.validation.js +18 -1
  38. package/dist/components/exams/entry/EntryRestartDialog.js +11 -1
  39. package/dist/components/exams/entry/StudentExamEntryPageContainer.js +358 -1
  40. package/dist/components/exams/entry/exam-entry.states.js +26 -1
  41. package/dist/components/exams/entry/exam-entry.utils.js +55 -1
  42. package/dist/components/exams/entry/fullscreen-loading.js +18 -1
  43. package/dist/components/exams/entry/index.js +4 -1
  44. package/dist/components/exams/exam-preview.types.d.ts +30 -0
  45. package/dist/components/exams/exam-preview.types.js +62 -0
  46. package/dist/components/exams/exam-question-status.js +45 -1
  47. package/dist/components/exams/exam-template.utils.js +18 -1
  48. package/dist/components/exams/index.d.ts +1 -0
  49. package/dist/components/exams/index.js +19 -1
  50. package/dist/components/exams/rooms/StudentContestRoomsPageContainer.js +183 -1
  51. package/dist/components/exams/rooms/contest-rooms.utils.js +93 -1
  52. package/dist/components/exams/rooms/index.js +2 -1
  53. package/dist/components/exams/take/ExamTakingApiContext.js +25 -1
  54. package/dist/components/exams/take/ExamTakingPageContainer.js +955 -1
  55. package/dist/components/exams/take/GenericQuestionRenderer.js +14 -1
  56. package/dist/components/exams/take/components/QuestionRenderer.js +207 -1
  57. package/dist/components/exams/take/components/index.js +11 -1
  58. package/dist/components/exams/take/components/question-renderers/ArticlesViewer.js +8 -1
  59. package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +30 -1
  60. package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.js +63 -1
  61. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +136 -1
  62. package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.js +379 -1
  63. package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.d.ts +20 -0
  64. package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js +141 -0
  65. package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.js +131 -1
  66. package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.js +76 -1
  67. package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.js +68 -1
  68. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +42 -1
  69. package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.js +131 -1
  70. package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.js +76 -1
  71. package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.js +68 -1
  72. package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.js +62 -1
  73. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.d.ts +1 -1
  74. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.js +51 -1
  75. package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.js +171 -1
  76. package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +198 -1
  77. package/dist/components/exams/take/components/question-renderers/MoversWordOrderAndMatchGroupRenderer.d.ts +15 -0
  78. package/dist/components/exams/take/components/question-renderers/MoversWordOrderAndMatchGroupRenderer.js +36 -0
  79. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.js +92 -1
  80. package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +11 -1
  81. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +56 -1
  82. package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
  83. package/dist/components/exams/take/components/question-renderers/index.js +25 -1
  84. package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.js +85 -1
  85. package/dist/components/exams/take/components/speaking/SpeakingContentFrame.js +10 -1
  86. package/dist/components/exams/take/components/speaking/SpeakingExamLayout.js +15 -1
  87. package/dist/components/exams/take/components/speaking/SpeakingHeader.js +6 -1
  88. package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.js +8 -1
  89. package/dist/components/exams/take/components/speaking/SpeakingMicButton.js +59 -1
  90. package/dist/components/exams/take/components/speaking/SpeakingNavButton.js +7 -1
  91. package/dist/components/exams/take/components/speaking/SpeakingPartBadge.js +5 -1
  92. package/dist/components/exams/take/components/speaking/SpeakingReadDisplayedContent.module.css +120 -0
  93. package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.js +73 -1
  94. package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.js +8 -1
  95. package/dist/components/exams/take/components/speaking/SpeakingSpontaneousQA.module.css +344 -0
  96. package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.js +32 -1
  97. package/dist/components/exams/take/components/speaking/index.js +1 -1
  98. package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.js +63 -1
  99. package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.js +95 -1
  100. package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.js +35 -1
  101. package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.js +147 -1
  102. package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.js +232 -1
  103. package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.js +193 -1
  104. package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.js +155 -1
  105. package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.js +212 -1
  106. package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.js +77 -1
  107. package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.js +64 -1
  108. package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.js +69 -1
  109. package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.js +64 -1
  110. package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.js +92 -1
  111. package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.js +202 -1
  112. package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.js +339 -1
  113. package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.d.ts +19 -0
  114. package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +76 -0
  115. package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.js +56 -1
  116. package/dist/components/exams/take/components/speaking/renderers/SpontaneousQACambridgeLayout.d.ts +24 -0
  117. package/dist/components/exams/take/components/speaking/renderers/SpontaneousQACambridgeLayout.js +120 -0
  118. package/dist/components/exams/take/components/speaking/renderers/SpontaneousQAInlineAudioPlayer.d.ts +8 -0
  119. package/dist/components/exams/take/components/speaking/renderers/SpontaneousQAInlineAudioPlayer.js +94 -0
  120. package/dist/components/exams/take/components/speaking/renderers/SpontaneousQASummerSkyAudioPlayer.d.ts +8 -0
  121. package/dist/components/exams/take/components/speaking/renderers/SpontaneousQASummerSkyAudioPlayer.js +101 -0
  122. package/dist/components/exams/take/components/speaking/renderers/index.d.ts +1 -0
  123. package/dist/components/exams/take/components/speaking/renderers/index.js +14 -1
  124. package/dist/components/exams/take/exam-taking.utils.d.ts +13 -0
  125. package/dist/components/exams/take/exam-taking.utils.js +203 -1
  126. package/dist/components/exams/take/hooks/index.js +2 -1
  127. package/dist/components/exams/take/hooks/useAnswerAutosave.js +71 -1
  128. package/dist/components/exams/take/hooks/useAttemptSubmit.js +38 -1
  129. package/dist/components/exams/take/hooks/useAutoSave.js +83 -1
  130. package/dist/components/exams/take/hooks/useExamCountdown.js +35 -1
  131. package/dist/components/exams/take/hooks/useExamTimer.js +97 -1
  132. package/dist/components/exams/take/hooks/useProctoringCamera.js +118 -1
  133. package/dist/components/exams/take/hooks/useProctoringSnapshot.js +76 -1
  134. package/dist/components/exams/take/hooks/useSpeakingConversationAudio.js +178 -1
  135. package/dist/components/exams/take/hooks/useSpeakingNavigation.js +51 -1
  136. package/dist/components/exams/take/hooks/useSpeakingRecorder.js +406 -1
  137. package/dist/components/exams/take/hooks/useSpeakingRendererSetup.js +136 -1
  138. package/dist/components/exams/take/hooks/useSpontaneousQaGroupRecorder.d.ts +11 -0
  139. package/dist/components/exams/take/hooks/useSpontaneousQaGroupRecorder.js +352 -0
  140. package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.d.ts +42 -0
  141. package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +166 -0
  142. package/dist/components/exams/take/hooks/useToeicAudio.js +35 -1
  143. package/dist/components/exams/take/index.js +6 -1
  144. package/dist/components/exams/take/types/speaking-cue-card.types.js +1 -1
  145. package/dist/components/exams/take/types/speaking.types.js +7 -1
  146. package/dist/components/exams/take/types.js +4 -1
  147. package/dist/components/exams/take/utils/answer-transformers.js +452 -1
  148. package/dist/components/exams/take/utils/cambridge-yle-inline-speaking.d.ts +21 -0
  149. package/dist/components/exams/take/utils/cambridge-yle-inline-speaking.js +44 -0
  150. package/dist/components/exams/take/utils/coloring-assignments.js +40 -1
  151. package/dist/components/exams/take/utils/cross-out-word-group.transformer.d.ts +21 -0
  152. package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +83 -0
  153. package/dist/components/exams/take/utils/question-transformers.d.ts +43 -2
  154. package/dist/components/exams/take/utils/question-transformers.js +1137 -1
  155. package/dist/components/features/exam-entry/entry-state-cards.js +19 -1
  156. package/dist/components/features/exam-entry/index.js +2 -1
  157. package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.js +121 -1
  158. package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.js +37 -1
  159. package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.js +15 -1
  160. package/dist/components/features/exam-entry/themed/cambridge-yle-waiting-parts.js +80 -1
  161. package/dist/components/features/exam-entry/themed/components/CloseButton.js +6 -1
  162. package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.js +17 -1
  163. package/dist/components/features/exam-entry/themed/components/ModeSelector.js +6 -1
  164. package/dist/components/features/exam-entry/themed/components/StartButton.js +12 -1
  165. package/dist/components/features/exam-entry/themed/components/index.js +4 -1
  166. package/dist/components/features/exam-entry/themed/index.js +3 -1
  167. package/dist/components/index.d.ts +1 -0
  168. package/dist/components/index.js +43 -1
  169. package/dist/components/questions/_shared/config/question-types.config.js +362 -1
  170. package/dist/components/questions/_shared/constants/color-palette.js +21 -1
  171. package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +105 -1
  172. package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +222 -1
  173. package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +140 -1
  174. package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +123 -1
  175. package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +50 -1
  176. package/dist/components/questions/_shared/hooks/useQuestionFormState.js +54 -1
  177. package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +123 -1
  178. package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +121 -1
  179. package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +70 -1
  180. package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +194 -1
  181. package/dist/components/questions/_shared/index.js +7 -1
  182. package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
  183. package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
  184. package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +3 -1
  185. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +1 -0
  186. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +1 -1
  187. package/dist/components/questions/_shared/types/color-region.config.js +1 -1
  188. package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +2 -0
  189. package/dist/components/questions/_shared/types/cross-out-word-group.type.js +1 -1
  190. package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
  191. package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
  192. package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
  193. package/dist/components/questions/_shared/types/index.d.ts +1 -0
  194. package/dist/components/questions/_shared/types/index.js +14 -1
  195. package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
  196. package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
  197. package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
  198. package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
  199. package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
  200. package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
  201. package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
  202. package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
  203. package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +3 -1
  204. package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
  205. package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +3 -1
  206. package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
  207. package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
  208. package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
  209. package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
  210. package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
  211. package/dist/components/questions/_shared/types/multiple-choice.type.d.ts +5 -0
  212. package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
  213. package/dist/components/questions/_shared/types/question-group.type.js +11 -1
  214. package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
  215. package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
  216. package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
  217. package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
  218. package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +37 -0
  219. package/dist/components/questions/_shared/types/spontaneous-qa-group.type.js +1 -0
  220. package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
  221. package/dist/components/questions/_shared/types/true-false-group.type.js +1 -1
  222. package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
  223. package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
  224. package/dist/components/questions/_shared/types/word-fill-structured-form.type.d.ts +20 -11
  225. package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
  226. package/dist/components/questions/_shared/types/word-order-and-match-group.type.d.ts +38 -0
  227. package/dist/components/questions/_shared/types/word-order-and-match-group.type.js +1 -0
  228. package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
  229. package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
  230. package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +3 -1
  231. package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
  232. package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
  233. package/dist/components/questions/_shared/utils/answer-builder.js +79 -1
  234. package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +39 -1
  235. package/dist/components/questions/_shared/utils/id-generator.js +3 -1
  236. package/dist/components/questions/_shared/utils/parseFillBlankStem.d.ts +30 -0
  237. package/dist/components/questions/_shared/utils/parseFillBlankStem.js +69 -0
  238. package/dist/components/questions/_shared/utils/question-validation.js +21 -1
  239. package/dist/components/questions/components/ColorNode.js +85 -1
  240. package/dist/components/questions/components/ColorRegionClient.js +545 -1
  241. package/dist/components/questions/components/ColorRegionCreator.js +420 -1
  242. package/dist/components/questions/components/ColorRegionNode.js +59 -1
  243. package/dist/components/questions/components/CreatorGuide.js +103 -1
  244. package/dist/components/questions/components/NameNode.js +26 -1
  245. package/dist/components/questions/components/QuestionBankOption.js +46 -1
  246. package/dist/components/questions/components/QuestionBasicInfoForm.js +9 -1
  247. package/dist/components/questions/components/QuestionCommonFields.js +33 -1
  248. package/dist/components/questions/components/QuestionSearchDropdown.js +258 -1
  249. package/dist/components/questions/components/QuestionTypeSelector.js +32 -1
  250. package/dist/components/questions/components/QuestionTypeSpecificForms.js +6 -1
  251. package/dist/components/questions/components/RegionNode.js +94 -1
  252. package/dist/components/questions/components/index.js +14 -1
  253. package/dist/components/questions/creator/QuestionCreator.js +90 -1
  254. package/dist/components/questions/creator/QuestionGroupCreator.js +119 -1
  255. package/dist/components/questions/creator/dedicated-component-router.js +79 -1
  256. package/dist/components/questions/creator/index.js +3 -1
  257. package/dist/components/questions/creator/question-type-registry.js +90 -1
  258. package/dist/components/questions/groups/ArticlesGroupCard.js +121 -1
  259. package/dist/components/questions/groups/BasicQuestionGroupCard.js +50 -1
  260. package/dist/components/questions/groups/DocumentGroupCard.js +114 -1
  261. package/dist/components/questions/groups/MatchAnswerGroupCard.js +25 -1
  262. package/dist/components/questions/index.d.ts +1 -0
  263. package/dist/components/questions/index.js +11 -1
  264. package/dist/components/questions/question-bank/EditQuestionBankDialog.d.ts +2 -0
  265. package/dist/components/questions/question-bank/EditQuestionBankDialog.js +114 -0
  266. package/dist/components/questions/question-bank/QuestionEditorRenderer.d.ts +23 -0
  267. package/dist/components/questions/question-bank/QuestionEditorRenderer.js +75 -0
  268. package/dist/components/questions/question-bank/difficulty.d.ts +15 -0
  269. package/dist/components/questions/question-bank/difficulty.js +33 -0
  270. package/dist/components/questions/question-bank/editor-adapters/choice-adapters.d.ts +5 -0
  271. package/dist/components/questions/question-bank/editor-adapters/choice-adapters.js +101 -0
  272. package/dist/components/questions/question-bank/editor-adapters/index.d.ts +5 -0
  273. package/dist/components/questions/question-bank/editor-adapters/index.js +4 -0
  274. package/dist/components/questions/question-bank/editor-adapters/media-adapters.d.ts +6 -0
  275. package/dist/components/questions/question-bank/editor-adapters/media-adapters.js +140 -0
  276. package/dist/components/questions/question-bank/editor-adapters/types.d.ts +1 -0
  277. package/dist/components/questions/question-bank/editor-adapters/types.js +1 -0
  278. package/dist/components/questions/question-bank/editor-adapters/unsupported-fallback.d.ts +8 -0
  279. package/dist/components/questions/question-bank/editor-adapters/unsupported-fallback.js +6 -0
  280. package/dist/components/questions/question-bank/editor-adapters/writing-adapters.d.ts +5 -0
  281. package/dist/components/questions/question-bank/editor-adapters/writing-adapters.js +114 -0
  282. package/dist/components/questions/question-bank/index.d.ts +4 -0
  283. package/dist/components/questions/question-bank/index.js +3 -0
  284. package/dist/components/questions/question-bank/types.d.ts +59 -0
  285. package/dist/components/questions/question-bank/types.js +1 -0
  286. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +37 -1
  287. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +351 -1
  288. package/dist/components/questions/types/answer-the-question/register.js +67 -1
  289. package/dist/components/questions/types/answer-the-question/transform.js +36 -1
  290. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +141 -1
  291. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +118 -1
  292. package/dist/components/questions/types/arrange-letters-into-words/index.js +2 -1
  293. package/dist/components/questions/types/arrange-letters-into-words/register.js +4 -1
  294. package/dist/components/questions/types/arrange-letters-into-words/transform.js +24 -1
  295. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +74 -1
  296. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +169 -1
  297. package/dist/components/questions/types/choose-correct-adjective/index.js +2 -1
  298. package/dist/components/questions/types/choose-correct-adjective/register.js +55 -1
  299. package/dist/components/questions/types/choose-correct-adjective/transform.js +16 -1
  300. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +149 -1
  301. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +568 -1
  302. package/dist/components/questions/types/choose-the-correct-answer/index.js +3 -1
  303. package/dist/components/questions/types/choose-the-correct-answer/normalize.d.ts +9 -0
  304. package/dist/components/questions/types/choose-the-correct-answer/normalize.js +88 -1
  305. package/dist/components/questions/types/choose-the-correct-answer/register.js +99 -1
  306. package/dist/components/questions/types/choose-the-correct-answer/transform.js +70 -1
  307. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +22 -1
  308. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +163 -1
  309. package/dist/components/questions/types/choose-the-correct-answer-group/index.d.ts +1 -0
  310. package/dist/components/questions/types/choose-the-correct-answer-group/index.js +3 -1
  311. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.d.ts +4 -0
  312. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +94 -0
  313. package/dist/components/questions/types/choose-the-correct-answer-group/register.js +36 -1
  314. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +55 -1
  315. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +27 -1
  316. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +231 -1
  317. package/dist/components/questions/types/cross-out-word-group/index.d.ts +1 -0
  318. package/dist/components/questions/types/cross-out-word-group/index.js +5 -1
  319. package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.d.ts +6 -0
  320. package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +110 -0
  321. package/dist/components/questions/types/cross-out-word-group/register.js +36 -1
  322. package/dist/components/questions/types/cross-out-word-group/transform.js +67 -1
  323. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +275 -1
  324. package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +464 -1
  325. package/dist/components/questions/types/fill-in-blank/index.js +2 -1
  326. package/dist/components/questions/types/fill-in-blank/register.js +148 -1
  327. package/dist/components/questions/types/fill-in-blank/transform.js +88 -1
  328. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +117 -1
  329. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +671 -1
  330. package/dist/components/questions/types/fill-missing-words-in-grid/index.js +2 -1
  331. package/dist/components/questions/types/fill-missing-words-in-grid/register.js +37 -1
  332. package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +15 -1
  333. package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +262 -1
  334. package/dist/components/questions/types/gn-speaking-interview/register.js +121 -1
  335. package/dist/components/questions/types/gn-speaking-interview/transform.js +48 -1
  336. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +16 -1
  337. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +27 -1
  338. package/dist/components/questions/types/image-object-matching/index.js +2 -1
  339. package/dist/components/questions/types/image-object-matching/register.js +3 -1
  340. package/dist/components/questions/types/image-object-matching/transform.js +2 -1
  341. package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +102 -1
  342. package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +344 -1
  343. package/dist/components/questions/types/label-the-picture/index.js +2 -1
  344. package/dist/components/questions/types/label-the-picture/register.js +54 -1
  345. package/dist/components/questions/types/label-the-picture/transform.js +28 -1
  346. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +78 -1
  347. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +232 -1
  348. package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +2 -1
  349. package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +82 -1
  350. package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +27 -1
  351. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +70 -1
  352. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +386 -1
  353. package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +2 -1
  354. package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +100 -1
  355. package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +33 -1
  356. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +106 -1
  357. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +457 -1
  358. package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +2 -1
  359. package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +110 -1
  360. package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +35 -1
  361. package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +8 -1
  362. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +22 -1
  363. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +291 -1
  364. package/dist/components/questions/types/listen-and-speak-answer/index.js +2 -1
  365. package/dist/components/questions/types/listen-and-speak-answer/register.js +84 -1
  366. package/dist/components/questions/types/listen-and-speak-answer/transform.js +27 -1
  367. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +32 -1
  368. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +295 -1
  369. package/dist/components/questions/types/listen-and-speak-compare-images/register.js +79 -1
  370. package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +27 -1
  371. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +34 -1
  372. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +285 -1
  373. package/dist/components/questions/types/listen-and-speak-image-group/register.js +81 -1
  374. package/dist/components/questions/types/listen-and-speak-image-group/transform.js +26 -1
  375. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +38 -1
  376. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +385 -1
  377. package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +87 -1
  378. package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +32 -1
  379. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +26 -1
  380. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +247 -1
  381. package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +59 -1
  382. package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +18 -1
  383. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +26 -1
  384. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +256 -1
  385. package/dist/components/questions/types/listen-and-speak-question-list/register.js +81 -1
  386. package/dist/components/questions/types/listen-and-speak-question-list/transform.js +26 -1
  387. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +26 -1
  388. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +230 -1
  389. package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +56 -1
  390. package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +16 -1
  391. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +72 -1
  392. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +230 -1
  393. package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
  394. package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +66 -1
  395. package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +42 -1
  396. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +96 -1
  397. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +238 -1
  398. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +2 -1
  399. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +76 -1
  400. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +19 -1
  401. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +134 -1
  402. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +617 -1
  403. package/dist/components/questions/types/look-picture-fill-word-hint/index.js +2 -1
  404. package/dist/components/questions/types/look-picture-fill-word-hint/register.js +82 -1
  405. package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +29 -1
  406. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +38 -1
  407. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +359 -1
  408. package/dist/components/questions/types/match-by-writing-answer/index.js +3 -1
  409. package/dist/components/questions/types/match-by-writing-answer/register.js +58 -1
  410. package/dist/components/questions/types/match-by-writing-answer/transform.js +61 -1
  411. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +16 -1
  412. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +23 -1
  413. package/dist/components/questions/types/read-and-color-objects/index.js +2 -1
  414. package/dist/components/questions/types/read-and-color-objects/register.js +3 -1
  415. package/dist/components/questions/types/read-and-color-objects/transform.js +36 -1
  416. package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +125 -1
  417. package/dist/components/questions/types/read-displayed-content/register.js +64 -1
  418. package/dist/components/questions/types/read-displayed-content/transform.js +12 -1
  419. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +65 -1
  420. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +511 -1
  421. package/dist/components/questions/types/read-passage-and-answer-questions/index.js +2 -1
  422. package/dist/components/questions/types/read-passage-and-answer-questions/register.js +58 -1
  423. package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +28 -1
  424. package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +373 -1
  425. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +41 -1
  426. package/dist/components/questions/types/speaking-conversation/register.js +110 -1
  427. package/dist/components/questions/types/speaking-conversation/transform.js +34 -1
  428. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +74 -1
  429. package/dist/components/questions/types/speaking-cue-card/register.js +52 -1
  430. package/dist/components/questions/types/speaking-cue-card/transform.js +12 -1
  431. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +10 -1
  432. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +161 -1
  433. package/dist/components/questions/types/speaking-describe-image/index.js +2 -1
  434. package/dist/components/questions/types/speaking-describe-image/register.js +69 -1
  435. package/dist/components/questions/types/speaking-describe-image/transform.js +16 -1
  436. package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +234 -1
  437. package/dist/components/questions/types/spontaneous-qa/register.js +93 -1
  438. package/dist/components/questions/types/spontaneous-qa/transform.js +34 -1
  439. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +2 -0
  440. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +9 -0
  441. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.d.ts +2 -0
  442. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +179 -0
  443. package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.d.ts +5 -0
  444. package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +43 -0
  445. package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +14 -0
  446. package/dist/components/questions/types/spontaneous-qa-group/flatten.js +69 -0
  447. package/dist/components/questions/types/spontaneous-qa-group/index.d.ts +2 -0
  448. package/dist/components/questions/types/spontaneous-qa-group/index.js +2 -0
  449. package/dist/components/questions/types/spontaneous-qa-group/register.d.ts +2 -0
  450. package/dist/components/questions/types/spontaneous-qa-group/register.js +139 -0
  451. package/dist/components/questions/types/spontaneous-qa-group/transform.d.ts +2 -0
  452. package/dist/components/questions/types/spontaneous-qa-group/transform.js +52 -0
  453. package/dist/components/questions/types/true-false/TrueFalseClient.js +75 -1
  454. package/dist/components/questions/types/true-false/TrueFalseCreator.js +244 -1
  455. package/dist/components/questions/types/true-false/index.js +2 -1
  456. package/dist/components/questions/types/true-false/register.js +77 -1
  457. package/dist/components/questions/types/true-false/transform.js +32 -1
  458. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +25 -1
  459. package/dist/components/questions/types/true-false-group/TrueFalseGroupCreator.js +129 -1
  460. package/dist/components/questions/types/true-false-group/index.d.ts +1 -0
  461. package/dist/components/questions/types/true-false-group/index.js +5 -1
  462. package/dist/components/questions/types/true-false-group/map-true-false-group-data.d.ts +4 -0
  463. package/dist/components/questions/types/true-false-group/map-true-false-group-data.js +69 -0
  464. package/dist/components/questions/types/true-false-group/register.js +36 -1
  465. package/dist/components/questions/types/true-false-group/transform.js +43 -1
  466. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +233 -1
  467. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +631 -1
  468. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +28 -1
  469. package/dist/components/questions/types/word-fill-paragraph/index.js +2 -1
  470. package/dist/components/questions/types/word-fill-paragraph/register.js +136 -1
  471. package/dist/components/questions/types/word-fill-paragraph/transform.js +56 -1
  472. package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.js +86 -1
  473. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +246 -1
  474. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.d.ts +2 -0
  475. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +484 -1
  476. package/dist/components/questions/types/word-fill-structured-form/index.d.ts +1 -0
  477. package/dist/components/questions/types/word-fill-structured-form/index.js +3 -1
  478. package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.d.ts +7 -0
  479. package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +113 -0
  480. package/dist/components/questions/types/word-fill-structured-form/register.js +36 -1
  481. package/dist/components/questions/types/word-fill-structured-form/transform.js +77 -1
  482. package/dist/components/questions/types/word-fill-structured-form/wfsf-word-bank.utils.js +110 -1
  483. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.d.ts +3 -0
  484. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +190 -0
  485. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupCreator.d.ts +3 -0
  486. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupCreator.js +182 -0
  487. package/dist/components/questions/types/word-order-and-match-group/index.d.ts +4 -0
  488. package/dist/components/questions/types/word-order-and-match-group/index.js +4 -0
  489. package/dist/components/questions/types/word-order-and-match-group/register.d.ts +2 -0
  490. package/dist/components/questions/types/word-order-and-match-group/register.js +121 -0
  491. package/dist/components/questions/types/word-order-and-match-group/transform.d.ts +2 -0
  492. package/dist/components/questions/types/word-order-and-match-group/transform.js +70 -0
  493. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +84 -1
  494. package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +192 -1
  495. package/dist/components/questions/types/word-ordering/index.js +2 -1
  496. package/dist/components/questions/types/word-ordering/register.js +46 -1
  497. package/dist/components/questions/types/word-ordering/transform.js +24 -1
  498. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +99 -1
  499. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +320 -1
  500. package/dist/components/questions/types/write-a-short-letter/index.js +2 -1
  501. package/dist/components/questions/types/write-a-short-letter/register.js +68 -1
  502. package/dist/components/questions/types/write-a-short-letter/transform.js +25 -1
  503. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +61 -1
  504. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +196 -1
  505. package/dist/components/questions/types/write-correct-verb-form/index.js +2 -1
  506. package/dist/components/questions/types/write-correct-verb-form/register.js +62 -1
  507. package/dist/components/questions/types/write-correct-verb-form/transform.js +17 -1
  508. package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +22 -1
  509. package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +174 -1
  510. package/dist/components/questions/types/write-sentences/index.js +2 -1
  511. package/dist/components/questions/types/write-sentences/register.js +58 -1
  512. package/dist/components/questions/types/write-sentences/transform.js +28 -1
  513. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +96 -1
  514. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +161 -1
  515. package/dist/components/questions/types/write-short-paragraph/index.js +2 -1
  516. package/dist/components/questions/types/write-short-paragraph/register.js +4 -1
  517. package/dist/components/questions/types/write-short-paragraph/transform.js +21 -1
  518. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.d.ts +8 -0
  519. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +11 -0
  520. package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +7 -0
  521. package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +8 -0
  522. package/dist/components/questions/viewer/QuestionGroupViewer.js +36 -1
  523. package/dist/components/questions/viewer/QuestionViewer.js +692 -1
  524. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +5 -0
  525. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +82 -0
  526. package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +8 -0
  527. package/dist/components/questions/viewer/TrueFalseGroupViewer.js +11 -0
  528. package/dist/components/questions/viewer/WordFillStructuredFormViewer.d.ts +9 -0
  529. package/dist/components/questions/viewer/WordFillStructuredFormViewer.js +14 -0
  530. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -0
  531. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +95 -0
  532. package/dist/components/questions/viewer/index.d.ts +6 -0
  533. package/dist/components/questions/viewer/index.js +8 -1
  534. package/dist/components/results/ResultReviewPageContainer.js +220 -1
  535. package/dist/components/results/ReviewQuestionRenderer.js +55 -1
  536. package/dist/components/results/hooks/useResultReviewPolling.js +76 -1
  537. package/dist/components/results/index.js +22 -1
  538. package/dist/components/results/renderers/QuestionExplanations.js +19 -1
  539. package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.js +94 -1
  540. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +75 -1
  541. package/dist/components/results/renderers/ReviewColoringRenderer.js +110 -1
  542. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +48 -1
  543. package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.js +87 -1
  544. package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.js +104 -1
  545. package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.js +123 -1
  546. package/dist/components/results/renderers/ReviewListeningHeader.js +104 -1
  547. package/dist/components/results/renderers/ReviewSpeakingAIPanel.js +116 -1
  548. package/dist/components/results/renderers/ReviewSpeakingMedia.js +48 -1
  549. package/dist/components/results/renderers/ReviewSpeakingQuestionItem.js +42 -1
  550. package/dist/components/results/renderers/ReviewSpeakingRenderer.js +12 -1
  551. package/dist/components/results/renderers/ReviewSpeakingRenderer.types.js +85 -1
  552. package/dist/components/results/renderers/review-question-body-dedicated.js +104 -1
  553. package/dist/components/results/renderers/review-question-body-movers.js +148 -1
  554. package/dist/components/results/renderers/review-speaking-map.js +226 -1
  555. package/dist/components/results/renderers/review-spontaneous-qa-group.d.ts +4 -0
  556. package/dist/components/results/renderers/review-spontaneous-qa-group.js +74 -0
  557. package/dist/components/results/report/BadgeTitle.js +5 -1
  558. package/dist/components/results/report/ExamSkillRow.js +20 -1
  559. package/dist/components/results/report/OceanEduExamReport.js +58 -1
  560. package/dist/components/results/report/OceanEduSpeakingReport.js +62 -1
  561. package/dist/components/results/report/OverallAssessmentSection.js +17 -1
  562. package/dist/components/results/report/ResultErrorState.js +18 -1
  563. package/dist/components/results/report/ResultLoadingState.js +10 -1
  564. package/dist/components/results/report/ResultNotPublicState.js +27 -1
  565. package/dist/components/results/report/SpeakingReportParts.js +14 -1
  566. package/dist/components/results/report/SpeakingReportShared.js +86 -1
  567. package/dist/components/results/report/SpeakingRubricSection.js +171 -1
  568. package/dist/components/results/report/speaking-report.utils.js +198 -1
  569. package/dist/components/results/result-review.utils.js +6 -1
  570. package/dist/components/results/review-data.js +222 -1
  571. package/dist/components/results/review-renderer.utils.js +97 -1
  572. package/dist/components/results/review-status.utils.js +36 -1
  573. package/dist/components/results/review-types.js +1 -1
  574. package/dist/components/results/speaking/SpeakingResultAudioPlayer.js +77 -1
  575. package/dist/components/results/speaking/SpeakingReviewResult.js +48 -1
  576. package/dist/components/results/utils/review-score.utils.js +72 -1
  577. package/dist/components/results/utils/speaking-summary.js +224 -1
  578. package/dist/components/results/utils/transform-speaking-review.js +183 -1
  579. package/dist/components/shared/RichTextEditor.d.ts +9 -0
  580. package/dist/components/shared/RichTextEditor.js +47 -0
  581. package/dist/components/shared/audio/SpeakingAudioPlayer.js +100 -1
  582. package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +140 -1
  583. package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +10 -1
  584. package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.d.ts +1 -1
  585. package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +18 -1
  586. package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +11 -1
  587. package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +11 -1
  588. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.d.ts +2 -1
  589. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +19 -1
  590. package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +13 -1
  591. package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.d.ts +1 -1
  592. package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +3 -1
  593. package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +6 -1
  594. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +5 -1
  595. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +9 -1
  596. package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.d.ts +2 -1
  597. package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +61 -1
  598. package/dist/components/themes/cambridge-yle/CambridgeYleTimer.d.ts +1 -1
  599. package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +25 -1
  600. package/dist/components/themes/cambridge-yle/index.js +13 -1
  601. package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +25 -1
  602. package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +89 -1
  603. package/dist/components/themes/english-certification/EcWordFillParagraph.js +56 -1
  604. package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +192 -1
  605. package/dist/components/themes/english-certification/EcWritingTask.js +60 -1
  606. package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +108 -1
  607. package/dist/components/themes/english-certification/EnglishCertificationBrand.js +11 -1
  608. package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +34 -1
  609. package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +15 -1
  610. package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +19 -1
  611. package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +166 -1
  612. package/dist/components/themes/english-certification/EnglishCertificationHeader.js +31 -1
  613. package/dist/components/themes/english-certification/EnglishCertificationInstructionBanner.js +79 -1
  614. package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +71 -1
  615. package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +79 -1
  616. package/dist/components/themes/english-certification/EnglishCertificationPartHeader.js +47 -1
  617. package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +77 -1
  618. package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +25 -1
  619. package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +36 -1
  620. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +81 -1
  621. package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +95 -1
  622. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +18 -1
  623. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +32 -1
  624. package/dist/components/themes/english-certification/english-cert-scroll.js +163 -1
  625. package/dist/components/themes/english-certification/english-certification-theme.js +58 -1
  626. package/dist/components/themes/english-certification/grouped-question-utils.js +243 -1
  627. package/dist/components/themes/english-certification/index.js +24 -1
  628. package/dist/components/themes/index.js +25 -1
  629. package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +170 -1
  630. package/dist/components/themes/summer-sky/SummerSkyExitModal.js +21 -1
  631. package/dist/components/themes/summer-sky/SummerSkyLayout.js +221 -1
  632. package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +18 -1
  633. package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +193 -1
  634. package/dist/components/themes/types.js +1 -1
  635. package/dist/components/ui/alert-dialog.js +26 -1
  636. package/dist/components/ui/alert.js +22 -1
  637. package/dist/components/ui/avatar.js +12 -1
  638. package/dist/components/ui/badge.js +38 -1
  639. package/dist/components/ui/button.js +35 -1
  640. package/dist/components/ui/calendar.js +76 -1
  641. package/dist/components/ui/card.js +16 -1
  642. package/dist/components/ui/carousel.js +91 -1
  643. package/dist/components/ui/checkbox.js +9 -1
  644. package/dist/components/ui/collapsible.js +13 -1
  645. package/dist/components/ui/command.js +34 -1
  646. package/dist/components/ui/confirm-dialog.js +49 -1
  647. package/dist/components/ui/dialog.js +23 -1
  648. package/dist/components/ui/drawer.js +35 -1
  649. package/dist/components/ui/dropdown-menu.js +33 -1
  650. package/dist/components/ui/file-upload.js +155 -1
  651. package/dist/components/ui/form.js +62 -1
  652. package/dist/components/ui/hover-card.js +10 -1
  653. package/dist/components/ui/image-preview.js +29 -1
  654. package/dist/components/ui/input.js +8 -1
  655. package/dist/components/ui/label.js +9 -1
  656. package/dist/components/ui/language-switcher.js +15 -1
  657. package/dist/components/ui/multi-select.js +90 -1
  658. package/dist/components/ui/points-input.js +33 -1
  659. package/dist/components/ui/popover.js +26 -1
  660. package/dist/components/ui/progress.js +16 -1
  661. package/dist/components/ui/radio-group.js +12 -1
  662. package/dist/components/ui/scroll-area.js +11 -1
  663. package/dist/components/ui/select.js +38 -1
  664. package/dist/components/ui/separator.js +8 -1
  665. package/dist/components/ui/sheet.js +40 -1
  666. package/dist/components/ui/single-select.js +76 -1
  667. package/dist/components/ui/skeleton.js +6 -1
  668. package/dist/components/ui/switch.js +8 -1
  669. package/dist/components/ui/table.js +20 -1
  670. package/dist/components/ui/tabs.js +17 -1
  671. package/dist/components/ui/textarea.js +8 -1
  672. package/dist/components/ui/tooltip.js +11 -1
  673. package/dist/index.d.ts +3 -1
  674. package/dist/index.js +24 -1
  675. package/dist/shared/constants/ApiConstant.d.ts +1 -0
  676. package/dist/shared/constants/ApiConstant.js +278 -1
  677. package/dist/shared/constants/QueryKeyConstant.js +112 -1
  678. package/dist/shared/constants/exam-level.enum.js +41 -1
  679. package/dist/shared/constants/landings/placement-test.constants.js +431 -1
  680. package/dist/shared/constants/question-skills.js +277 -1
  681. package/dist/shared/constants/routes.js +81 -1
  682. package/dist/shared/index.js +10 -1
  683. package/dist/shared/lib/hooks/index.js +10 -1
  684. package/dist/shared/lib/hooks/useAlertDialog.js +57 -1
  685. package/dist/shared/lib/hooks/useAudioPlayCounter.js +30 -1
  686. package/dist/shared/lib/hooks/useAudioPreview.js +81 -1
  687. package/dist/shared/lib/hooks/useAudioUpload.js +24 -1
  688. package/dist/shared/lib/hooks/useConfirmDialog.js +89 -1
  689. package/dist/shared/lib/hooks/useDebouncedCallback.js +32 -1
  690. package/dist/shared/lib/hooks/useDelayedAutoPlay.js +12 -1
  691. package/dist/shared/lib/hooks/useFileUpload.js +24 -1
  692. package/dist/shared/lib/hooks/useImageUrl.js +5 -1
  693. package/dist/shared/lib/hooks/usePresignedFileUrl.js +108 -1
  694. package/dist/shared/lib/hooks/useReactHookForm.js +31 -1
  695. package/dist/shared/lib/hooks/useToast.js +62 -1
  696. package/dist/shared/lib/html-content.js +22 -1
  697. package/dist/shared/lib/i18n/index.js +54 -1
  698. package/dist/shared/lib/i18n/messages/en/admin.d.ts +11 -4
  699. package/dist/shared/lib/i18n/messages/en/admin.js +2111 -1
  700. package/dist/shared/lib/i18n/messages/en/auth.js +86 -1
  701. package/dist/shared/lib/i18n/messages/en/common.js +140 -1
  702. package/dist/shared/lib/i18n/messages/en/file-upload.js +18 -1
  703. package/dist/shared/lib/i18n/messages/en/student.js +133 -1
  704. package/dist/shared/lib/i18n/messages/en/terms.js +37 -1
  705. package/dist/shared/lib/i18n/messages/en.d.ts +11 -4
  706. package/dist/shared/lib/i18n/messages/en.js +15 -1
  707. package/dist/shared/lib/i18n/messages/vi/admin.d.ts +11 -4
  708. package/dist/shared/lib/i18n/messages/vi/admin.js +2111 -1
  709. package/dist/shared/lib/i18n/messages/vi/auth.js +86 -1
  710. package/dist/shared/lib/i18n/messages/vi/common.js +140 -1
  711. package/dist/shared/lib/i18n/messages/vi/file-upload.js +18 -1
  712. package/dist/shared/lib/i18n/messages/vi/student.js +133 -1
  713. package/dist/shared/lib/i18n/messages/vi/terms.js +37 -1
  714. package/dist/shared/lib/i18n/messages/vi.d.ts +11 -4
  715. package/dist/shared/lib/i18n/messages/vi.js +15 -1
  716. package/dist/shared/lib/i18n/types.js +1 -1
  717. package/dist/shared/lib/stores/examQuestionStore.js +212 -1
  718. package/dist/shared/lib/stores/examTakingStore.js +240 -1
  719. package/dist/shared/lib/stores/gradingTransientStore.js +39 -1
  720. package/dist/shared/lib/stores/localeStore.js +10 -1
  721. package/dist/shared/lib/themes/config/starters.config.js +71 -1
  722. package/dist/shared/lib/themes/index.js +49 -1
  723. package/dist/shared/lib/themes/types.js +2 -1
  724. package/dist/shared/lib/toast.js +43 -1
  725. package/dist/shared/lib/utils/public-page.js +44 -1
  726. package/dist/shared/lib/utils/question-reverse-transform.js +438 -1
  727. package/dist/shared/lib/utils/question-transform-types.js +1 -1
  728. package/dist/shared/lib/utils/question-transform-validation.js +194 -1
  729. package/dist/shared/lib/utils/question-transform.js +199 -1
  730. package/dist/shared/lib/utils.js +168 -1
  731. package/dist/shared/lib/validation/choose-correct-answer.validation.js +111 -1
  732. package/dist/shared/lib/validation/label-the-picture.validation.js +19 -1
  733. package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +46 -1
  734. package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +21 -1
  735. package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +34 -1
  736. package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +40 -1
  737. package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +45 -1
  738. package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +19 -1
  739. package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +39 -1
  740. package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +32 -1
  741. package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +19 -1
  742. package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +28 -1
  743. package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +12 -1
  744. package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +47 -1
  745. package/dist/shared/lib/validation/read-and-color-objects.validation.js +29 -1
  746. package/dist/shared/lib/validation/read-displayed-content.validation.js +28 -1
  747. package/dist/shared/lib/validation/speaking-conversation.validation.js +75 -1
  748. package/dist/shared/lib/validation/speaking-describe-image.validation.js +31 -1
  749. package/dist/shared/lib/validation/spontaneous-qa.validation.js +48 -1
  750. package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +38 -1
  751. package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +39 -1
  752. package/dist/shared/lib/validation/word-fill-paragraph.validation.js +43 -1
  753. package/dist/shared/lib/validation/word-ordering.validation.js +33 -1
  754. package/dist/shared/types/branch.types.js +6 -1
  755. package/dist/shared/types/common.types.d.ts +1 -1
  756. package/dist/shared/types/common.types.js +49 -1
  757. package/dist/shared/types/entities/exam-schedule.types.js +5 -1
  758. package/dist/shared/types/entities/exam.types.js +1 -1
  759. package/dist/shared/types/entities/question.types.js +4 -1
  760. package/dist/shared/types/entities/result.types.js +4 -1
  761. package/dist/shared/types/entities/user.types.js +4 -1
  762. package/dist/shared/types/exam-taking.types.js +2 -1
  763. package/dist/shared/types/history.types.js +34 -1
  764. package/dist/shared/types/index.js +1 -1
  765. package/dist/shared/types/questions/choose-correct-adjective.js +4 -1
  766. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  767. package/dist/shared/types/questions/choose-the-correct-answer-group.js +1 -1
  768. package/dist/shared/types/questions/choose-the-correct-answer.js +9 -1
  769. package/dist/shared/types/questions/cross-out-word-group.d.ts +2 -0
  770. package/dist/shared/types/questions/cross-out-word-group.js +1 -1
  771. package/dist/shared/types/questions/fill-missing-words-in-grid.js +11 -1
  772. package/dist/shared/types/questions/index.d.ts +1 -0
  773. package/dist/shared/types/questions/index.js +46 -1
  774. package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
  775. package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
  776. package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
  777. package/dist/shared/types/questions/listen-and-speak-info-exchange.js +3 -1
  778. package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +3 -1
  779. package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
  780. package/dist/shared/types/questions/listen-and-speak-with-story-images.js +3 -1
  781. package/dist/shared/types/questions/look-picture-choose-correct-answer.js +5 -1
  782. package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +9 -1
  783. package/dist/shared/types/questions/look-picture-fill-word-hint.js +7 -1
  784. package/dist/shared/types/questions/read-displayed-content.js +1 -1
  785. package/dist/shared/types/questions/speaking-conversation.js +7 -1
  786. package/dist/shared/types/questions/speaking-describe-image.js +1 -1
  787. package/dist/shared/types/questions/spontaneous-qa.js +1 -1
  788. package/dist/shared/types/questions/word-order-and-match-group.d.ts +33 -0
  789. package/dist/shared/types/questions/word-order-and-match-group.js +1 -0
  790. package/dist/shared/types/questions/write-correct-verb-form.js +12 -1
  791. package/dist/shared/types/role.types.js +1 -1
  792. package/dist/shared/types/structure-management.js +28 -1
  793. package/dist/shared/types/student.types.js +57 -1
  794. package/dist/shared/types/teacher-dashboard-rooms.js +2 -1
  795. package/package.json +184 -165
  796. package/dist/mcp/config.d.ts +0 -12
  797. package/dist/mcp/config.js +0 -1
  798. package/dist/mcp/http-client.d.ts +0 -6
  799. package/dist/mcp/http-client.js +0 -1
  800. package/dist/mcp/index.d.ts +0 -6
  801. package/dist/mcp/index.js +0 -2
  802. package/dist/mcp/tools/_helpers.d.ts +0 -4
  803. package/dist/mcp/tools/_helpers.js +0 -1
  804. package/dist/mcp/tools/exams.d.ts +0 -2
  805. package/dist/mcp/tools/exams.js +0 -1
  806. package/dist/mcp/tools/index.d.ts +0 -2
  807. package/dist/mcp/tools/index.js +0 -1
  808. package/dist/mcp/tools/results.d.ts +0 -2
  809. package/dist/mcp/tools/results.js +0 -1
@@ -1 +1,1137 @@
1
- export function groupQuestionsByPart(t,e){const n=new Map,o=new Map;for(const e of t){const t=o.get(e.part_id)||[];t.push(e),o.set(e.part_id,t)}for(const[t,r]of o.entries()){const o=[...r].sort((t,e)=>t.question_number-e.question_number),i=o[0];if(!i)continue;const s=e?.find(e=>String(e.id)===String(t)),a=void 0!==s?.id?Number(s.id):void 0,u=s?.name?.match(/\d+/),c=u?parseInt(u[0],10):i.part_no,l=s?.group,m=Boolean(s?.isGroup)&&"DOCUMENT"===l?.type,p=s?.sections??[],d=new Set(p.map(t=>t.questionType).filter(Boolean)),g=p.length>1&&d.size>1;if(m||g){const e=`${t}_part`;n.set(e,{partId:t,partNo:c,questionType:i.question_type,questions:o,name:s?.name,instructions:s?.instructions,audioUrl:s?.audioUrl,templateOrder:a,category:s?.category,points:s?.points,isGroup:!0,group:l,sections:s?.sections,startIndex:s?.startIndex,endIndex:s?.endIndex});continue}if(!s?.sections?.length){const e=`${t}_part`;n.set(e,{partId:t,partNo:c,questionType:i.question_type,questions:o,name:s?.name,instructions:s?.instructions,audioUrl:s?.audioUrl,templateOrder:a,category:s?.category,points:s?.points,isGroup:s?.isGroup,optionMode:s?.optionMode,optionCount:s?.optionCount,viewMode:s?.viewMode,defaultValue:s?.defaultValue,multipleAnswers:s?.multipleAnswers,questionConfig:s?.questionConfig,wordBankConfig:s?.wordBankConfig,group:s?.group,startIndex:s?.startIndex,endIndex:s?.endIndex});continue}for(const[e,r]of s.sections.entries()){const i=o.filter(t=>{const e=t.question_number-1;return e>=r.startIndex&&e<=r.endIndex});if(!i.length)continue;const u=`${t}_section_${e}`,l=Boolean(r.isGroup||r.group?.type);n.set(u,{partId:t,partNo:c,questionType:r.questionType||i[0].question_type,questions:i,name:s.name,instructions:r.instructions||r.name||s.instructions,audioUrl:s.audioUrl,templateOrder:a,category:s.category,points:s.points,sectionId:r.sectionId,isGroup:l,optionMode:r.optionMode,optionCount:r.optionCount,viewMode:r.viewMode,defaultValue:r.defaultValue,multipleAnswers:r.multipleAnswers,questionConfig:r.questionConfig,wordBankConfig:r.wordBankConfig,group:r.group,startIndex:r.startIndex,endIndex:r.endIndex})}}return Array.from(n.values()).sort((t,e)=>{const n=void 0===t.templateOrder||isNaN(t.templateOrder)?1/0:t.templateOrder,o=void 0===e.templateOrder||isNaN(e.templateOrder)?1/0:e.templateOrder;return n!==o?n-o:(t.questions[0]?.question_number||0)-(e.questions[0]?.question_number||0)})}export function transformFillInBlank(t){const e=t[0],n=e?.content,o=e?.questionGroup?.payload,r=[];for(const e of t){const t=e.content,n=e.is_example||!1,o=t.question||t.blanks?.[0]?.text||"";if([...o.matchAll(/\{(\d+)\}/g)].length>1){const i=o.split("\n").filter(t=>t.trim());for(const o of i){const i=o.match(/\{(\d+)\}/);if(!i)continue;const s=parseInt(i[1],10),a=o.split(/\{\d+\}/),u=a[0]?.trim()||"",c=a[1]?.trim()||"";r.push({id:`${e.id}-blank-${s}`,questionNumber:e.question_number,textBefore:u,textAfter:c,imageUrl:t.imageUrl,hints:t.hints,isExample:n,correctAnswer:n?e.correct_answer?.answers?.[s]:void 0})}}else{const i=o.match(/^([\s\S]*?)\{0\}([\s\S]*)$/),s=i?i[1].trim():o,a=i?i[2].trim():"";r.push({id:e.id,questionNumber:e.question_number,textBefore:s,textAfter:a,imageUrl:t.imageUrl,hints:t.hints,isExample:n,correctAnswer:n?e.correct_answer?.answers?.[0]:void 0})}}return{questions:r,title:n?.title||o?.title||"",imageUrl:o?n?.imageUrl:void 0,audioUrl:n?.audioUrl,exampleText:n?.exampleText,exampleHighlight:n?.exampleHighlight,instruction:n?.instruction||"LISTEN AND WRITE",showHints:n?.showHints,groupContent:o?.content||o?.groupContent,groupTitle:o?.title,groupImageUrl:o?.mainImageUrl||o?.imageUrl}}export function transformWriteCorrectVerbForm(t){const e=t[0],n=e?.content;return{questions:t.map(t=>{const e=t.content,n=e.sentence||"",o=n.match(/^([\s\S]*?)\{0\}([\s\S]*)$/),r=o?o[1].trim():n,i=o?o[2].trim():"";return{id:t.id,questionNumber:t.question_number,textBefore:r,textAfter:i,verbHint:e.verbHint}}),title:"WRITE THE CORRECT VERB FORM",instruction:n?.instruction||"Write the correct form of the verb in parentheses."}}const t=["A","B","C","D","E","F"];export function transformChooseCorrectAnswer(e){const n=e[0]?.content,o=e[0]?.questionGroup?.payload,r=e.find(t=>t.is_example),i=e.filter(t=>!t.is_example);let s;if(r){const e=r.content,n=(e.options||[]).map((e,n)=>{const o="object"==typeof e&&null!==e,i=o&&e.id?String(e.id):`${r.id}-opt-${n}`,s=o?e.text??e.content??"":String(e),a=o?e.imageUrl:void 0;return{id:i,label:t[n]||String(n+1),text:s,...a?{imageUrl:a}:{}}}),o=r.correct_answer?.answer??r.correct_answer?.optionId;let i="";if("string"==typeof o&&o){const t=n.find(t=>t.id===o);i=t?t.id:o}else"number"==typeof o&&o>=0&&o<n.length&&(i=n[o]?.id||"");s={questionNumber:r.question_number,questionText:e.question||"",options:n,correctOptionId:i||n[0]?.id||"",optionType:e.optionType||"text",imageUrl:e.imageUrl,audioUrl:e.audioUrl}}const a=i.map(e=>{const n=e.content,o=n.optionType||"text",r=(n.options||[]).map((n,o)=>{const r="object"==typeof n&&null!==n,i=r&&n.id?String(n.id):`${e.id}-opt-${o}`,s=r?n.text??n.content??"":String(n),a=r?n.imageUrl:void 0;return{id:i,label:t[o]||String(o+1),text:s,...a?{imageUrl:a}:{}}}),i=e.correct_answer?.answer??e.correct_answer?.optionId??e.correct_answer?.optionIds,s=t=>{if(null==t)return;const e=String(t),n=r.find(t=>t.id===e);if(n)return n.id;const o=e.match(/-opt-(\d+)$/);if(o){const t=parseInt(o[1],10);if(t>=0&&t<r.length)return r[t]?.id}if("number"==typeof t&&t>=0&&t<r.length)return r[t]?.id;const i=parseInt(e,10);return!isNaN(i)&&i>=0&&i<r.length&&!e.startsWith("opt-")?r[i]?.id:e};let a=[];if(Array.isArray(i))a=i.map(s).filter(t=>Boolean(t));else{const t=s(i);t&&(a=[t])}return{id:e.id,questionNumber:e.question_number,questionText:n.question||"",options:r,correctOptionId:a[0],correctOptionIds:a,imageUrl:n.imageUrl,audioUrl:n.audioUrl,optionType:o,multipleAnswers:Boolean(n.multipleAnswers||Array.isArray(i)),explanation:e.explanation}}),u=a.some(t=>!!t.audioUrl?.trim())||!!s?.audioUrl?.trim();return{questions:a,example:s,imageUrl:n?.imageUrl,groupImageUrl:o?.mainImageUrl||o?.imageUrl,groupTitle:o?.title,groupContent:o?.content,articles:o?.articles,audioUrl:u?void 0:n?.audioUrl,instruction:n?.instruction||"CHOOSE THE CORRECT ANSWER"}}export function transformChooseTheCorrectAnswerGroup(e){const n=e[0],o=n?.content,r=Array.isArray(o?.items)?o.items:[],i=Array.isArray(n?.correct_answer?.answer)?n.correct_answer.answer:[],s=r.find(t=>t.isExample),a=r.filter(t=>!t.isExample),u=(e,n)=>(e||[]).map((e,o)=>{const r="object"==typeof e&&null!==e;return{id:r&&e.id?String(e.id):`${n}-opt-${o+1}`,label:t[o]||String(o+1),text:String(r?e.text??"":e),...r&&e.imageUrl?{imageUrl:e.imageUrl}:{}}});let c;if(s){const t=r.indexOf(s),e=u(s.content?.options,`${n.id}-ex`),o=s.correctAnswer?.answer||i[t]||e[0]?.id||"";c={questionNumber:s.questionNumber||t+1,questionText:s.content?.question||"",options:e,correctOptionId:o,optionType:s.content?.optionType||"text"}}return{questions:a.map(t=>{const e=r.indexOf(t),o=u(t.content?.options,`${n.id}-item-${e}`),s=t.correctAnswer?.answer||i[e]||"";return{id:`${n.id}-item-${e}`,questionNumber:t.questionNumber||e+1,questionText:t.content?.question||"",options:o,correctOptionId:s,correctOptionIds:s?[s]:[],optionType:t.content?.optionType||"text"}}),example:c,instruction:o?.meta?.instruction||"Choose the correct answer."}}export function transformChooseCorrectAdjective(e){return{questions:e.map(e=>{const n=e.content,o=n.sentence||"",r=o.match(/^([\s\S]*?)\{0\}([\s\S]*)$/),i=r?r[1]:o,s=r?r[2]:"";return{id:e.id,questionNumber:e.question_number,sentenceBefore:i,sentenceAfter:s,options:(n.options||[]).map((n,o)=>({id:`${e.id}-opt-${o}`,label:t[o]||String(o+1),text:n}))}}),instruction:"CHOOSE THE CORRECT ADJECTIVE"}}export function transformWriteShortLetter(t){const e=t.content;if(e.instructions?.mainInstruction||e.informations?.length)return{prompt:e.instructions?.mainInstruction||"",requirements:e.informations,minWords:e.minWords,maxWords:e.maxWords,guideImages:e.guideImages};return{prompt:e.contexts?.filter(t=>"text"===t.type).map(t=>t.text).join("\n")||"",minWords:e.minWords,maxWords:e.maxWords,guideImages:e.guideImages}}export function transformColorObjects(t){const e=t[0],n=e?.content,o=n?.colorNodes?.map(t=>({id:t.id,color:t.color,label:t.label}))||n?.hintItems||[],r=n?.regionNodes?.map(t=>({id:t.id,x:t.x,y:t.y,width:t.width,height:t.height,correctHintId:t.linkedColorNodeId}))||n?.regions||[],i=n?.instructions;return{instruction:(i&&i.length>0?i.join(" "):n?.instruction)||"READ AND COLOR THE OBJECTS",instructions:i,imageUrl:n?.backgroundImageUrl||n?.imageUrl||"",audioUrl:n?.audioUrl,hintItems:o,regions:r}}export function transformPictureChoose(e){const n=e[0]?.content;return{subQuestions:e.map(e=>{const n=e.content;return{id:e.id,questionNumber:e.question_number,imageUrl:n.imageUrl||"",questionText:n.questionText,options:(n.options||[]).map((n,o)=>({id:n.id||`${e.id}-opt-${o}`,label:t[o]||String(o+1),text:"string"==typeof n?n:n.text}))}}),instruction:n?.instruction||"LOOK AT THE PICTURE AND CHOOSE THE CORRECT ANSWER"}}export function transformPictureFillBlankChoose(e){const n=e[0]?.content;return{subQuestions:e.map(e=>{const n=e.content;return{id:e.id,questionNumber:e.question_number,imageUrl:n.imageUrl||"",question:n.question||"",options:(n.options||[]).map((n,o)=>({id:n.id||`${e.id}-opt-${o}`,label:t[o]||String(o+1),text:"string"==typeof n?n:n.text}))}}),instruction:n?.instruction||"LOOK AT THE PICTURE, FILL IN THE BLANK AND CHOOSE THE ANSWER"}}export function transformReadingPassage(e){if(function(t){const e=t[0];if(!e)return!1;const n=e.content;return void 0!==n?.question&&!n?.questions&&void 0!==e.questionGroup?.payload?.passage}(e)){const n=e[0],o=n.questionGroup?.payload||{},r=e.map(e=>{const n=e.content;return{id:e.id,questionNumber:e.question_number,questionText:n?.question||"",options:(n?.options||[]).map((n,o)=>({id:`${e.id}-opt-${o}`,label:t[o]||String(o+1),text:"string"==typeof n?n:String(n)}))}});return{passage:o.passage||"",passageTitle:o.passageTitle,passageImageUrl:o.passageImageUrl||o.mainImageUrl,instruction:o.instructions||"READ THE PASSAGE AND ANSWER QUESTIONS",questions:r}}const n=e[0],o=n?.content,r=o?.questions?.map((e,o)=>({id:e.id||`${n.id}-sq-${o}`,questionNumber:o+1,questionText:e.question,options:(e.options||[]).map((o,r)=>({id:`${e.id||n.id}-opt-${r}`,label:t[r]||String(r+1),text:o}))}))||[];return{passage:o?.passage||"",passageTitle:o?.passageTitle,passageImageUrl:o?.passageImageUrl||o?.mainImageUrl,instruction:o?.instructions?.mainInstruction||"READ THE PASSAGE AND ANSWER QUESTIONS",questions:r}}export function transformLabelPicture(t){const e=t[0],n=e?.content;return{imageUrl:n?.imageUrl||"",wordBank:n?.wordBank||[],labels:n?.labels?.map(t=>({id:t.id,number:t.number,isPreFilled:t.isPreFilled||!1,correctAnswer:t.correctAnswer}))||[],instruction:n?.instruction||"LABEL THE PICTURE"}}function e(t){return t&&"string"==typeof t.word&&!t.subQuestions}export function transformFillWordHint(t){const n=t[0],o=n?.content;if(t.length>1&&t.every(t=>e(t.content))){const e=n.questionGroup?.payload?.hintLetters||[];return{subQuestions:t.map(t=>{const e=t.content,n=e.word.replace(/\{\d+\}/,"_");return{id:t.id,questionNumber:t.question_number,imageUrl:e.imageUrl||"",word:n}}),hintLetters:e,instruction:o?.instruction||"LOOK AT THE PICTURE AND FILL IN THE MISSING LETTER"}}if(e(o)){const t=o,e=n.questionGroup?.payload?.hintLetters||[];return{subQuestions:function(t){const e=[],n=t.split(",").map(t=>t.trim());for(const t of n){let n;const o=/\{(\d+)\}/g;for(;null!==(n=o.exec(t));){const o=parseInt(n[1],10),r=t.replace(n[0],"_");e.push({index:o,displayWord:r})}}return e.sort((t,e)=>t.index-e.index),e}(t.word).map(e=>({id:`${n.id}-blank-${e.index}`,questionNumber:n.question_number,imageUrl:t.imageUrl||"",word:e.displayWord,blankIndex:e.index})),hintLetters:e,instruction:t.instruction||"LOOK AT THE PICTURE AND FILL IN THE MISSING LETTER"}}const r=o;return{subQuestions:r?.subQuestions?.map((t,e)=>({id:t.id,questionNumber:n.question_number+e,imageUrl:t.imageUrl||"",word:t.word||""}))||[],hintLetters:r?.hintLetters||n.questionGroup?.payload?.hintLetters||[],instruction:r?.instruction||"LOOK AT THE PICTURE AND FILL IN THE MISSING LETTER"}}export function transformGridFill(t){const e=t[0],n=e?.content;return{grid:n?.grid||[],rows:n?.rows||0,columns:n?.columns||0,instruction:n?.instruction||"FILL IN THE MISSING WORDS",disabledCells:n?.disabledCells||[]}}export function transformAnswerTheQuestion(t){const e=t[0],n=e?.questionGroup?.payload,o=e?.content;return{questions:t.map(t=>{const e=t.content,n=t.is_example||e.isExample||!1;return{id:t.id,questionNumber:t.question_number,question:(e.question||"").replace(/\{\d+\}/g,"______"),imageUrl:e.imageUrl,prefixText:e.prefixText,suffixText:e.suffixText,suggestedAnswers:e.suggestedAnswers,isExample:n,exampleAnswer:n?t.correct_answer?.answer:void 0}}),instruction:"ANSWER THE QUESTION",imageUrl:n?.mainImageUrl||n?.imageUrl||o?.imageUrl,title:o?.title,groupContent:n?.groupContent||o?.groupContent}}export function transformTrueFalse(t){const e=t[0]?.questionGroup?.payload;return{questions:t.map(t=>{const e=t.content,n=t.is_example||e.isExample||!1,o=n&&"boolean"==typeof t.correct_answer?.answer?t.correct_answer.answer:void 0;return{id:t.id,questionNumber:t.question_number,question:e.question||"",imageUrl:e.imageUrl,isExample:n,exampleCorrectAnswer:o}}),instruction:"READ AND CHOOSE TRUE OR FALSE",imageUrl:e?.mainImageUrl||e?.imageUrl,groupContent:e?.content||e?.groupContent}}export function transformWordOrdering(t){return{questions:t.map(t=>{const e=t.content,n=t.is_example||e.isExample||!1,o=Array.isArray(t.correct_answer?.answer)?t.correct_answer.answer:Array.isArray(e.answer)?e.answer:void 0;return{id:t.id,questionNumber:t.question_number,words:e.words||[],imageUrl:e.imageUrl,isExample:n,exampleAnswer:n?o:void 0}}),instruction:"ARRANGE THE LETTERS TO MAKE A WORD"}}export function transformWordFillParagraph(t){const e=t[0],n=e?.questionGroup?.payload,o=e?.content,r=o?.title||n?.title||t.find(t=>t.content?.title)?.content?.title,i=o?.wordBank||[],s=i.map(t=>"string"==typeof t?{word:t}:t),a=o?.viewMode,u=void 0!==o?.wordBank?.[0]?.options;let c;c="EMAIL"===a?"EMAIL":a||!o?.emailFrom&&!o?.emailTo?"DEFAULT"===a?"DEFAULT":"WITH_OPTIONS"===a||u?"WITH_OPTIONS":a||0!==i.length?"WITH_WORD_BANK":"DEFAULT":"EMAIL";const l=o?.emailFrom||o?.metadata?.emailFrom,m=o?.emailTo||o?.metadata?.emailTo;return{questionId:e?.id||"",paragraph:o?.paragraph||"",title:r,imageUrl:o?.imageUrl,wordBank:s,viewMode:c,instruction:"READ AND FILL IN THE BLANKS",exampleAnswer:o?.exampleAnswer,emailFrom:l,emailTo:m}}export function transformMatchByWritingAnswer(t){const e=t[0]?.questionGroup?.payload,n=t.some(t=>!!t.content?.askerName),o=t.map(t=>{const e=t.content,n=t.is_example||e.isExample||!1;return{id:t.id,questionNumber:t.question_number,question:e.question||"",imageUrl:e.imageUrl,isExample:n,exampleAnswer:n?t.correct_answer?.answer:void 0,askerName:e.askerName,answererName:e.answererName}}),r=e?.options?.map(t=>({answer:t.answer,imageUrl:t.imageUrl||"",content:t.content||""}))||[],i=n?t[0]?.content:void 0;return{questions:o,options:r,instruction:"LOOK AND WRITE THE ANSWER",viewMode:n?"CONVERSATION":"DEFAULT",groupImageUrl:e?.groupImageUrl||"",askerName:i?.askerName,answererName:i?.answererName}}export function transformWriteSentences(t){const e=t[0],n=e?.content,o=e?.questionGroup?.payload;return{questions:t.map(t=>({id:t.id,questionNumber:t.question_number})),instruction:n?.instruction||"WRITE SENTENCES ABOUT THE PICTURE",imageUrl:o?.mainImageUrl||o?.imageUrl||n?.imageUrl,title:n?.title,groupContent:o?.groupContent||n?.groupContent}}export function transformWordFillStructuredForm(t){const e=t[0],n=e?.questionGroup?.payload;return{questions:t.map(t=>{const e=t.content,n=t.correct_answer||{};let o=e?.content||"";const r=e?.blocks;return!o&&r?.length&&(o=r.map(t=>(t.heading?`<h3>${t.heading}</h3>`:"")+(t.content||"")).join("")),{id:t.id,questionNumber:t.question_number,title:e?.title||"",imageUrl:e?.imageUrl,content:o,answers:n?.answers||{},caseSensitive:n?.caseSensitive??e?.caseSensitive??!1}}),instruction:n?.instructions||"COMPLETE THE FORM BELOW",groupTitle:n?.title,groupContent:n?.content,groupImageUrl:n?.imageUrl,audioUrl:e?.audio_url||n?.audioUrl}}export function transformListenChooseObjects(t){const e=t[0],n=e?.content,o=e?.questionGroup?.payload||{};return{audioUrl:n?.audioUrl||"",backgroundImageUrl:o.backgroundImageUrl||"",objectNodes:o.objectNodes||[],correctObjectId:e?.correctAnswer?.objectId||e?.correct_answer?.objectId||"",isBackup:n?.isBackup||!1,instruction:"LISTEN AND CHOOSE THE CORRECT OBJECT"}}export function transformListenSpeakAnswer(t){return{questions:t.map(t=>{const e=t.content,n=t.questionGroup?.payload||{};return{id:t.id,questionNumber:t.question_number,audioUrl:e?.audioUrl||"",arrowNode:e?.arrowNode,backgroundImageUrl:n.backgroundImageUrl||n.imageUrl||"",isBackup:e?.isBackup||!1}}),instruction:"LISTEN AND SPEAK YOUR ANSWER"}}
1
+ /**
2
+ * Question Transformers - Transform API response to renderer props
3
+ *
4
+ * Maps backend question types to frontend renderer props.
5
+ * Each transformer handles specific question type data structure.
6
+ */
7
+ /**
8
+ * Group questions by part_id AND questionGroup.id
9
+ * This ensures questions with different passages (different questionGroups)
10
+ * within the same part are properly separated.
11
+ *
12
+ * Handles template sections: parts with `sections` array are split into
13
+ * multiple QuestionPart entries with section-specific questionTypes.
14
+ * Used for Movers P8 (2 sections) and P11 (3 sections).
15
+ *
16
+ * @param questions - API questions to group
17
+ * @param templateParts - Optional template parts with name/instructions metadata
18
+ */
19
+ export function groupQuestionsByPart(questions, templateParts) {
20
+ const partMap = new Map();
21
+ const questionsByPart = new Map();
22
+ for (const q of questions) {
23
+ const existing = questionsByPart.get(q.part_id) || [];
24
+ existing.push(q);
25
+ questionsByPart.set(q.part_id, existing);
26
+ }
27
+ for (const [partId, partQuestions] of questionsByPart.entries()) {
28
+ const sortedPartQuestions = [...partQuestions].sort((a, b) => a.question_number - b.question_number);
29
+ const firstQuestion = sortedPartQuestions[0];
30
+ if (!firstQuestion)
31
+ continue;
32
+ // Find template part to get name/instructions metadata
33
+ const templatePart = templateParts?.find((tp) => String(tp.id) === String(partId));
34
+ const templateOrder = templatePart?.id !== undefined ? Number(templatePart.id) : undefined;
35
+ // Extract partNo from template name (e.g. "Part 8") or fallback to question's part_no
36
+ const partNameMatch = templatePart?.name?.match(/\d+/);
37
+ const parsedPartNo = partNameMatch ? parseInt(partNameMatch[0], 10) : firstQuestion.part_no;
38
+ // IELTS Reading: a part carries a part-level DOCUMENT group (the shared
39
+ // passage) AND sections with mixed sub-types. Render it like TOEIC Part 7 —
40
+ // a single screen with the passage on the left and ALL questions on the
41
+ // right — so keep it whole and pass the sections through for the renderer.
42
+ const partLevelGroup = templatePart?.group;
43
+ const hasDocumentGroup = Boolean(templatePart?.isGroup) && partLevelGroup?.type === 'DOCUMENT';
44
+ // Mixed multi-section part (e.g. Listening SECTION 2: CHOOSE + MATCH +
45
+ // WORD_FILL): keep it whole and pass the sections through so the grouped
46
+ // renderer can stack each sub-section on one screen. Without this the part
47
+ // would be split into one screen per section and only the first sub-section
48
+ // would be reachable.
49
+ const templateSections = (templatePart?.sections ?? []);
50
+ const distinctSectionTypes = new Set(templateSections.map((s) => s.questionType).filter(Boolean));
51
+ const isMixedMultiSection = templateSections.length > 1 && distinctSectionTypes.size > 1;
52
+ if (hasDocumentGroup || isMixedMultiSection) {
53
+ const key = `${partId}_part`;
54
+ partMap.set(key, {
55
+ partId,
56
+ partNo: parsedPartNo,
57
+ questionType: firstQuestion.question_type,
58
+ questions: sortedPartQuestions,
59
+ name: templatePart?.name,
60
+ instructions: templatePart?.instructions,
61
+ audioUrl: templatePart?.audioUrl,
62
+ templateOrder,
63
+ category: templatePart?.category,
64
+ points: templatePart?.points,
65
+ isGroup: true,
66
+ group: partLevelGroup,
67
+ sections: templatePart?.sections,
68
+ startIndex: templatePart?.startIndex,
69
+ endIndex: templatePart?.endIndex,
70
+ });
71
+ continue;
72
+ }
73
+ // Parts without sections should render as exactly one screen per part
74
+ if (!templatePart?.sections?.length) {
75
+ const key = `${partId}_part`;
76
+ partMap.set(key, {
77
+ partId,
78
+ partNo: parsedPartNo,
79
+ questionType: firstQuestion.question_type,
80
+ questions: sortedPartQuestions,
81
+ name: templatePart?.name,
82
+ instructions: templatePart?.instructions,
83
+ audioUrl: templatePart?.audioUrl,
84
+ templateOrder,
85
+ category: templatePart?.category,
86
+ points: templatePart?.points,
87
+ isGroup: templatePart?.isGroup,
88
+ optionMode: templatePart?.optionMode,
89
+ optionCount: templatePart?.optionCount,
90
+ viewMode: templatePart?.viewMode,
91
+ defaultValue: templatePart?.defaultValue,
92
+ multipleAnswers: templatePart?.multipleAnswers,
93
+ questionConfig: templatePart?.questionConfig,
94
+ wordBankConfig: templatePart?.wordBankConfig,
95
+ group: templatePart?.group,
96
+ startIndex: templatePart?.startIndex,
97
+ endIndex: templatePart?.endIndex,
98
+ });
99
+ continue;
100
+ }
101
+ // Parts with sections should render as exactly one screen per section.
102
+ // Template startIndex/endIndex are stored as zero-based exam-order positions,
103
+ // while API question_number is one-based. Convert question_number → zero-based
104
+ // position before matching, otherwise mixed-section parts (e.g. IELTS Part 3)
105
+ // can render the wrong heading and only a tail subset of questions.
106
+ for (const [sectionIdx, section] of templatePart.sections.entries()) {
107
+ const sectionQuestions = sortedPartQuestions.filter((question) => {
108
+ const templatePosition = question.question_number - 1;
109
+ return templatePosition >= section.startIndex && templatePosition <= section.endIndex;
110
+ });
111
+ if (!sectionQuestions.length)
112
+ continue;
113
+ const key = `${partId}_section_${sectionIdx}`;
114
+ // A section is a grouped question when the template marks it isGroup OR it
115
+ // carries a nested group payload (e.g. IELTS SECTION 2 DOCUMENT/MATCH_ANSWER).
116
+ const sectionIsGroup = Boolean(section.isGroup || section.group?.type);
117
+ partMap.set(key, {
118
+ partId,
119
+ partNo: parsedPartNo,
120
+ questionType: section.questionType || sectionQuestions[0].question_type,
121
+ questions: sectionQuestions,
122
+ name: templatePart.name,
123
+ instructions: section.instructions || section.name || templatePart.instructions,
124
+ audioUrl: templatePart.audioUrl,
125
+ templateOrder,
126
+ category: templatePart.category,
127
+ points: templatePart.points,
128
+ sectionId: section.sectionId,
129
+ isGroup: sectionIsGroup,
130
+ optionMode: section.optionMode,
131
+ optionCount: section.optionCount,
132
+ viewMode: section.viewMode,
133
+ defaultValue: section.defaultValue,
134
+ multipleAnswers: section.multipleAnswers,
135
+ questionConfig: section.questionConfig,
136
+ wordBankConfig: section.wordBankConfig,
137
+ group: section.group,
138
+ startIndex: section.startIndex,
139
+ endIndex: section.endIndex,
140
+ });
141
+ }
142
+ }
143
+ // Sort by templateOrder, then by question_number of first question in group
144
+ return Array.from(partMap.values()).sort((a, b) => {
145
+ const orderA = a.templateOrder !== undefined && !isNaN(a.templateOrder) ? a.templateOrder : Infinity;
146
+ const orderB = b.templateOrder !== undefined && !isNaN(b.templateOrder) ? b.templateOrder : Infinity;
147
+ if (orderA !== orderB)
148
+ return orderA - orderB;
149
+ return (a.questions[0]?.question_number || 0) - (b.questions[0]?.question_number || 0);
150
+ });
151
+ }
152
+ export function transformFillInBlank(questions) {
153
+ const firstQuestion = questions[0];
154
+ const content = firstQuestion?.content;
155
+ const groupPayload = firstQuestion?.questionGroup?.payload;
156
+ const transformed = [];
157
+ /** Take UI shows plain textBefore/textAfter — strip `**` so markers never leak. */
158
+ const stripStemBoldMarkers = (s) => s.replace(/\*\*/g, '');
159
+ for (const q of questions) {
160
+ const qContent = q.content;
161
+ const isExample = q.is_example || false;
162
+ // Support both: content.question (new) and content.blanks[0].text (legacy)
163
+ const text = qContent.question || qContent.blanks?.[0]?.text || '';
164
+ // Check for multiple placeholders: {0}, {1}, etc.
165
+ const placeholderMatches = [...text.matchAll(/\{(\d+)\}/g)];
166
+ if (placeholderMatches.length > 1) {
167
+ // Multi-placeholder: split into separate lines by newline or by placeholder
168
+ const lines = text.split('\n').filter((l) => l.trim());
169
+ for (const line of lines) {
170
+ const lineMatch = line.match(/\{(\d+)\}/);
171
+ if (!lineMatch)
172
+ continue;
173
+ const blankIndex = parseInt(lineMatch[1], 10);
174
+ const parts = line.split(/\{\d+\}/);
175
+ const textBefore = stripStemBoldMarkers(parts[0]?.trim() || '');
176
+ const textAfter = stripStemBoldMarkers(parts[1]?.trim() || '');
177
+ transformed.push({
178
+ id: `${q.id}-blank-${blankIndex}`,
179
+ questionNumber: q.question_number,
180
+ textBefore,
181
+ textAfter,
182
+ imageUrl: qContent.imageUrl,
183
+ hints: qContent.hints,
184
+ isExample,
185
+ correctAnswer: isExample ? q.correct_answer?.answers?.[blankIndex] : undefined,
186
+ });
187
+ }
188
+ }
189
+ else {
190
+ // Single placeholder or no placeholder.
191
+ // Use [\s\S] so stems with newlines still match (`.` does not cross `\n`).
192
+ const match = text.match(/^([\s\S]*?)\{0\}([\s\S]*)$/);
193
+ const textBefore = stripStemBoldMarkers(match ? match[1].trim() : text);
194
+ const textAfter = stripStemBoldMarkers(match ? match[2].trim() : '');
195
+ transformed.push({
196
+ id: q.id,
197
+ questionNumber: q.question_number,
198
+ textBefore,
199
+ textAfter,
200
+ imageUrl: qContent.imageUrl,
201
+ hints: qContent.hints,
202
+ isExample,
203
+ correctAnswer: isExample ? q.correct_answer?.answers?.[0] : undefined,
204
+ });
205
+ }
206
+ }
207
+ return {
208
+ questions: transformed,
209
+ // No hardcoded fallback: the part instruction banner already states the task,
210
+ // so an extra "FILL IN THE BLANK" heading is redundant.
211
+ title: content?.title || groupPayload?.title || '',
212
+ // Shared header image: only meaningful for grouped parts (legacy data had the group
213
+ // payload merged into content). For non-grouped parts each question renders its own
214
+ // content.imageUrl below, so lifting the first one here would duplicate it.
215
+ imageUrl: groupPayload ? content?.imageUrl : undefined,
216
+ audioUrl: content?.audioUrl,
217
+ exampleText: content?.exampleText,
218
+ exampleHighlight: content?.exampleHighlight,
219
+ instruction: content?.instruction || 'LISTEN AND WRITE',
220
+ showHints: content?.showHints,
221
+ groupContent: groupPayload?.content || groupPayload?.groupContent,
222
+ groupTitle: groupPayload?.title,
223
+ groupImageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl,
224
+ };
225
+ }
226
+ export function transformWriteCorrectVerbForm(questions) {
227
+ const firstQuestion = questions[0];
228
+ const content = firstQuestion?.content;
229
+ const transformed = questions.map((q) => {
230
+ const qContent = q.content;
231
+ const sentence = qContent.sentence || '';
232
+ // Parse "John {0} his homework." -> textBefore: "John", textAfter: "his homework."
233
+ // [\s\S] allows instruction text with newlines before the blank.
234
+ const match = sentence.match(/^([\s\S]*?)\{0\}([\s\S]*)$/);
235
+ const textBefore = match ? match[1].trim() : sentence;
236
+ const textAfter = match ? match[2].trim() : '';
237
+ return {
238
+ id: q.id,
239
+ questionNumber: q.question_number,
240
+ textBefore,
241
+ textAfter,
242
+ verbHint: qContent.verbHint, // Pass verbHint per question
243
+ };
244
+ });
245
+ return {
246
+ questions: transformed,
247
+ title: 'WRITE THE CORRECT VERB FORM',
248
+ instruction: content?.instruction ||
249
+ 'Write the correct form of the verb in parentheses.',
250
+ };
251
+ }
252
+ // ============================================
253
+ // Deterministic Shuffle Utility
254
+ // ============================================
255
+ /**
256
+ * Simple hash function to create a numeric seed from a string.
257
+ * Used for deterministic (repeatable) shuffling based on questionId.
258
+ */
259
+ function simpleHash(str) {
260
+ let hash = 0;
261
+ for (let i = 0; i < str.length; i++) {
262
+ const char = str.charCodeAt(i);
263
+ hash = ((hash << 5) - hash) + char;
264
+ hash |= 0; // Convert to 32-bit integer
265
+ }
266
+ return Math.abs(hash);
267
+ }
268
+ /**
269
+ * Fisher-Yates shuffle with a deterministic seed.
270
+ * Same seed always produces the same shuffled order.
271
+ * Uses a Linear Congruential Generator (LCG) for pseudo-random numbers.
272
+ */
273
+ function seededShuffle(array, seed) {
274
+ const shuffled = [...array];
275
+ let currentSeed = simpleHash(seed);
276
+ for (let i = shuffled.length - 1; i > 0; i--) {
277
+ currentSeed = (currentSeed * 1664525 + 1013904223) & 0x7fffffff; // LCG
278
+ const j = currentSeed % (i + 1);
279
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
280
+ }
281
+ return shuffled;
282
+ }
283
+ const OPTION_LABELS = ['A', 'B', 'C', 'D', 'E', 'F'];
284
+ export function transformChooseCorrectAnswer(questions, optionsConfig) {
285
+ const shouldShuffleOptions = optionsConfig?.shuffleOptions ?? true;
286
+ const firstContent = questions[0]?.content;
287
+ const groupPayload = questions[0]?.questionGroup?.payload;
288
+ // Separate example from regular questions
289
+ const exampleQuestion = questions.find((q) => q.is_example);
290
+ const regularQuestions = questions.filter((q) => !q.is_example);
291
+ // Build example object if an example question exists
292
+ let example;
293
+ if (exampleQuestion) {
294
+ const exContent = exampleQuestion.content;
295
+ const exOptions = (exContent.options || []).map((opt, idx) => {
296
+ const isObj = typeof opt === 'object' && opt !== null;
297
+ const optId = isObj && opt.id ? String(opt.id) : `${exampleQuestion.id}-opt-${idx}`;
298
+ const optText = isObj ? (opt.text ?? opt.content ?? '') : String(opt);
299
+ const optImage = isObj ? opt.imageUrl : undefined;
300
+ return {
301
+ id: optId,
302
+ label: OPTION_LABELS[idx] || String(idx + 1),
303
+ text: optText,
304
+ ...(optImage ? { imageUrl: optImage } : {}),
305
+ };
306
+ });
307
+ const exRawAns = exampleQuestion.correct_answer?.answer ?? exampleQuestion.correct_answer?.optionId;
308
+ let exCorrectId = '';
309
+ if (typeof exRawAns === 'string' && exRawAns) {
310
+ const match = exOptions.find((o) => o.id === exRawAns);
311
+ exCorrectId = match ? match.id : exRawAns;
312
+ }
313
+ else if (typeof exRawAns === 'number' && exRawAns >= 0 && exRawAns < exOptions.length) {
314
+ exCorrectId = exOptions[exRawAns]?.id || '';
315
+ }
316
+ example = {
317
+ questionNumber: exampleQuestion.question_number,
318
+ questionText: exContent.question || '',
319
+ options: exOptions,
320
+ correctOptionId: exCorrectId || exOptions[0]?.id || '',
321
+ optionType: (exContent.optionType || 'text'),
322
+ imageUrl: exContent.imageUrl,
323
+ audioUrl: exContent.audioUrl,
324
+ };
325
+ }
326
+ const transformed = regularQuestions.map((q) => {
327
+ const content = q.content;
328
+ const optionType = content.optionType || 'text';
329
+ const options = (content.options || []).map((opt, idx) => {
330
+ const isObj = typeof opt === 'object' && opt !== null;
331
+ const optId = isObj && opt.id ? String(opt.id) : `${q.id}-opt-${idx}`;
332
+ const optText = isObj ? (opt.text ?? opt.content ?? '') : String(opt);
333
+ const optImage = isObj ? opt.imageUrl : undefined;
334
+ return {
335
+ id: optId,
336
+ label: OPTION_LABELS[idx] || String(idx + 1),
337
+ text: optText,
338
+ ...(optImage ? { imageUrl: optImage } : {}),
339
+ };
340
+ });
341
+ const rawAns = q.correct_answer?.answer ?? q.correct_answer?.optionId ?? q.correct_answer?.optionIds;
342
+ const findOptionId = (val) => {
343
+ if (val === undefined || val === null)
344
+ return undefined;
345
+ const strVal = String(val);
346
+ const matchedById = options.find((o) => o.id === strVal);
347
+ if (matchedById)
348
+ return matchedById.id;
349
+ // Handle legacy synthetic option IDs like "questionId-opt-0"
350
+ const optMatch = strVal.match(/-opt-(\d+)$/);
351
+ if (optMatch) {
352
+ const optIdx = parseInt(optMatch[1], 10);
353
+ if (optIdx >= 0 && optIdx < options.length) {
354
+ return options[optIdx]?.id;
355
+ }
356
+ }
357
+ if (typeof val === 'number' && val >= 0 && val < options.length) {
358
+ return options[val]?.id;
359
+ }
360
+ const num = parseInt(strVal, 10);
361
+ if (!isNaN(num) && num >= 0 && num < options.length && !strVal.startsWith('opt-')) {
362
+ return options[num]?.id;
363
+ }
364
+ return strVal;
365
+ };
366
+ let correctOptionIds = [];
367
+ if (Array.isArray(rawAns)) {
368
+ correctOptionIds = rawAns.map(findOptionId).filter((id) => Boolean(id));
369
+ }
370
+ else {
371
+ const singleId = findOptionId(rawAns);
372
+ if (singleId)
373
+ correctOptionIds = [singleId];
374
+ }
375
+ // Shuffle options deterministically using questionId as seed when enabled.
376
+ // Since answers use optionId (not index), shuffling display order is safe.
377
+ const finalOptions = shouldShuffleOptions
378
+ ? seededShuffle(options, q.id).map((opt, idx) => ({
379
+ ...opt,
380
+ label: OPTION_LABELS[idx] || String(idx + 1),
381
+ }))
382
+ : options;
383
+ return {
384
+ id: q.id,
385
+ questionNumber: q.question_number,
386
+ questionText: content.question || '',
387
+ options: finalOptions,
388
+ correctOptionId: correctOptionIds[0],
389
+ correctOptionIds,
390
+ imageUrl: content.imageUrl, // Per-question image
391
+ audioUrl: content.audioUrl, // Per-question audio (questionConfig includes "audio")
392
+ optionType: optionType,
393
+ multipleAnswers: Boolean(content.multipleAnswers || Array.isArray(rawAns)),
394
+ explanation: q.explanation,
395
+ };
396
+ });
397
+ // Each question can carry its own audio (questionConfig "audio"). Only fall back to the
398
+ // first question's audio as a part-level player when nothing has per-question audio —
399
+ // otherwise question 1's clip would be lifted to the top and the rest would be lost.
400
+ const hasPerQuestionAudio = transformed.some((q) => !!q.audioUrl?.trim()) || !!example?.audioUrl?.trim();
401
+ return {
402
+ questions: transformed,
403
+ example,
404
+ imageUrl: firstContent?.imageUrl,
405
+ groupImageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl,
406
+ groupTitle: groupPayload?.title,
407
+ groupContent: groupPayload?.content,
408
+ articles: groupPayload?.articles,
409
+ audioUrl: hasPerQuestionAudio ? undefined : firstContent?.audioUrl,
410
+ instruction: firstContent?.instruction || 'CHOOSE THE CORRECT ANSWER',
411
+ };
412
+ }
413
+ export function transformChooseTheCorrectAnswerGroup(questions) {
414
+ const parent = questions[0];
415
+ const content = parent?.content;
416
+ const items = Array.isArray(content?.items) ? content.items : [];
417
+ const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
418
+ ? parent.correct_answer.answer
419
+ : [];
420
+ const exampleItem = items.find((item) => item.isExample);
421
+ const regularItems = items.filter((item) => !item.isExample);
422
+ const mapOptions = (options, prefix) => (options || []).map((opt, idx) => {
423
+ const isObj = typeof opt === 'object' && opt !== null;
424
+ return {
425
+ id: isObj && opt.id ? String(opt.id) : `${prefix}-opt-${idx + 1}`,
426
+ label: OPTION_LABELS[idx] || String(idx + 1),
427
+ text: isObj ? String(opt.text ?? '') : String(opt),
428
+ ...(isObj && opt.imageUrl ? { imageUrl: opt.imageUrl } : {}),
429
+ };
430
+ });
431
+ let example;
432
+ if (exampleItem) {
433
+ const exampleIndex = items.indexOf(exampleItem);
434
+ const exOptions = mapOptions(exampleItem.content?.options, `${parent.id}-ex`);
435
+ const exCorrect = exampleItem.correctAnswer?.answer || parentCorrect[exampleIndex] || exOptions[0]?.id || '';
436
+ example = {
437
+ questionNumber: exampleItem.questionNumber || exampleIndex + 1,
438
+ questionText: exampleItem.content?.question || '',
439
+ options: exOptions,
440
+ correctOptionId: exCorrect,
441
+ optionType: (exampleItem.content?.optionType || 'text'),
442
+ };
443
+ }
444
+ const transformed = regularItems.map((item) => {
445
+ const itemIndex = items.indexOf(item);
446
+ const options = mapOptions(item.content?.options, `${parent.id}-item-${itemIndex}`);
447
+ const correctId = item.correctAnswer?.answer || parentCorrect[itemIndex] || '';
448
+ return {
449
+ id: `${parent.id}-item-${itemIndex}`,
450
+ questionNumber: item.questionNumber || itemIndex + 1,
451
+ questionText: item.content?.question || '',
452
+ options,
453
+ correctOptionId: correctId,
454
+ correctOptionIds: correctId ? [correctId] : [],
455
+ optionType: (item.content?.optionType || 'text'),
456
+ };
457
+ });
458
+ return {
459
+ questions: transformed,
460
+ example,
461
+ instruction: content?.meta?.instruction || 'Choose the correct answer.',
462
+ };
463
+ }
464
+ export function transformChooseCorrectAdjective(questions) {
465
+ const transformed = questions.map((q) => {
466
+ const content = q.content;
467
+ const sentence = content.sentence || '';
468
+ // Parse sentence with {0} placeholder ([\s\S] crosses newlines)
469
+ const match = sentence.match(/^([\s\S]*?)\{0\}([\s\S]*)$/);
470
+ const sentenceBefore = match ? match[1] : sentence;
471
+ const sentenceAfter = match ? match[2] : '';
472
+ return {
473
+ id: q.id,
474
+ questionNumber: q.question_number,
475
+ sentenceBefore,
476
+ sentenceAfter,
477
+ options: (content.options || []).map((opt, idx) => ({
478
+ id: `${q.id}-opt-${idx}`,
479
+ label: OPTION_LABELS[idx] || String(idx + 1),
480
+ text: opt,
481
+ })),
482
+ };
483
+ });
484
+ return {
485
+ questions: transformed,
486
+ instruction: 'CHOOSE THE CORRECT ADJECTIVE',
487
+ };
488
+ }
489
+ export function transformWriteShortLetter(question) {
490
+ const content = question.content;
491
+ // New structure: use instructions.mainInstruction as prompt, informations as requirements
492
+ if (content.instructions?.mainInstruction || content.informations?.length) {
493
+ return {
494
+ prompt: content.instructions?.mainInstruction || '',
495
+ requirements: content.informations,
496
+ minWords: content.minWords,
497
+ maxWords: content.maxWords,
498
+ guideImages: content.guideImages,
499
+ };
500
+ }
501
+ // Legacy structure: build prompt from contexts array
502
+ const prompt = content.contexts
503
+ ?.filter((c) => c.type === 'text')
504
+ .map((c) => c.text)
505
+ .join('\n') || '';
506
+ return {
507
+ prompt,
508
+ minWords: content.minWords,
509
+ maxWords: content.maxWords,
510
+ guideImages: content.guideImages,
511
+ };
512
+ }
513
+ export function transformColorObjects(questions) {
514
+ const firstQuestion = questions[0];
515
+ const content = firstQuestion?.content;
516
+ // Map colorNodes to hintItems format
517
+ const hintItems = content?.colorNodes?.map((node) => ({
518
+ id: node.id,
519
+ color: node.color,
520
+ label: node.label,
521
+ })) ||
522
+ content?.hintItems ||
523
+ [];
524
+ // Map regionNodes to regions format
525
+ const regions = content?.regionNodes?.map((node) => ({
526
+ id: node.id,
527
+ x: node.x,
528
+ y: node.y,
529
+ width: node.width,
530
+ height: node.height,
531
+ correctHintId: node.linkedColorNodeId,
532
+ })) ||
533
+ content?.regions ||
534
+ [];
535
+ // Get raw instructions array for numbered list display
536
+ const instructionsArray = content?.instructions;
537
+ // Get instruction text (joined or single)
538
+ const instruction = (instructionsArray && instructionsArray.length > 0
539
+ ? instructionsArray.join(' ')
540
+ : content?.instruction) || 'READ AND COLOR THE OBJECTS';
541
+ return {
542
+ instruction,
543
+ instructions: instructionsArray,
544
+ imageUrl: content?.backgroundImageUrl || content?.imageUrl || '',
545
+ audioUrl: content?.audioUrl,
546
+ hintItems,
547
+ regions,
548
+ };
549
+ }
550
+ export function transformPictureChoose(questions) {
551
+ const firstContent = questions[0]?.content;
552
+ const subQuestions = questions.map((q) => {
553
+ const content = q.content;
554
+ return {
555
+ id: q.id,
556
+ questionNumber: q.question_number,
557
+ imageUrl: content.imageUrl || '',
558
+ questionText: content.questionText,
559
+ options: (content.options || []).map((opt, idx) => ({
560
+ id: opt.id || `${q.id}-opt-${idx}`,
561
+ label: OPTION_LABELS[idx] || String(idx + 1),
562
+ text: typeof opt === 'string' ? opt : opt.text,
563
+ })),
564
+ };
565
+ });
566
+ return {
567
+ subQuestions,
568
+ instruction: firstContent?.instruction || 'LOOK AT THE PICTURE AND CHOOSE THE CORRECT ANSWER',
569
+ };
570
+ }
571
+ export function transformPictureFillBlankChoose(questions) {
572
+ const firstContent = questions[0]?.content;
573
+ const subQuestions = questions.map((q) => {
574
+ const content = q.content;
575
+ return {
576
+ id: q.id,
577
+ questionNumber: q.question_number,
578
+ imageUrl: content.imageUrl || '',
579
+ question: content.question || '',
580
+ options: (content.options || []).map((opt, idx) => ({
581
+ id: opt.id || `${q.id}-opt-${idx}`,
582
+ label: OPTION_LABELS[idx] || String(idx + 1),
583
+ text: typeof opt === 'string' ? opt : opt.text,
584
+ })),
585
+ };
586
+ });
587
+ return {
588
+ subQuestions,
589
+ instruction: firstContent?.instruction ||
590
+ 'LOOK AT THE PICTURE, FILL IN THE BLANK AND CHOOSE THE ANSWER',
591
+ };
592
+ }
593
+ /**
594
+ * Detect if data uses new format (separate questions with questionGroup)
595
+ * New format: content has 'question' field (not 'questions' array)
596
+ * AND questionGroup.payload has passage
597
+ */
598
+ function isNewReadingPassageFormat(questions) {
599
+ const first = questions[0];
600
+ if (!first)
601
+ return false;
602
+ const content = first.content;
603
+ return (content?.question !== undefined &&
604
+ !content?.questions &&
605
+ first.questionGroup?.payload?.passage !== undefined);
606
+ }
607
+ export function transformReadingPassage(questions) {
608
+ if (isNewReadingPassageFormat(questions)) {
609
+ // NEW FORMAT: Multiple questions with shared questionGroup.payload
610
+ const firstQuestion = questions[0];
611
+ const groupPayload = firstQuestion.questionGroup?.payload || {};
612
+ const transformedQuestions = questions.map((q) => {
613
+ const content = q.content;
614
+ return {
615
+ id: q.id,
616
+ questionNumber: q.question_number,
617
+ questionText: content?.question || '',
618
+ options: (content?.options || []).map((opt, idx) => ({
619
+ id: `${q.id}-opt-${idx}`,
620
+ label: OPTION_LABELS[idx] || String(idx + 1),
621
+ text: typeof opt === 'string' ? opt : String(opt),
622
+ })),
623
+ };
624
+ });
625
+ return {
626
+ passage: groupPayload.passage || '',
627
+ passageTitle: groupPayload.passageTitle,
628
+ passageImageUrl: groupPayload.passageImageUrl || groupPayload.mainImageUrl,
629
+ instruction: groupPayload.instructions || 'READ THE PASSAGE AND ANSWER QUESTIONS',
630
+ questions: transformedQuestions,
631
+ };
632
+ }
633
+ // OLD FORMAT: Single question with embedded sub-questions in content.questions[]
634
+ const firstQuestion = questions[0];
635
+ const content = firstQuestion?.content;
636
+ const subQuestions = content?.questions?.map((sq, idx) => ({
637
+ id: sq.id || `${firstQuestion.id}-sq-${idx}`,
638
+ questionNumber: idx + 1,
639
+ questionText: sq.question,
640
+ options: (sq.options || []).map((opt, optIdx) => ({
641
+ id: `${sq.id || firstQuestion.id}-opt-${optIdx}`,
642
+ label: OPTION_LABELS[optIdx] || String(optIdx + 1),
643
+ text: opt,
644
+ })),
645
+ })) || [];
646
+ return {
647
+ passage: content?.passage || '',
648
+ passageTitle: content?.passageTitle,
649
+ passageImageUrl: content?.passageImageUrl || content?.mainImageUrl,
650
+ instruction: content?.instructions?.mainInstruction || 'READ THE PASSAGE AND ANSWER QUESTIONS',
651
+ questions: subQuestions,
652
+ };
653
+ }
654
+ export function transformLabelPicture(questions) {
655
+ const firstQuestion = questions[0];
656
+ const content = firstQuestion?.content;
657
+ return {
658
+ imageUrl: content?.imageUrl || '',
659
+ wordBank: content?.wordBank || [],
660
+ labels: content?.labels?.map((l) => ({
661
+ id: l.id,
662
+ number: l.number,
663
+ isPreFilled: l.isPreFilled || false,
664
+ correctAnswer: l.correctAnswer, // From questionContent for preFilled labels
665
+ })) || [],
666
+ instruction: content?.instruction || 'LABEL THE PICTURE',
667
+ };
668
+ }
669
+ /**
670
+ * Detect if content uses new format (single word with {n} placeholders)
671
+ */
672
+ function isNewFillWordHintFormat(content) {
673
+ return content && typeof content.word === 'string' && !content.subQuestions;
674
+ }
675
+ /**
676
+ * Parse word string with numbered placeholders like "c{0}t, {1}lephan"
677
+ * Returns array of parsed blanks with their segments
678
+ */
679
+ function parseNumberedPlaceholders(word) {
680
+ const results = [];
681
+ // Split by comma to handle multiple words like "c{0}t, {1}lephan"
682
+ const wordSegments = word.split(',').map((s) => s.trim());
683
+ for (const segment of wordSegments) {
684
+ // Find all placeholders in this segment
685
+ let match;
686
+ const segmentRegex = /\{(\d+)\}/g;
687
+ while ((match = segmentRegex.exec(segment)) !== null) {
688
+ const placeholderIndex = parseInt(match[1], 10);
689
+ // Replace {n} with _ for display
690
+ const displayWord = segment.replace(match[0], '_');
691
+ results.push({
692
+ index: placeholderIndex,
693
+ displayWord,
694
+ });
695
+ }
696
+ }
697
+ // Sort by index to ensure correct order
698
+ results.sort((a, b) => a.index - b.index);
699
+ return results;
700
+ }
701
+ export function transformFillWordHint(questions) {
702
+ const firstQuestion = questions[0];
703
+ const content = firstQuestion?.content;
704
+ // MULTI-QUESTION FORMAT: Multiple separate questions with same group_id
705
+ // Each has own id, imageUrl, and single {0} placeholder
706
+ if (questions.length > 1 && questions.every((q) => isNewFillWordHintFormat(q.content))) {
707
+ const hintLetters = firstQuestion.questionGroup?.payload?.hintLetters || [];
708
+ const subQuestions = questions.map((q) => {
709
+ const qContent = q.content;
710
+ // Replace {0} with underscore for display
711
+ const displayWord = qContent.word.replace(/\{\d+\}/, '_');
712
+ return {
713
+ id: q.id, // Use actual question ID (not synthetic)
714
+ questionNumber: q.question_number,
715
+ imageUrl: qContent.imageUrl || '',
716
+ word: displayWord,
717
+ // No blankIndex → triggers OLD FORMAT rendering (grid with individual images)
718
+ };
719
+ });
720
+ return {
721
+ subQuestions,
722
+ hintLetters,
723
+ instruction: content?.instruction ||
724
+ 'LOOK AT THE PICTURE AND FILL IN THE MISSING LETTER',
725
+ };
726
+ }
727
+ // Detect format and transform accordingly
728
+ if (isNewFillWordHintFormat(content)) {
729
+ // NEW FORMAT: Single word with {0}, {1} placeholders
730
+ const newContent = content;
731
+ // Get hintLetters from questionGroup.payload (new location)
732
+ const hintLetters = firstQuestion.questionGroup?.payload?.hintLetters || [];
733
+ // Parse the word string to extract placeholders
734
+ const placeholders = parseNumberedPlaceholders(newContent.word);
735
+ // Create sub-questions for each placeholder
736
+ const subQuestions = placeholders.map((p) => ({
737
+ id: `${firstQuestion.id}-blank-${p.index}`,
738
+ questionNumber: firstQuestion.question_number,
739
+ imageUrl: newContent.imageUrl || '',
740
+ word: p.displayWord,
741
+ blankIndex: p.index,
742
+ }));
743
+ return {
744
+ subQuestions,
745
+ hintLetters,
746
+ instruction: newContent.instruction ||
747
+ 'LOOK AT THE PICTURE AND FILL IN THE MISSING LETTER',
748
+ };
749
+ }
750
+ // OLD FORMAT: subQuestions array with individual images
751
+ const oldContent = content;
752
+ const subQuestions = oldContent?.subQuestions?.map((sq, idx) => ({
753
+ id: sq.id,
754
+ questionNumber: firstQuestion.question_number + idx,
755
+ imageUrl: sq.imageUrl || '',
756
+ word: sq.word || '',
757
+ })) || [];
758
+ // Try hintLetters from content first, then from questionGroup.payload
759
+ const hintLetters = oldContent?.hintLetters ||
760
+ firstQuestion.questionGroup?.payload?.hintLetters ||
761
+ [];
762
+ return {
763
+ subQuestions,
764
+ hintLetters,
765
+ instruction: oldContent?.instruction ||
766
+ 'LOOK AT THE PICTURE AND FILL IN THE MISSING LETTER',
767
+ };
768
+ }
769
+ export function transformGridFill(questions) {
770
+ const firstQuestion = questions[0];
771
+ const content = firstQuestion?.content;
772
+ return {
773
+ grid: content?.grid || [],
774
+ rows: content?.rows || 0,
775
+ columns: content?.columns || 0,
776
+ instruction: content?.instruction || 'FILL IN THE MISSING WORDS',
777
+ disabledCells: content?.disabledCells || [],
778
+ };
779
+ }
780
+ export function transformAnswerTheQuestion(questions) {
781
+ const firstQuestion = questions[0];
782
+ const groupPayload = firstQuestion?.questionGroup?.payload;
783
+ const firstContent = firstQuestion?.content;
784
+ const transformed = questions.map((q) => {
785
+ const content = q.content;
786
+ const isExample = q.is_example || content.isExample || false;
787
+ return {
788
+ id: q.id,
789
+ questionNumber: q.question_number,
790
+ question: (content.question || '').replace(/\{\d+\}/g, '______'),
791
+ imageUrl: content.imageUrl,
792
+ prefixText: content.prefixText,
793
+ suffixText: content.suffixText,
794
+ suggestedAnswers: content.suggestedAnswers,
795
+ isExample,
796
+ exampleAnswer: isExample ? q.correct_answer?.answer : undefined,
797
+ };
798
+ });
799
+ return {
800
+ questions: transformed,
801
+ instruction: 'ANSWER THE QUESTION',
802
+ imageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl || firstContent?.imageUrl,
803
+ title: firstContent?.title,
804
+ groupContent: groupPayload?.groupContent || firstContent?.groupContent,
805
+ };
806
+ }
807
+ export function transformTrueFalse(questions) {
808
+ const groupPayload = questions[0]?.questionGroup?.payload;
809
+ const transformed = questions.map((q) => {
810
+ const content = q.content;
811
+ const isExample = q.is_example || content.isExample || false;
812
+ const exampleCorrectAnswer = isExample && typeof q.correct_answer?.answer === 'boolean'
813
+ ? q.correct_answer.answer
814
+ : undefined;
815
+ return {
816
+ id: q.id,
817
+ questionNumber: q.question_number,
818
+ question: content.question || '',
819
+ imageUrl: content.imageUrl,
820
+ isExample,
821
+ exampleCorrectAnswer,
822
+ };
823
+ });
824
+ return {
825
+ questions: transformed,
826
+ instruction: 'READ AND CHOOSE TRUE OR FALSE',
827
+ imageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl,
828
+ groupContent: groupPayload?.content || groupPayload?.groupContent,
829
+ };
830
+ }
831
+ // ============================================
832
+ // TRUE_FALSE_GROUP Transformer
833
+ // ============================================
834
+ export function transformTrueFalseGroup(questions) {
835
+ const parent = questions[0];
836
+ const content = parent?.content;
837
+ const items = Array.isArray(content?.items) ? content.items : [];
838
+ const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
839
+ ? parent.correct_answer.answer
840
+ : [];
841
+ const transformed = items.map((item, index) => {
842
+ const isExample = item.isExample === true;
843
+ const exampleCorrectAnswer = isExample && typeof (item.correctAnswer?.answer ?? parentCorrect[index]) === 'boolean'
844
+ ? (item.correctAnswer?.answer ?? parentCorrect[index])
845
+ : undefined;
846
+ return {
847
+ id: `${parent.id}-item-${index}`,
848
+ questionNumber: item.questionNumber || index + 1,
849
+ question: item.content?.question || '',
850
+ isExample,
851
+ exampleCorrectAnswer,
852
+ };
853
+ });
854
+ return {
855
+ questions: transformed,
856
+ instruction: content?.meta?.instruction || 'READ AND CHOOSE TRUE OR FALSE',
857
+ };
858
+ }
859
+ export function transformWordOrdering(questions) {
860
+ const transformed = questions.map((q) => {
861
+ const content = q.content;
862
+ const isExample = q.is_example || content.isExample || false;
863
+ const exampleAnswer = Array.isArray(q.correct_answer?.answer)
864
+ ? q.correct_answer.answer
865
+ : Array.isArray(content.answer)
866
+ ? content.answer
867
+ : undefined;
868
+ return {
869
+ id: q.id,
870
+ questionNumber: q.question_number,
871
+ words: content.words || [],
872
+ imageUrl: content.imageUrl,
873
+ isExample,
874
+ exampleAnswer: isExample ? exampleAnswer : undefined,
875
+ };
876
+ });
877
+ return {
878
+ questions: transformed,
879
+ instruction: 'ARRANGE THE LETTERS TO MAKE A WORD',
880
+ };
881
+ }
882
+ export function transformWordFillParagraph(questions) {
883
+ const firstQuestion = questions[0];
884
+ const groupPayload = firstQuestion?.questionGroup?.payload;
885
+ const content = firstQuestion?.content;
886
+ // Try to find title in content of any question or fallback to group payload
887
+ const title = content?.title || groupPayload?.title || questions.find(q => q.content?.title)?.content?.title;
888
+ // Normalize wordBank: handle both string[] (legacy DB format) and WordBankItem[] formats
889
+ const rawWordBank = content?.wordBank || [];
890
+ const wordBank = rawWordBank.map((item) => typeof item === 'string' ? { word: item } : item);
891
+ const storedViewMode = content?.viewMode;
892
+ const hasOptions = content?.wordBank?.[0]?.options !== undefined;
893
+ let viewMode;
894
+ if (storedViewMode === 'EMAIL') {
895
+ viewMode = 'EMAIL';
896
+ }
897
+ else if (!storedViewMode && (content?.emailFrom || content?.emailTo)) {
898
+ // Defensive fallback: if viewMode is null/undefined but email fields are present, treat as EMAIL
899
+ viewMode = 'EMAIL';
900
+ }
901
+ else if (storedViewMode === 'DEFAULT') {
902
+ viewMode = 'DEFAULT';
903
+ }
904
+ else if (storedViewMode === 'WITH_OPTIONS' || hasOptions) {
905
+ viewMode = 'WITH_OPTIONS';
906
+ }
907
+ else if (!storedViewMode && rawWordBank.length === 0) {
908
+ viewMode = 'DEFAULT';
909
+ }
910
+ else {
911
+ viewMode = 'WITH_WORD_BANK';
912
+ }
913
+ const emailFrom = content?.emailFrom || content?.metadata?.emailFrom;
914
+ const emailTo = content?.emailTo || content?.metadata?.emailTo;
915
+ return {
916
+ questionId: firstQuestion?.id || '',
917
+ paragraph: content?.paragraph || '',
918
+ title,
919
+ imageUrl: content?.imageUrl,
920
+ wordBank,
921
+ viewMode,
922
+ instruction: 'READ AND FILL IN THE BLANKS',
923
+ exampleAnswer: content?.exampleAnswer,
924
+ emailFrom,
925
+ emailTo,
926
+ };
927
+ }
928
+ export function transformMatchByWritingAnswer(questions) {
929
+ const groupPayload = questions[0]?.questionGroup?.payload;
930
+ const isConversation = questions.some((q) => !!q.content?.askerName);
931
+ const transformed = questions.map((q) => {
932
+ const content = q.content;
933
+ const isExample = q.is_example || content.isExample || false;
934
+ return {
935
+ id: q.id,
936
+ questionNumber: q.question_number,
937
+ question: content.question || '',
938
+ imageUrl: content.imageUrl,
939
+ isExample,
940
+ exampleAnswer: isExample ? q.correct_answer?.answer : undefined,
941
+ askerName: content.askerName,
942
+ answererName: content.answererName,
943
+ };
944
+ });
945
+ const options = groupPayload?.options?.map((opt) => ({
946
+ answer: opt.answer,
947
+ imageUrl: opt.imageUrl || '',
948
+ content: opt.content || '',
949
+ })) || [];
950
+ const firstConversationQ = isConversation
951
+ ? questions[0]?.content
952
+ : undefined;
953
+ return {
954
+ questions: transformed,
955
+ options,
956
+ instruction: 'LOOK AND WRITE THE ANSWER',
957
+ viewMode: isConversation ? 'CONVERSATION' : 'DEFAULT',
958
+ groupImageUrl: groupPayload?.groupImageUrl || '',
959
+ askerName: firstConversationQ?.askerName,
960
+ answererName: firstConversationQ?.answererName,
961
+ };
962
+ }
963
+ export function transformWriteSentences(questions) {
964
+ const firstQuestion = questions[0];
965
+ const content = firstQuestion?.content;
966
+ const groupPayload = firstQuestion?.questionGroup?.payload;
967
+ return {
968
+ questions: questions.map((q) => ({
969
+ id: q.id,
970
+ questionNumber: q.question_number,
971
+ })),
972
+ instruction: content?.instruction || 'WRITE SENTENCES ABOUT THE PICTURE',
973
+ imageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl || content?.imageUrl,
974
+ title: content?.title,
975
+ groupContent: groupPayload?.groupContent || content?.groupContent,
976
+ };
977
+ }
978
+ function normalizeWfsfBlankStyle(raw) {
979
+ const v = String(raw || '').toLowerCase();
980
+ if (v === 'letter' || v === 'box')
981
+ return 'letter';
982
+ if (v === 'phrase' || v === 'sentence')
983
+ return 'phrase';
984
+ return 'word';
985
+ }
986
+ function normalizeWfsfWordBank(raw) {
987
+ if (!Array.isArray(raw) || raw.length === 0)
988
+ return undefined;
989
+ const bank = raw.map((entry) => {
990
+ if (entry == null)
991
+ return null;
992
+ if (typeof entry !== 'object' || Array.isArray(entry))
993
+ return null;
994
+ const rec = entry;
995
+ if (Array.isArray(rec.options)) {
996
+ const options = rec.options
997
+ .map((o) => String(o ?? '').trim())
998
+ .filter((o) => o.length > 0);
999
+ return options.length >= 2 ? { options } : null;
1000
+ }
1001
+ const word = String(rec.word ?? '').trim();
1002
+ if (word) {
1003
+ const w = rec.isExample
1004
+ ? { word, isExample: true }
1005
+ : { word };
1006
+ return w;
1007
+ }
1008
+ return null;
1009
+ });
1010
+ return bank.some((e) => e != null) ? bank : undefined;
1011
+ }
1012
+ export function transformWordFillStructuredForm(questions) {
1013
+ const firstQuestion = questions[0];
1014
+ const groupPayload = firstQuestion?.questionGroup?.payload;
1015
+ const transformed = questions.map((q) => {
1016
+ const content = q.content;
1017
+ const correctAnswer = q.correct_answer || {};
1018
+ // Resolve content: prefer single `content` string, fallback to merging old `blocks[]`
1019
+ let resolvedContent = content?.content || '';
1020
+ const blocks = content?.blocks;
1021
+ if (!resolvedContent && blocks?.length) {
1022
+ resolvedContent = blocks
1023
+ .map((b) => {
1024
+ const heading = b.heading ? `<h3>${b.heading}</h3>` : '';
1025
+ return heading + (b.content || '');
1026
+ })
1027
+ .join('');
1028
+ }
1029
+ const wordBank = normalizeWfsfWordBank(content?.wordBank);
1030
+ const hasOptions = Boolean(wordBank?.some((e) => e != null && 'options' in e && Array.isArray(e.options)));
1031
+ const hasWords = Boolean(wordBank?.some((e) => e != null && 'word' in e && e.word));
1032
+ const viewMode = content?.viewMode ||
1033
+ (hasOptions ? 'WITH_OPTIONS' : hasWords ? 'WITH_WORD_BANK' : undefined);
1034
+ return {
1035
+ id: q.id,
1036
+ questionNumber: q.question_number,
1037
+ title: content?.title || '',
1038
+ imageUrl: content?.imageUrl,
1039
+ content: resolvedContent,
1040
+ answers: correctAnswer?.answers || {},
1041
+ caseSensitive: correctAnswer?.caseSensitive ?? content?.caseSensitive ?? false,
1042
+ blankStyle: normalizeWfsfBlankStyle(content?.blankStyle),
1043
+ ...(viewMode ? { viewMode } : {}),
1044
+ ...(wordBank ? { wordBank } : {}),
1045
+ };
1046
+ });
1047
+ return {
1048
+ questions: transformed,
1049
+ instruction: groupPayload?.instructions || 'COMPLETE THE FORM BELOW',
1050
+ groupTitle: groupPayload?.title,
1051
+ groupContent: groupPayload?.content,
1052
+ groupImageUrl: groupPayload?.imageUrl,
1053
+ audioUrl: firstQuestion?.audio_url || groupPayload?.audioUrl,
1054
+ };
1055
+ }
1056
+ export function transformListenChooseObjects(questions) {
1057
+ const firstQuestion = questions[0];
1058
+ const content = firstQuestion?.content;
1059
+ const groupPayload = firstQuestion?.questionGroup?.payload || {};
1060
+ return {
1061
+ audioUrl: content?.audioUrl || '',
1062
+ backgroundImageUrl: groupPayload.backgroundImageUrl || '',
1063
+ objectNodes: (groupPayload.objectNodes || []),
1064
+ // Backend returns correctAnswer in review mode (snake_case or camelCase)
1065
+ correctObjectId: firstQuestion?.correctAnswer?.objectId ||
1066
+ firstQuestion?.correct_answer?.objectId ||
1067
+ '',
1068
+ isBackup: content?.isBackup || false,
1069
+ instruction: 'LISTEN AND CHOOSE THE CORRECT OBJECT',
1070
+ };
1071
+ }
1072
+ export function transformListenSpeakAnswer(questions) {
1073
+ const transformed = questions.map((q) => {
1074
+ const content = q.content;
1075
+ const groupPayload = q.questionGroup?.payload || {};
1076
+ return {
1077
+ id: q.id,
1078
+ questionNumber: q.question_number,
1079
+ audioUrl: content?.audioUrl || '',
1080
+ arrowNode: content?.arrowNode,
1081
+ backgroundImageUrl: groupPayload.backgroundImageUrl || groupPayload.imageUrl || '',
1082
+ isBackup: content?.isBackup || false,
1083
+ };
1084
+ });
1085
+ return {
1086
+ questions: transformed,
1087
+ instruction: 'LISTEN AND SPEAK YOUR ANSWER',
1088
+ };
1089
+ }
1090
+ export function transformWordOrderAndMatchGroup(questions) {
1091
+ const parent = questions[0];
1092
+ const rawContent = (parent?.content || {});
1093
+ const rawList = parent?.wordOrderMatchList || rawContent.wordOrderMatchList || rawContent;
1094
+ const meta = rawList.meta || rawContent.meta || {};
1095
+ const items = Array.isArray(rawList.items)
1096
+ ? rawList.items
1097
+ : Array.isArray(rawContent.items)
1098
+ ? rawContent.items
1099
+ : [];
1100
+ const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
1101
+ ? parent.correct_answer.answer
1102
+ : [];
1103
+ const matchOptions = Array.isArray(meta.matchOptions)
1104
+ ? meta.matchOptions.map((opt) => String(opt || '').trim().toUpperCase()).filter(Boolean)
1105
+ : ['A', 'B', 'C', 'D', 'E', 'F'];
1106
+ const transformed = items.map((item, index) => {
1107
+ const rawItemContent = item?.content || {};
1108
+ const scrambledWords = Array.isArray(rawItemContent.scrambledWords)
1109
+ ? rawItemContent.scrambledWords.map((w) => String(w || '').trim()).filter(Boolean)
1110
+ : [];
1111
+ const correctAnswerRaw = item?.correctAnswer || item?.correct_answer || {};
1112
+ const answerObj = correctAnswerRaw.answer && typeof correctAnswerRaw.answer === 'object'
1113
+ ? correctAnswerRaw.answer
1114
+ : correctAnswerRaw && typeof correctAnswerRaw === 'object' && (correctAnswerRaw.orderedPhrase || correctAnswerRaw.matchLabel)
1115
+ ? correctAnswerRaw
1116
+ : parentCorrect[index] || {};
1117
+ const orderedPhrase = typeof answerObj.orderedPhrase === 'string' ? answerObj.orderedPhrase : '';
1118
+ const matchLabel = typeof answerObj.matchLabel === 'string' ? answerObj.matchLabel.toUpperCase() : 'A';
1119
+ const isExample = item?.isExample === true;
1120
+ return {
1121
+ id: `${parent?.id || 'q'}-item-${index}`,
1122
+ questionNumber: item.questionNumber || index + 1,
1123
+ scrambledWords,
1124
+ orderedPhrase,
1125
+ matchLabel,
1126
+ isExample,
1127
+ points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
1128
+ };
1129
+ });
1130
+ return {
1131
+ questions: transformed,
1132
+ instruction: meta.instruction ||
1133
+ 'Put the useful phrases in order, then match them to parts A–F in Exercise 4.',
1134
+ matchOptions,
1135
+ explanation: parent?.explanation,
1136
+ };
1137
+ }