@tinyweb_dev/oe-exam-sdk 0.1.28 → 0.1.30

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 (796) hide show
  1. package/README.md +4 -4
  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.js +3 -1
  310. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +94 -1
  311. package/dist/components/questions/types/choose-the-correct-answer-group/register.js +36 -1
  312. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +55 -1
  313. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +27 -1
  314. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +231 -1
  315. package/dist/components/questions/types/cross-out-word-group/index.js +5 -1
  316. package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.d.ts +1 -1
  317. package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +110 -1
  318. package/dist/components/questions/types/cross-out-word-group/register.js +36 -1
  319. package/dist/components/questions/types/cross-out-word-group/transform.js +67 -1
  320. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +275 -1
  321. package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +464 -1
  322. package/dist/components/questions/types/fill-in-blank/index.js +2 -1
  323. package/dist/components/questions/types/fill-in-blank/register.js +148 -1
  324. package/dist/components/questions/types/fill-in-blank/transform.js +88 -1
  325. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +117 -1
  326. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +671 -1
  327. package/dist/components/questions/types/fill-missing-words-in-grid/index.js +2 -1
  328. package/dist/components/questions/types/fill-missing-words-in-grid/register.js +37 -1
  329. package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +15 -1
  330. package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +262 -1
  331. package/dist/components/questions/types/gn-speaking-interview/register.js +121 -1
  332. package/dist/components/questions/types/gn-speaking-interview/transform.js +48 -1
  333. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +16 -1
  334. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +27 -1
  335. package/dist/components/questions/types/image-object-matching/index.js +2 -1
  336. package/dist/components/questions/types/image-object-matching/register.js +3 -1
  337. package/dist/components/questions/types/image-object-matching/transform.js +2 -1
  338. package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +102 -1
  339. package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +344 -1
  340. package/dist/components/questions/types/label-the-picture/index.js +2 -1
  341. package/dist/components/questions/types/label-the-picture/register.js +54 -1
  342. package/dist/components/questions/types/label-the-picture/transform.js +28 -1
  343. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +78 -1
  344. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +232 -1
  345. package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +2 -1
  346. package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +82 -1
  347. package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +27 -1
  348. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +70 -1
  349. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +386 -1
  350. package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +2 -1
  351. package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +100 -1
  352. package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +33 -1
  353. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +106 -1
  354. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +457 -1
  355. package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +2 -1
  356. package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +110 -1
  357. package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +35 -1
  358. package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +8 -1
  359. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +22 -1
  360. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +291 -1
  361. package/dist/components/questions/types/listen-and-speak-answer/index.js +2 -1
  362. package/dist/components/questions/types/listen-and-speak-answer/register.js +84 -1
  363. package/dist/components/questions/types/listen-and-speak-answer/transform.js +27 -1
  364. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +32 -1
  365. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +295 -1
  366. package/dist/components/questions/types/listen-and-speak-compare-images/register.js +79 -1
  367. package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +27 -1
  368. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +34 -1
  369. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +285 -1
  370. package/dist/components/questions/types/listen-and-speak-image-group/register.js +81 -1
  371. package/dist/components/questions/types/listen-and-speak-image-group/transform.js +26 -1
  372. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +38 -1
  373. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +385 -1
  374. package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +87 -1
  375. package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +32 -1
  376. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +26 -1
  377. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +247 -1
  378. package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +59 -1
  379. package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +18 -1
  380. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +26 -1
  381. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +256 -1
  382. package/dist/components/questions/types/listen-and-speak-question-list/register.js +81 -1
  383. package/dist/components/questions/types/listen-and-speak-question-list/transform.js +26 -1
  384. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +26 -1
  385. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +230 -1
  386. package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +56 -1
  387. package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +16 -1
  388. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +72 -1
  389. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +230 -1
  390. package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
  391. package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +66 -1
  392. package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +42 -1
  393. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +96 -1
  394. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +238 -1
  395. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +2 -1
  396. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +76 -1
  397. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +19 -1
  398. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +134 -1
  399. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +617 -1
  400. package/dist/components/questions/types/look-picture-fill-word-hint/index.js +2 -1
  401. package/dist/components/questions/types/look-picture-fill-word-hint/register.js +82 -1
  402. package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +29 -1
  403. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +38 -1
  404. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +359 -1
  405. package/dist/components/questions/types/match-by-writing-answer/index.js +3 -1
  406. package/dist/components/questions/types/match-by-writing-answer/register.js +58 -1
  407. package/dist/components/questions/types/match-by-writing-answer/transform.js +61 -1
  408. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +16 -1
  409. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +23 -1
  410. package/dist/components/questions/types/read-and-color-objects/index.js +2 -1
  411. package/dist/components/questions/types/read-and-color-objects/register.js +3 -1
  412. package/dist/components/questions/types/read-and-color-objects/transform.js +36 -1
  413. package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +125 -1
  414. package/dist/components/questions/types/read-displayed-content/register.js +64 -1
  415. package/dist/components/questions/types/read-displayed-content/transform.js +12 -1
  416. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +65 -1
  417. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +511 -1
  418. package/dist/components/questions/types/read-passage-and-answer-questions/index.js +2 -1
  419. package/dist/components/questions/types/read-passage-and-answer-questions/register.js +58 -1
  420. package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +28 -1
  421. package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +373 -1
  422. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +41 -1
  423. package/dist/components/questions/types/speaking-conversation/register.js +110 -1
  424. package/dist/components/questions/types/speaking-conversation/transform.js +34 -1
  425. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +74 -1
  426. package/dist/components/questions/types/speaking-cue-card/register.js +52 -1
  427. package/dist/components/questions/types/speaking-cue-card/transform.js +12 -1
  428. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +10 -1
  429. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +161 -1
  430. package/dist/components/questions/types/speaking-describe-image/index.js +2 -1
  431. package/dist/components/questions/types/speaking-describe-image/register.js +69 -1
  432. package/dist/components/questions/types/speaking-describe-image/transform.js +16 -1
  433. package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +234 -1
  434. package/dist/components/questions/types/spontaneous-qa/register.js +93 -1
  435. package/dist/components/questions/types/spontaneous-qa/transform.js +34 -1
  436. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +2 -0
  437. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +9 -0
  438. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.d.ts +2 -0
  439. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +179 -0
  440. package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.d.ts +5 -0
  441. package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +43 -0
  442. package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +14 -0
  443. package/dist/components/questions/types/spontaneous-qa-group/flatten.js +69 -0
  444. package/dist/components/questions/types/spontaneous-qa-group/index.d.ts +2 -0
  445. package/dist/components/questions/types/spontaneous-qa-group/index.js +2 -0
  446. package/dist/components/questions/types/spontaneous-qa-group/register.d.ts +2 -0
  447. package/dist/components/questions/types/spontaneous-qa-group/register.js +139 -0
  448. package/dist/components/questions/types/spontaneous-qa-group/transform.d.ts +2 -0
  449. package/dist/components/questions/types/spontaneous-qa-group/transform.js +52 -0
  450. package/dist/components/questions/types/true-false/TrueFalseClient.js +75 -1
  451. package/dist/components/questions/types/true-false/TrueFalseCreator.js +244 -1
  452. package/dist/components/questions/types/true-false/index.js +2 -1
  453. package/dist/components/questions/types/true-false/register.js +77 -1
  454. package/dist/components/questions/types/true-false/transform.js +32 -1
  455. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +25 -1
  456. package/dist/components/questions/types/true-false-group/TrueFalseGroupCreator.js +129 -1
  457. package/dist/components/questions/types/true-false-group/index.js +5 -1
  458. package/dist/components/questions/types/true-false-group/map-true-false-group-data.js +69 -1
  459. package/dist/components/questions/types/true-false-group/register.js +36 -1
  460. package/dist/components/questions/types/true-false-group/transform.js +43 -1
  461. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +233 -1
  462. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +631 -1
  463. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +28 -1
  464. package/dist/components/questions/types/word-fill-paragraph/index.js +2 -1
  465. package/dist/components/questions/types/word-fill-paragraph/register.js +136 -1
  466. package/dist/components/questions/types/word-fill-paragraph/transform.js +56 -1
  467. package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.js +86 -1
  468. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +246 -1
  469. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.d.ts +2 -0
  470. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +484 -1
  471. package/dist/components/questions/types/word-fill-structured-form/index.js +3 -1
  472. package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +113 -1
  473. package/dist/components/questions/types/word-fill-structured-form/register.js +36 -1
  474. package/dist/components/questions/types/word-fill-structured-form/transform.js +77 -1
  475. package/dist/components/questions/types/word-fill-structured-form/wfsf-word-bank.utils.js +110 -1
  476. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.d.ts +3 -0
  477. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +190 -0
  478. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupCreator.d.ts +3 -0
  479. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupCreator.js +182 -0
  480. package/dist/components/questions/types/word-order-and-match-group/index.d.ts +4 -0
  481. package/dist/components/questions/types/word-order-and-match-group/index.js +4 -0
  482. package/dist/components/questions/types/word-order-and-match-group/register.d.ts +2 -0
  483. package/dist/components/questions/types/word-order-and-match-group/register.js +121 -0
  484. package/dist/components/questions/types/word-order-and-match-group/transform.d.ts +2 -0
  485. package/dist/components/questions/types/word-order-and-match-group/transform.js +70 -0
  486. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +84 -1
  487. package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +192 -1
  488. package/dist/components/questions/types/word-ordering/index.js +2 -1
  489. package/dist/components/questions/types/word-ordering/register.js +46 -1
  490. package/dist/components/questions/types/word-ordering/transform.js +24 -1
  491. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +99 -1
  492. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +320 -1
  493. package/dist/components/questions/types/write-a-short-letter/index.js +2 -1
  494. package/dist/components/questions/types/write-a-short-letter/register.js +68 -1
  495. package/dist/components/questions/types/write-a-short-letter/transform.js +25 -1
  496. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +61 -1
  497. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +196 -1
  498. package/dist/components/questions/types/write-correct-verb-form/index.js +2 -1
  499. package/dist/components/questions/types/write-correct-verb-form/register.js +62 -1
  500. package/dist/components/questions/types/write-correct-verb-form/transform.js +17 -1
  501. package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +22 -1
  502. package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +174 -1
  503. package/dist/components/questions/types/write-sentences/index.js +2 -1
  504. package/dist/components/questions/types/write-sentences/register.js +58 -1
  505. package/dist/components/questions/types/write-sentences/transform.js +28 -1
  506. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +96 -1
  507. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +161 -1
  508. package/dist/components/questions/types/write-short-paragraph/index.js +2 -1
  509. package/dist/components/questions/types/write-short-paragraph/register.js +4 -1
  510. package/dist/components/questions/types/write-short-paragraph/transform.js +21 -1
  511. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +11 -1
  512. package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +8 -1
  513. package/dist/components/questions/viewer/QuestionGroupViewer.js +36 -1
  514. package/dist/components/questions/viewer/QuestionViewer.js +692 -1
  515. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +2 -1
  516. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +82 -1
  517. package/dist/components/questions/viewer/TrueFalseGroupViewer.js +11 -1
  518. package/dist/components/questions/viewer/WordFillStructuredFormViewer.js +14 -1
  519. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +3 -1
  520. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +95 -1
  521. package/dist/components/questions/viewer/index.js +8 -1
  522. package/dist/components/results/ResultReviewPageContainer.js +220 -1
  523. package/dist/components/results/ReviewQuestionRenderer.js +55 -1
  524. package/dist/components/results/hooks/useResultReviewPolling.js +76 -1
  525. package/dist/components/results/index.js +22 -1
  526. package/dist/components/results/renderers/QuestionExplanations.js +19 -1
  527. package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.js +94 -1
  528. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +75 -1
  529. package/dist/components/results/renderers/ReviewColoringRenderer.js +110 -1
  530. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +48 -1
  531. package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.js +87 -1
  532. package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.js +104 -1
  533. package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.js +123 -1
  534. package/dist/components/results/renderers/ReviewListeningHeader.js +104 -1
  535. package/dist/components/results/renderers/ReviewSpeakingAIPanel.js +116 -1
  536. package/dist/components/results/renderers/ReviewSpeakingMedia.js +48 -1
  537. package/dist/components/results/renderers/ReviewSpeakingQuestionItem.js +42 -1
  538. package/dist/components/results/renderers/ReviewSpeakingRenderer.js +12 -1
  539. package/dist/components/results/renderers/ReviewSpeakingRenderer.types.js +85 -1
  540. package/dist/components/results/renderers/review-question-body-dedicated.js +104 -1
  541. package/dist/components/results/renderers/review-question-body-movers.js +148 -1
  542. package/dist/components/results/renderers/review-speaking-map.js +226 -1
  543. package/dist/components/results/renderers/review-spontaneous-qa-group.d.ts +4 -0
  544. package/dist/components/results/renderers/review-spontaneous-qa-group.js +74 -0
  545. package/dist/components/results/report/BadgeTitle.js +5 -1
  546. package/dist/components/results/report/ExamSkillRow.js +20 -1
  547. package/dist/components/results/report/OceanEduExamReport.js +58 -1
  548. package/dist/components/results/report/OceanEduSpeakingReport.js +62 -1
  549. package/dist/components/results/report/OverallAssessmentSection.js +17 -1
  550. package/dist/components/results/report/ResultErrorState.js +18 -1
  551. package/dist/components/results/report/ResultLoadingState.js +10 -1
  552. package/dist/components/results/report/ResultNotPublicState.js +27 -1
  553. package/dist/components/results/report/SpeakingReportParts.js +14 -1
  554. package/dist/components/results/report/SpeakingReportShared.js +86 -1
  555. package/dist/components/results/report/SpeakingRubricSection.js +171 -1
  556. package/dist/components/results/report/speaking-report.utils.js +198 -1
  557. package/dist/components/results/result-review.utils.js +6 -1
  558. package/dist/components/results/review-data.js +222 -1
  559. package/dist/components/results/review-renderer.utils.js +97 -1
  560. package/dist/components/results/review-status.utils.js +36 -1
  561. package/dist/components/results/review-types.js +1 -1
  562. package/dist/components/results/speaking/SpeakingResultAudioPlayer.js +77 -1
  563. package/dist/components/results/speaking/SpeakingReviewResult.js +48 -1
  564. package/dist/components/results/utils/review-score.utils.js +72 -1
  565. package/dist/components/results/utils/speaking-summary.js +224 -1
  566. package/dist/components/results/utils/transform-speaking-review.js +183 -1
  567. package/dist/components/shared/RichTextEditor.d.ts +9 -0
  568. package/dist/components/shared/RichTextEditor.js +47 -0
  569. package/dist/components/shared/audio/SpeakingAudioPlayer.js +100 -1
  570. package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +140 -1
  571. package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +10 -1
  572. package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.d.ts +1 -1
  573. package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +18 -1
  574. package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +11 -1
  575. package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +11 -1
  576. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.d.ts +2 -1
  577. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +19 -1
  578. package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +13 -1
  579. package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.d.ts +1 -1
  580. package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +3 -1
  581. package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +6 -1
  582. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +5 -1
  583. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +9 -1
  584. package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.d.ts +2 -1
  585. package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +61 -1
  586. package/dist/components/themes/cambridge-yle/CambridgeYleTimer.d.ts +1 -1
  587. package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +25 -1
  588. package/dist/components/themes/cambridge-yle/index.js +13 -1
  589. package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +25 -1
  590. package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +89 -1
  591. package/dist/components/themes/english-certification/EcWordFillParagraph.js +56 -1
  592. package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +192 -1
  593. package/dist/components/themes/english-certification/EcWritingTask.js +60 -1
  594. package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +108 -1
  595. package/dist/components/themes/english-certification/EnglishCertificationBrand.js +11 -1
  596. package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +34 -1
  597. package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +15 -1
  598. package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +19 -1
  599. package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +166 -1
  600. package/dist/components/themes/english-certification/EnglishCertificationHeader.js +31 -1
  601. package/dist/components/themes/english-certification/EnglishCertificationInstructionBanner.js +79 -1
  602. package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +71 -1
  603. package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +79 -1
  604. package/dist/components/themes/english-certification/EnglishCertificationPartHeader.js +47 -1
  605. package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +77 -1
  606. package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +25 -1
  607. package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +36 -1
  608. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +81 -1
  609. package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +95 -1
  610. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +18 -1
  611. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +32 -1
  612. package/dist/components/themes/english-certification/english-cert-scroll.js +163 -1
  613. package/dist/components/themes/english-certification/english-certification-theme.js +58 -1
  614. package/dist/components/themes/english-certification/grouped-question-utils.js +243 -1
  615. package/dist/components/themes/english-certification/index.js +24 -1
  616. package/dist/components/themes/index.js +25 -1
  617. package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +170 -1
  618. package/dist/components/themes/summer-sky/SummerSkyExitModal.js +21 -1
  619. package/dist/components/themes/summer-sky/SummerSkyLayout.js +221 -1
  620. package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +18 -1
  621. package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +193 -1
  622. package/dist/components/themes/types.js +1 -1
  623. package/dist/components/ui/alert-dialog.js +26 -1
  624. package/dist/components/ui/alert.js +22 -1
  625. package/dist/components/ui/avatar.js +12 -1
  626. package/dist/components/ui/badge.js +38 -1
  627. package/dist/components/ui/button.js +35 -1
  628. package/dist/components/ui/calendar.js +76 -1
  629. package/dist/components/ui/card.js +16 -1
  630. package/dist/components/ui/carousel.js +91 -1
  631. package/dist/components/ui/checkbox.js +9 -1
  632. package/dist/components/ui/collapsible.js +13 -1
  633. package/dist/components/ui/command.js +34 -1
  634. package/dist/components/ui/confirm-dialog.js +49 -1
  635. package/dist/components/ui/dialog.js +23 -1
  636. package/dist/components/ui/drawer.js +35 -1
  637. package/dist/components/ui/dropdown-menu.js +33 -1
  638. package/dist/components/ui/file-upload.js +155 -1
  639. package/dist/components/ui/form.js +62 -1
  640. package/dist/components/ui/hover-card.js +10 -1
  641. package/dist/components/ui/image-preview.js +29 -1
  642. package/dist/components/ui/input.js +8 -1
  643. package/dist/components/ui/label.js +9 -1
  644. package/dist/components/ui/language-switcher.js +15 -1
  645. package/dist/components/ui/multi-select.js +90 -1
  646. package/dist/components/ui/points-input.js +33 -1
  647. package/dist/components/ui/popover.js +26 -1
  648. package/dist/components/ui/progress.js +16 -1
  649. package/dist/components/ui/radio-group.js +12 -1
  650. package/dist/components/ui/scroll-area.js +11 -1
  651. package/dist/components/ui/select.js +38 -1
  652. package/dist/components/ui/separator.js +8 -1
  653. package/dist/components/ui/sheet.js +40 -1
  654. package/dist/components/ui/single-select.js +76 -1
  655. package/dist/components/ui/skeleton.js +6 -1
  656. package/dist/components/ui/switch.js +8 -1
  657. package/dist/components/ui/table.js +20 -1
  658. package/dist/components/ui/tabs.js +17 -1
  659. package/dist/components/ui/textarea.js +8 -1
  660. package/dist/components/ui/tooltip.js +11 -1
  661. package/dist/index.d.ts +3 -0
  662. package/dist/index.js +24 -1
  663. package/dist/shared/constants/ApiConstant.d.ts +1 -0
  664. package/dist/shared/constants/ApiConstant.js +278 -1
  665. package/dist/shared/constants/QueryKeyConstant.js +112 -1
  666. package/dist/shared/constants/exam-level.enum.js +41 -1
  667. package/dist/shared/constants/landings/placement-test.constants.js +431 -1
  668. package/dist/shared/constants/question-skills.js +277 -1
  669. package/dist/shared/constants/routes.js +81 -1
  670. package/dist/shared/index.js +10 -1
  671. package/dist/shared/lib/hooks/index.js +10 -1
  672. package/dist/shared/lib/hooks/useAlertDialog.js +57 -1
  673. package/dist/shared/lib/hooks/useAudioPlayCounter.js +30 -1
  674. package/dist/shared/lib/hooks/useAudioPreview.js +81 -1
  675. package/dist/shared/lib/hooks/useAudioUpload.js +24 -1
  676. package/dist/shared/lib/hooks/useConfirmDialog.js +89 -1
  677. package/dist/shared/lib/hooks/useDebouncedCallback.js +32 -1
  678. package/dist/shared/lib/hooks/useDelayedAutoPlay.js +12 -1
  679. package/dist/shared/lib/hooks/useFileUpload.js +24 -1
  680. package/dist/shared/lib/hooks/useImageUrl.js +5 -1
  681. package/dist/shared/lib/hooks/usePresignedFileUrl.js +108 -1
  682. package/dist/shared/lib/hooks/useReactHookForm.js +31 -1
  683. package/dist/shared/lib/hooks/useToast.js +62 -1
  684. package/dist/shared/lib/html-content.js +22 -1
  685. package/dist/shared/lib/i18n/index.js +54 -1
  686. package/dist/shared/lib/i18n/messages/en/admin.d.ts +11 -4
  687. package/dist/shared/lib/i18n/messages/en/admin.js +2111 -1
  688. package/dist/shared/lib/i18n/messages/en/auth.js +86 -1
  689. package/dist/shared/lib/i18n/messages/en/common.js +140 -1
  690. package/dist/shared/lib/i18n/messages/en/file-upload.js +18 -1
  691. package/dist/shared/lib/i18n/messages/en/student.js +133 -1
  692. package/dist/shared/lib/i18n/messages/en/terms.js +37 -1
  693. package/dist/shared/lib/i18n/messages/en.d.ts +11 -4
  694. package/dist/shared/lib/i18n/messages/en.js +15 -1
  695. package/dist/shared/lib/i18n/messages/vi/admin.d.ts +11 -4
  696. package/dist/shared/lib/i18n/messages/vi/admin.js +2111 -1
  697. package/dist/shared/lib/i18n/messages/vi/auth.js +86 -1
  698. package/dist/shared/lib/i18n/messages/vi/common.js +140 -1
  699. package/dist/shared/lib/i18n/messages/vi/file-upload.js +18 -1
  700. package/dist/shared/lib/i18n/messages/vi/student.js +133 -1
  701. package/dist/shared/lib/i18n/messages/vi/terms.js +37 -1
  702. package/dist/shared/lib/i18n/messages/vi.d.ts +11 -4
  703. package/dist/shared/lib/i18n/messages/vi.js +15 -1
  704. package/dist/shared/lib/i18n/types.js +1 -1
  705. package/dist/shared/lib/stores/examQuestionStore.js +212 -1
  706. package/dist/shared/lib/stores/examTakingStore.js +240 -1
  707. package/dist/shared/lib/stores/gradingTransientStore.js +39 -1
  708. package/dist/shared/lib/stores/localeStore.js +10 -1
  709. package/dist/shared/lib/themes/config/starters.config.js +71 -1
  710. package/dist/shared/lib/themes/index.js +49 -1
  711. package/dist/shared/lib/themes/types.js +2 -1
  712. package/dist/shared/lib/toast.js +43 -1
  713. package/dist/shared/lib/utils/public-page.js +44 -1
  714. package/dist/shared/lib/utils/question-reverse-transform.js +438 -1
  715. package/dist/shared/lib/utils/question-transform-types.js +1 -1
  716. package/dist/shared/lib/utils/question-transform-validation.js +194 -1
  717. package/dist/shared/lib/utils/question-transform.js +199 -1
  718. package/dist/shared/lib/utils.js +168 -1
  719. package/dist/shared/lib/validation/choose-correct-answer.validation.js +111 -1
  720. package/dist/shared/lib/validation/label-the-picture.validation.js +19 -1
  721. package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +46 -1
  722. package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +21 -1
  723. package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +34 -1
  724. package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +40 -1
  725. package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +45 -1
  726. package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +19 -1
  727. package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +39 -1
  728. package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +32 -1
  729. package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +19 -1
  730. package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +28 -1
  731. package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +12 -1
  732. package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +47 -1
  733. package/dist/shared/lib/validation/read-and-color-objects.validation.js +29 -1
  734. package/dist/shared/lib/validation/read-displayed-content.validation.js +28 -1
  735. package/dist/shared/lib/validation/speaking-conversation.validation.js +75 -1
  736. package/dist/shared/lib/validation/speaking-describe-image.validation.js +31 -1
  737. package/dist/shared/lib/validation/spontaneous-qa.validation.js +48 -1
  738. package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +38 -1
  739. package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +39 -1
  740. package/dist/shared/lib/validation/word-fill-paragraph.validation.js +43 -1
  741. package/dist/shared/lib/validation/word-ordering.validation.js +33 -1
  742. package/dist/shared/types/branch.types.js +6 -1
  743. package/dist/shared/types/common.types.js +49 -1
  744. package/dist/shared/types/entities/exam-schedule.types.js +5 -1
  745. package/dist/shared/types/entities/exam.types.js +1 -1
  746. package/dist/shared/types/entities/question.types.js +4 -1
  747. package/dist/shared/types/entities/result.types.js +4 -1
  748. package/dist/shared/types/entities/user.types.js +4 -1
  749. package/dist/shared/types/exam-taking.types.js +2 -1
  750. package/dist/shared/types/history.types.js +34 -1
  751. package/dist/shared/types/index.js +1 -1
  752. package/dist/shared/types/questions/choose-correct-adjective.js +4 -1
  753. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  754. package/dist/shared/types/questions/choose-the-correct-answer-group.js +1 -1
  755. package/dist/shared/types/questions/choose-the-correct-answer.js +9 -1
  756. package/dist/shared/types/questions/cross-out-word-group.d.ts +2 -0
  757. package/dist/shared/types/questions/cross-out-word-group.js +1 -1
  758. package/dist/shared/types/questions/fill-missing-words-in-grid.js +11 -1
  759. package/dist/shared/types/questions/index.d.ts +1 -0
  760. package/dist/shared/types/questions/index.js +46 -1
  761. package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
  762. package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
  763. package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
  764. package/dist/shared/types/questions/listen-and-speak-info-exchange.js +3 -1
  765. package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +3 -1
  766. package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
  767. package/dist/shared/types/questions/listen-and-speak-with-story-images.js +3 -1
  768. package/dist/shared/types/questions/look-picture-choose-correct-answer.js +5 -1
  769. package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +9 -1
  770. package/dist/shared/types/questions/look-picture-fill-word-hint.js +7 -1
  771. package/dist/shared/types/questions/read-displayed-content.js +1 -1
  772. package/dist/shared/types/questions/speaking-conversation.js +7 -1
  773. package/dist/shared/types/questions/speaking-describe-image.js +1 -1
  774. package/dist/shared/types/questions/spontaneous-qa.js +1 -1
  775. package/dist/shared/types/questions/word-order-and-match-group.d.ts +33 -0
  776. package/dist/shared/types/questions/word-order-and-match-group.js +1 -0
  777. package/dist/shared/types/questions/write-correct-verb-form.js +12 -1
  778. package/dist/shared/types/role.types.js +1 -1
  779. package/dist/shared/types/structure-management.js +28 -1
  780. package/dist/shared/types/student.types.js +57 -1
  781. package/dist/shared/types/teacher-dashboard-rooms.js +2 -1
  782. package/package.json +184 -166
  783. package/dist/mcp/config.d.ts +0 -12
  784. package/dist/mcp/config.js +0 -1
  785. package/dist/mcp/http-client.d.ts +0 -6
  786. package/dist/mcp/http-client.js +0 -1
  787. package/dist/mcp/index.d.ts +0 -6
  788. package/dist/mcp/index.js +0 -2
  789. package/dist/mcp/tools/_helpers.d.ts +0 -4
  790. package/dist/mcp/tools/_helpers.js +0 -1
  791. package/dist/mcp/tools/exams.d.ts +0 -2
  792. package/dist/mcp/tools/exams.js +0 -1
  793. package/dist/mcp/tools/index.d.ts +0 -2
  794. package/dist/mcp/tools/index.js +0 -1
  795. package/dist/mcp/tools/results.d.ts +0 -2
  796. package/dist/mcp/tools/results.js +0 -1
@@ -1 +1,955 @@
1
- "use client";import{jsx as e,jsxs as t,Fragment as n}from"react/jsx-runtime";import{useEffect as i,useRef as s,useState as r,useMemo as a,useCallback as o}from"react";import d from"next/image";import l from"../../../components/themes/summer-sky/assets/images/clock.png";import c from"../../../components/themes/summer-sky/assets/images/submit.png";import m from"../../../components/themes/summer-sky/assets/images/has_answered.png";import{toast as p}from"sonner";import{Loader2 as u}from"lucide-react";import{t as g}from"../../../shared/lib/i18n";import{transformAnswersForSubmission as h}from"./utils/answer-transformers";import{getThemeForLevel as x,normalizeExamTheme as b}from"../../../shared/lib/themes";import{useExamTakingStore as f}from"../../../shared/lib/stores/examTakingStore";import{useGradingTransientStore as I}from"../../../shared/lib/stores/gradingTransientStore";import{AlertDialog as w,AlertDialogAction as E,AlertDialogCancel as S,AlertDialogContent as y,AlertDialogDescription as _,AlertDialogFooter as v,AlertDialogHeader as N,AlertDialogTitle as A}from"../../../components/ui/alert-dialog";import{useExamTimer as T}from"./hooks/useExamTimer";import{useAutoSave as C}from"./hooks/useAutoSave";import{useProctoringCamera as k}from"./hooks/useProctoringCamera";import{getStandardExamTheme as P}from"../../../components/themes";import{QuestionRenderer as q}from"./components/QuestionRenderer";import{FullscreenLoading as M}from"../entry/fullscreen-loading";import{SummerSkySubmitModal as R}from"../../../components/themes/summer-sky/SummerSkySubmitModal";import{SummerSkyExitModal as D}from"../../../components/themes/summer-sky/SummerSkyExitModal";import{EnglishCertificationExamLayout as L,EnglishCertificationQuestionRenderer as G,scrollToEnglishCertQuestion as Q}from"../../../components/themes/english-certification";import{groupQuestionsByPart as B}from"./utils/question-transformers";import{unwrap as O,unwrapAttempt as Y}from"./exam-taking.utils";import{ExamTakingApiProvider as F}from"./ExamTakingApiContext";function K({roomId:F,api:K,onNavigate:U,resultPath:$=e=>`/student/results/${e}`,fallbackPath:V="/student/contests/discover",proctoringEnabled:j=!1,achieversExamTitle:H,onSubmitted:z}){const W=f(),J=s(!1),X=o((e,t=!1)=>{U?U(e,{replace:t}):"undefined"!=typeof window&&(t?window.location.replace(e):window.location.href=e)},[U]),{isCameraActive:Z}=k({api:K,attemptId:W.attemptId,intervalMs:1e4,enabled:j}),[ee,te]=r(!0),ne=W.currentPartIndex,ie=W.currentQuestionGlobalIndex,se=o(e=>{const t=f.getState().currentPartIndex,n="function"==typeof e?e(t):e;f.getState().setCurrentPartIndex(n)},[]),re=o(e=>{const t=f.getState().currentQuestionGlobalIndex,n="function"==typeof e?e(t):e;f.getState().setCurrentQuestionGlobalIndex(n)},[]),[ae,oe]=r([]),[de,le]=r("SUMMER_SKY"),[ce,me]=r(""),[pe,ue]=r(!1),[ge,he]=r(!1),[xe,be]=r(null),[fe,Ie]=r(""),we=o(e=>{f.getState().markIntroPlayed(e)},[]),Ee="TOEIC"===W.examType||"ENGLISH_CERTIFICATION"===ce,Se=a(()=>{if(!Ee||0===W.parts.length)return ne;const e=W.currentIndex;for(let t=0;t<W.parts.length;t++){const n=W.parts[t],i=n.startIndex??0,s=n.endIndex??i;if(e>=i&&e<=s)return t}return 0},[Ee,W.currentIndex,W.parts,ne]);i(()=>{(async()=>{try{te(!0);let e=O(await K.getActiveAttempt(F));e||(e=Y(O(await K.startAttempt(F))));let t=null;try{t=K.getRoom?O(await K.getRoom(F)):null,t&&be(t)}catch{}const{examTemplate:n,questions:i}=O(await K.getQuestions(e.id));n?.title&&Ie(n.title);const s=n?.parts??e.exam?.template?.parts,r=B(i,s),a=n?.subTitle??null,o=(r.every(e=>e.questions.every(e=>"SPEAKING"===e.question_skill_type)),r.some(e=>"SPEAKING"===e.category||e.questions.every(e=>"SPEAKING"===e.question_skill_type)));oe(r);const d=n?.theme||e?.exam?.template?.theme||e?.exam?.theme||t?.exam?.template?.theme||t?.exam?.theme||"",l=b(d);if(o){const e=["SUMMER_SKY"].includes(d);le(e?d:"SUMMER_SKY")}const c=new Date(e.startedAt),m=60*e.durationMinutes*1e3,u=c.getTime()+m,h=Date.now();if(0===Math.max(0,u-h)){p.info(g("student.exam.take.timeExpired"));try{await K.submitAttempt(e.id)}catch{}return f.persist.clearStorage(),void X($(e.id))}W.hydrateFromServer({attemptId:e.id,examRoomId:F,questions:i.map(e=>({id:e.id,question_type:e.question_type,content:e.content,is_example:e.is_example,total_points:e.total_points,part_id:e.part_id,part_no:e.part_no,question_number:e.question_number})),durationMinutes:e.durationMinutes,startedAt:e.startedAt,examType:a??void 0,examTitle:n?.title??void 0,parts:s}),a&&W.setExamType(a),me(l),s&&W.setParts(s),te(!1)}catch(e){const t=e?.response?.data?.errorCode;if("ATTEMPT_ALREADY_SUBMITTED"===t)return p.info(g("student.exam.take.submitted")),void X(V,!0);console.error("Failed to load exam data:",e),p.error(g("student.exam.take.loadError")),X(V,!0)}})()},[F]);const ye=a(()=>ae.length>0&&ae.every(e=>e.questions.every(e=>"SPEAKING"===e.question_skill_type)),[ae]),_e=a(()=>{const e=ae[ne];return!!e&&(("SPEAKING_DESCRIBE_IMAGE"!==e.questionType&&"SPONTANEOUS_QA"!==e.questionType&&"SPEAKING_CUE_CARD"!==e.questionType&&"READ_DISPLAYED_CONTENT"!==e.questionType||"CAMBRIDGE_YLE"!==ce)&&("SPEAKING"===e.category||e.questions.every(e=>"SPEAKING"===e.question_skill_type)))},[ae,ne,ce]),{remainingSeconds:ve,warningLevel:Ne}=T({onTimeExpired:async()=>{p.info(g("student.exam.take.autoSubmit")),await ke()},onWarningThreshold:ye||Ee?void 0:()=>{p.warning(g("student.exam.take.timeWarning"))},warningThresholdSeconds:ye||Ee?void 0:300,warningThresholds:Ee?[{seconds:900,level:"warning",callback:()=>p.warning("Còn 15 phút!",{duration:3e3})},{seconds:300,level:"critical",callback:()=>p.warning("Còn 5 phút! Hãy nộp bài ngay!",{duration:5e3})}]:ye?[{seconds:300,level:"warning",callback:()=>p.warning("Còn 5 phút! Hãy hoàn thành bài thi.",{duration:5e3})},{seconds:120,level:"critical",callback:()=>p.warning("Còn 2 phút! Bài thi sẽ được tự động nộp.",{duration:5e3})}]:void 0}),Ae=C({api:K,attemptId:W.attemptId||"",parts:ae,intervalMs:Ee?15e3:3e4,onSaveError:e=>{console.error("Save error:",e)}}),Te=s(Ae.saveNow),Ce=s(ae);i(()=>{Te.current=Ae.saveNow,Ce.current=ae}),i(()=>{const e=()=>{const e=f.getState();if(!e.attemptId||!e.isDirty||e.isSubmitting)return;const t=h(e.answers,Ce.current);if(0!==t.length)try{K.saveAnswers?.({attemptId:e.attemptId,answers:t,keepalive:!0})}catch{}},t=()=>{"hidden"===document.visibilityState&&e()},n=()=>{const e=f.getState();e.isDirty&&!e.isSubmitting&&Te.current()};return document.addEventListener("visibilitychange",t),window.addEventListener("pagehide",e),window.addEventListener("online",n),()=>{document.removeEventListener("visibilitychange",t),window.removeEventListener("pagehide",e),window.removeEventListener("online",n)}},[]),i(()=>{ye||Te.current()},[W.currentIndex,ne,ie,ye]);const ke=async()=>{if(!J.current){J.current=!0,W.setIsSubmitting(!0),ue(!1);try{W.isDirty&&await Ae.saveNow();const e=O(await K.submitAttempt(W.attemptId)),t=e?.pendingAnswerIds??[];W.attemptId&&(I.getState().setPending(W.attemptId,t),z?.({attemptId:W.attemptId,pendingAnswerIds:t})),0===t.length?p.success(g("student.exam.take.submitSuccess")):p.success(g("student.exam.take.submittedAIGradingInProgress")),f.persist.clearStorage(),X($(W.attemptId),!0)}catch(e){console.error("Failed to submit exam:",e);const t=e?.response?.data?.errorCode;if("ATTEMPT_ALREADY_SUBMITTED"===t)return f.persist.clearStorage(),void X($(W.attemptId),!0);p.warning(g("student.exam.take.submitError")),J.current=!1,W.setIsSubmitting(!1)}}},Pe=o(()=>{ue(!0)},[]),qe=s(new Map),Me=s([]),Re=o((e,t)=>{W.setAnswer(e,t);const n=qe.current.get(e)||e,i=Me.current.findIndex(e=>e.questionId===n);-1!==i&&re(e=>e!==i?i:e)},[]),De=s("forward"),Le=e=>{if("CAMBRIDGE_YLE"!==ce)return!1;const t=ae[e];return"SPEAKING_DESCRIBE_IMAGE"===t?.questionType||"SPONTANEOUS_QA"===t?.questionType||"SPEAKING_CUE_CARD"===t?.questionType||"READ_DISPLAYED_CONTENT"===t?.questionType},Ge=()=>{const e=()=>{window.scrollTo({top:0,behavior:"smooth"});const e=document.getElementById("exam-main-scroll-area");if(e){e.scrollTo({top:0,behavior:"smooth"});e.querySelectorAll(".overflow-y-auto").forEach(e=>{e.scrollTo({top:0,behavior:"smooth"})})}};requestAnimationFrame(e),setTimeout(e,100),setTimeout(e,250)},Qe=()=>{if("ENGLISH_CERTIFICATION"===ce){const e=W.parts[Se+1];e&&W.goToQuestion(e.startIndex??0)}else if(Ee)W.goToQuestion(Math.min(W.currentIndex+1,W.questions.length-1));else if("CAMBRIDGE_YLE"===ce&&Le(ne)){const e=ie+1,t=We[e];t&&(t.partIndex!==ne&&(De.current="forward",se(t.partIndex)),re(e))}else De.current="forward",se(e=>{const t=Math.min(e+1,ae.length-1);if("CAMBRIDGE_YLE"===ce){const e=We.find(e=>e.partIndex===t);e&&re(e.globalIndex)}return t});Ge()},Be=()=>{if("ENGLISH_CERTIFICATION"===ce){const e=W.parts[Se-1];e&&W.goToQuestion(e.startIndex??0)}else if(Ee)W.goToQuestion(Math.max(W.currentIndex-1,0));else if("CAMBRIDGE_YLE"===ce&&Le(ne)){const e=ie-1,t=We[e];t&&(t.partIndex!==ne&&(De.current="backward",se(t.partIndex)),re(e))}else De.current="backward",se(e=>{const t=Math.max(e-1,0);if("CAMBRIDGE_YLE"===ce){const e=We.find(e=>e.partIndex===t);e&&re(e.globalIndex)}return t});Ge()},[Oe,Ye]=r(!1),Fe=()=>{he(!0)},Ke=a(()=>ae.reduce((e,t)=>e+("CAMBRIDGE_YLE"===ce?t.questions.length:t.questions.filter(e=>!e.is_example).length),0),[ae,ce]),Ue=a(()=>{const e=new Map;return ae.forEach(t=>{t.questions.forEach(n=>{if(n.is_example)return;const i=n.content;if("FILL_MISSING_WORDS_IN_GRID"===t.questionType&&i?.grid&&i.grid.forEach((t,i)=>{t.forEach((t,s)=>{e.set(`${i}-${s}`,n.id)})}),"LABEL_THE_PICTURE"===t.questionType&&i?.labels&&i.labels.forEach(t=>{t.id&&e.set(t.id,n.id)}),"READ_AND_COLOR_OBJECTS"===t.questionType){const t=i?.regions||i?.regionNodes;t?.forEach(t=>{t.id&&e.set(t.id,n.id)})}if("IMAGE_OBJECT_MATCHING"===t.questionType){const t=i?.regions||i?.regionNodes;t?.forEach(t=>{t.id&&e.set(t.id,n.id)})}if("LOOK_PICTURE_FILL_WORD_HINT"===t.questionType){const t=i?.word;if(t&&"string"==typeof t){const i=/\{(\d+)\}/g;let s;for(;null!==(s=i.exec(t));){const t=s[1];e.set(`${n.id}-blank-${t}`,n.id)}}}})}),e},[ae]);i(()=>{qe.current=Ue},[Ue]);const $e=a(()=>{if(!W.answers)return new Set;const e=new Set(ae.flatMap(e=>e.questions.filter(e=>!e.is_example).map(e=>e.id))),t=new Set;return Object.keys(W.answers).forEach(n=>{const i=W.answers[n];if(void 0===i||""===i)return;if(e.has(n))return void t.add(n);const s=Ue.get(n);s&&e.has(s)&&t.add(s)}),t},[W.answers,ae,Ue]),Ve=$e.size+("CAMBRIDGE_YLE"===ce?ae.reduce((e,t)=>e+t.questions.filter(e=>e.is_example).length,0):0),je=a(()=>xe?.exam?.name||xe?.name||(Ee?"TOEIC Listening and Reading Test":"Bài thi"),[xe,Ee]),He=a(()=>x(xe?.exam?.level).displayName,[xe?.exam?.level]),{sidebarParts:ze,sidebarQuestions:We}=a(()=>{const e=[],t=[];let n=0;return ae.forEach((i,s)=>{const r="CAMBRIDGE_YLE"===ce?i.questions:i.questions.filter(e=>!e.is_example);let a=0;r.forEach(e=>{const r=!("CAMBRIDGE_YLE"!==ce||!e.is_example)||$e.has(e.id);r&&a++;const o=i.questions.findIndex(t=>t.id===e.id);t.push({globalIndex:n,partIndex:s,indexInPart:o,questionNumber:e.question_number,isAnswered:r,questionId:e.id,isExample:e.is_example}),n++}),e.push({partIndex:s,partNo:i.partNo,name:i.name,category:i.category,points:i.points,totalQuestions:"CAMBRIDGE_YLE"===ce?i.questions.length:i.questions.filter(e=>!e.is_example).length,answeredCount:a})}),{sidebarParts:e,sidebarQuestions:t}},[ae,$e,ce]);if(i(()=>{Me.current=We},[We]),ee)return e(M,{});const Je=ne,Xe=ae[Je];if(!Xe)return e("div",{className:"flex min-h-screen items-center justify-center",children:t("div",{className:"text-center",children:[e("p",{className:"text-lg font-semibold text-red-500",children:"Không tìm thấy câu hỏi"}),e("button",{type:"button",onClick:()=>X(V),className:"mt-4 rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600",children:"Quay lại"})]})});if(_e)return t(n,{children:[e(q,{part:Xe,answers:W.answers||{},onAnswerChange:Re,isReviewMode:!1,onExit:Fe,onPartComplete:Je<ae.length-1?Qe:Pe,onPartBack:Je>0?Be:void 0,speakingTheme:de,initialQuestionIndex:"backward"===De.current?Xe.questions.length-1:0,skipTeacherVideo:W.playedIntroParts.has(Je)||fe!==H,onTeacherIntroPlayed:()=>we(Je)},`${Xe.partId}-${Je}`),"SUMMER_SKY"===de?pe&&e(R,{onClose:()=>ue(!1),onSubmit:ke,isSubmitting:W.isSubmitting}):e(w,{open:pe,onOpenChange:ue,children:t(y,{children:[t(N,{children:[e(A,{children:"Nộp bài thi"}),e(_,{children:"Bạn có chắc chắn muốn nộp bài không? Sau khi nộp, bạn sẽ không thể thay đổi câu trả lời."})]}),t(v,{children:[e(S,{disabled:W.isSubmitting,children:"Quay lại"}),e(E,{onClick:ke,disabled:W.isSubmitting,children:W.isSubmitting?t(n,{children:[e(u,{className:"mr-2 h-4 w-4 animate-spin"}),"Đang nộp..."]}):"Nộp bài"})]})]})}),t("div",{className:"fixed top-8 right-6 z-[100] flex flex-col items-end gap-4 sm:gap-6 lg:top-10 lg:right-16",children:[t("div",{className:"flex items-center gap-4 sm:gap-6",children:[ve>0&&t("div",{className:"flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] backdrop-blur-[8px] transition-all duration-300 ease-in-out border-none shadow-[0_4px_20px_rgba(0,0,0,0.25)] "+("critical"===Ne?"bg-[rgba(220,38,38,0.85)] shadow-[0_0_24px_rgba(220,38,38,0.5)] animate-pulse":"warning"===Ne?"bg-[rgba(180,120,0,0.85)]":"bg-[#001590]"),children:[e(d,{src:l,alt:"clock",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10"}),t("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:"'SVN-Volte Rounded', 'Quicksand', sans-serif",textShadow:"0 2px 4px rgba(0, 0, 0, 0.2)"},children:[Math.floor(ve/60).toString().padStart(2,"0"),":",(ve%60).toString().padStart(2,"0")]})]}),t("div",{className:"flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] bg-[#FF9900] shadow-[0_4px_20px_rgba(0,0,0,0.25)] border-none shrink-0",title:"Số câu hỏi đã hoàn thành",children:[e(d,{src:m,alt:"Progress",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10"}),t("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:'"SVN-Volte Rounded", "Quicksand", sans-serif',textShadow:"0 2px 4px rgba(0,0,0,0.2)"},children:[Ve,"/",Ke]})]})]}),t("button",{onClick:Pe,disabled:W.isSubmitting,className:"group cursor-pointer flex items-center h-[40px] sm:h-[50px] w-[130px] sm:w-[170px] rounded-[33.5px] shadow-[0_4px_20px_rgba(0,0,0,0.25)] transition-all duration-300 hover:scale-105 active:scale-95 disabled:pointer-events-none disabled:opacity-60 border-none "+(W.isSubmitting?"bg-slate-400":"bg-[#16CA3B] hover:bg-[#15b736]"),children:[e(d,{src:c,alt:"Submit",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none transition-transform duration-300 group-hover:scale-110 z-10"}),W.isSubmitting?e("div",{className:"flex-1 flex justify-center pr-[10px] sm:pr-[16px]",children:e(u,{className:"h-6 w-6 animate-spin text-white"})}):e("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:'"SVN-Volte Rounded", "Quicksand", sans-serif',textShadow:"0 2px 4px rgba(0,0,0,0.2)"},children:"Nộp bài"})]})]}),W.isSubmitting&&e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm",children:t("div",{className:"rounded-xl bg-white p-8 text-center shadow-2xl",children:[e(u,{className:"mx-auto h-12 w-12 animate-spin text-green-500"}),e("p",{className:"mt-4 text-lg font-semibold text-gray-700",children:"Đang nộp bài..."}),e("p",{className:"mt-2 text-sm text-gray-500",children:"Vui lòng đợi trong giây lát"})]})})]});if(Ee){const i=ae[Se],s=W.parts[Se],r=s?Math.max(0,W.currentIndex-(s.startIndex??0)):0,a=(s?.startIndex??0)+1,o=(s?.endIndex??a-1)+1,d={examName:je,remainingSeconds:W.remainingSeconds,totalQuestions:Ke,answeredCount:Ve,isSubmitting:W.isSubmitting,onSubmit:ke,onClose:Fe,lastSavedAt:W.lastSavedAt,isSaving:Ae.isSaving,partLabel:(s?.name||i?.name||"").toUpperCase()||void 0,partSubLabel:s?.category||void 0,questionRange:a===o?`${a}`:`${a}–${o}`},l=i?.questions?.[r],c="ENGLISH_CERTIFICATION"===ce,m={onPrevious:Be,onClear:c?void 0:()=>{l&&Re(l.id,{optionId:null})},onToggleMark:c?void 0:()=>{l&&W.toggleFlag(l.id)},onNext:Qe,isMarked:!!l&&W.flaggedQuestions.has(l.id),showSecondaryActions:!c,isPreviousDisabled:c?Se<=0:W.currentIndex<=0,isNextDisabled:c?Se>=W.parts.length-1:W.currentIndex>=W.questions.length-1},p={parts:W.parts,questions:W.questions,answers:W.answers,flaggedQuestions:W.flaggedQuestions,currentIndex:W.currentIndex,onQuestionClick:e=>{W.goToQuestion(e);const t=W.questions[e];t&&Q(t.id,{behavior:"smooth",block:"start"})},onPartClick:e=>{const t=W.parts[e];if(t){const n=t.startIndex??0;W.goToQuestion(n),W.setCurrentPartIndex(e);const i=W.questions[n];i&&Q(i.id,{behavior:"smooth",block:"start"})}}};if(i&&s)return t(n,{children:[e(L,{headerProps:d,navigationProps:p,footerProps:m,children:e(G,{part:i,partConfig:s,currentQuestionIndex:r,answers:W.answers,flaggedQuestions:W.flaggedQuestions,onAnswerChange:Re,onToggleFlag:e=>W.toggleFlag(e),isReviewMode:!1,isAudioPlayed:W.playedListeningAudioParts.has(i.partId),onAudioFirstPlay:()=>W.markListeningAudioPlayed(i.partId)})}),W.isSubmitting&&e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50",children:t("div",{className:"rounded-xl bg-white p-8 text-center shadow-2xl",children:[e(u,{className:"mx-auto h-12 w-12 animate-spin text-green-500"}),e("p",{className:"mt-4 text-lg font-semibold text-gray-700",children:"Đang nộp bài..."}),e("p",{className:"mt-2 text-sm text-gray-500",children:"Vui lòng đợi trong giây lát"})]})})]})}return t(n,{children:[(()=>{const{Layout:n,QuestionCard:i}=P(ce),s={headerProps:{examName:je,examType:"EXAM",levelLabel:He,totalQuestions:Ke,answeredCount:Ve,remainingSeconds:W.remainingSeconds,isSubmitting:W.isSubmitting,isCameraActive:Z,onSubmit:Pe},onPrevious:Be,onNext:Qe,onClose:Fe,isNavDisabled:W.isNavDisabled,partName:Xe?.name||(Xe?.partNo?`PART ${Xe.partNo}`:void 0)};if("CAMBRIDGE_YLE"===ce){const e=Le(Je);s.isFirstQuestion=e?0===ie:0===Je,s.isLastQuestion=e?ie===We.length-1:Je===ae.length-1,s.sidebarProps={parts:ze,questions:We,currentQuestionGlobalIndex:ie,isNavDisabled:W.isNavDisabled,onPartClick:e=>{if(W.isNavDisabled)return;De.current=e>Je?"forward":"backward",se(e);const t=We.find(t=>t.partIndex===e);t&&re(t.globalIndex),Ge()},onQuestionClick:e=>{if(W.isNavDisabled)return;const t=We[e];t&&t.partIndex!==Je&&(De.current=t.partIndex>Je?"forward":"backward",se(t.partIndex)),re(e),t&&setTimeout(()=>{const e=document.getElementById(`question-${t.questionId}`);e&&e.scrollIntoView({behavior:"smooth",block:"center"})},100)}}}else s.isFirstQuestion=0===Je,s.isLastQuestion=Je===ae.length-1;const r="CAMBRIDGE_YLE"===ce&&("SPEAKING_DESCRIBE_IMAGE"===Xe?.questionType||"SPONTANEOUS_QA"===Xe?.questionType||"SPEAKING_CUE_CARD"===Xe?.questionType||"READ_DISPLAYED_CONTENT"===Xe?.questionType),a=We[ie],o=r&&a?.partIndex===Je?a.indexInPart:void 0;return t(n,{...s,children:[e(i,{partName:Xe?.name||(Xe?.partNo?`PART ${Xe.partNo}`:void 0),children:e(q,{part:Xe,answers:W.answers||{},onAnswerChange:Re,isReviewMode:!1,currentQuestionIndex:o,onExit:Fe,onPartComplete:Je<ae.length-1?Qe:Pe,onPartBack:Je>0?Be:void 0,speakingTheme:"CAMBRIDGE_YLE"===ce?"CAMBRIDGE_YLE":de,initialQuestionIndex:"backward"===De.current?Xe?.questions.length-1:0,skipTeacherVideo:W.playedIntroParts.has(Je)||fe!==H,onTeacherIntroPlayed:()=>we(Je)})}),W.isSubmitting&&e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50",children:t("div",{className:"rounded-xl bg-white p-8 text-center shadow-2xl",children:[e(u,{className:"mx-auto h-12 w-12 animate-spin text-green-500"}),e("p",{className:"mt-4 text-lg font-semibold text-gray-700",children:"Đang nộp bài..."}),e("p",{className:"mt-2 text-sm text-gray-500",children:"Vui lòng đợi trong giây lát"})]})})]})})(),"CAMBRIDGE_YLE"===ce?pe&&e(R,{onClose:()=>ue(!1),onSubmit:ke,isSubmitting:W.isSubmitting}):e(w,{open:pe,onOpenChange:ue,children:t(y,{children:[t(N,{children:[e(A,{children:"Nộp bài thi"}),e(_,{children:"Bạn có chắc chắn muốn nộp bài không? Sau khi nộp, bạn sẽ không thể thay đổi câu trả lời."})]}),t(v,{children:[e(S,{disabled:W.isSubmitting,children:"Quay lại"}),e(E,{onClick:ke,disabled:W.isSubmitting,children:W.isSubmitting?t(n,{children:[e(u,{className:"mr-2 h-4 w-4 animate-spin"}),"Đang ná»™p..."]}):"Nộp bài"})]})]})}),ge&&e(D,{onClose:()=>he(!1),onConfirmExit:async()=>{if(!Oe){Ye(!0);try{const e=W.attemptId;e&&K.cancelAttempt&&await K.cancelAttempt(e)}catch(e){console.error("Failed to cancel attempt:",e)}finally{Ye(!1),he(!1),f.persist.clearStorage(),X(V)}}},isCancelling:Oe})]})}export function ExamTakingPageContainer(t){return e(F,{api:t.api,children:e(K,{...t})})}
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
4
+ import Image from 'next/image';
5
+ import summerSkyClockImg from '../../../components/themes/summer-sky/assets/images/clock.png';
6
+ import submitImg from '../../../components/themes/summer-sky/assets/images/submit.png';
7
+ import hasAnsweredImg from '../../../components/themes/summer-sky/assets/images/has_answered.png';
8
+ import { toast } from 'sonner';
9
+ import { Loader2 } from 'lucide-react';
10
+ import { t } from '../../../shared/lib/i18n';
11
+ import { transformAnswersForSubmission } from './utils/answer-transformers';
12
+ import { getThemeForLevel, normalizeExamTheme } from '../../../shared/lib/themes';
13
+ import { useExamTakingStore } from '../../../shared/lib/stores/examTakingStore';
14
+ import { useGradingTransientStore } from '../../../shared/lib/stores/gradingTransientStore';
15
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '../../../components/ui/alert-dialog';
16
+ import { useExamTimer } from './hooks/useExamTimer';
17
+ import { useAutoSave } from './hooks/useAutoSave';
18
+ import { useProctoringCamera } from './hooks/useProctoringCamera';
19
+ import { getStandardExamTheme } from '../../../components/themes';
20
+ import { QuestionRenderer } from './components/QuestionRenderer';
21
+ import { FullscreenLoading } from '../entry/fullscreen-loading';
22
+ import { SummerSkySubmitModal } from '../../../components/themes/summer-sky/SummerSkySubmitModal';
23
+ import { SummerSkyExitModal } from '../../../components/themes/summer-sky/SummerSkyExitModal';
24
+ import { EnglishCertificationExamLayout, EnglishCertificationQuestionRenderer, scrollToEnglishCertQuestion, } from '../../../components/themes/english-certification';
25
+ import { groupQuestionsByPart, } from './utils/question-transformers';
26
+ import { findSidebarQuestionForPart, getCambridgeYlePartQuestionCount, getPartInitialQuestionIndex, isCambridgeYleInlineSpeakingType, listSpontaneousQaGroupChildIds, } from './utils/cambridge-yle-inline-speaking';
27
+ import { unwrap, unwrapAttempt } from './exam-taking.utils';
28
+ import { ExamTakingApiProvider } from './ExamTakingApiContext';
29
+ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (attemptId) => `/student/results/${attemptId}`, fallbackPath = '/student/contests/discover', proctoringEnabled = false, achieversExamTitle, onSubmitted, }) {
30
+ const store = useExamTakingStore();
31
+ const submittingRef = useRef(false);
32
+ const navigate = useCallback((href, replace = false) => {
33
+ if (onNavigate)
34
+ onNavigate(href, { replace });
35
+ else if (typeof window !== 'undefined') {
36
+ if (replace)
37
+ window.location.replace(href);
38
+ else
39
+ window.location.href = href;
40
+ }
41
+ }, [onNavigate]);
42
+ // Proctoring camera: starts capturing automatically once attemptId is available
43
+ const { isCameraActive } = useProctoringCamera({
44
+ api,
45
+ attemptId: store.attemptId,
46
+ intervalMs: 10000,
47
+ enabled: proctoringEnabled,
48
+ });
49
+ const [isLoading, setIsLoading] = useState(true);
50
+ // Navigation position lives in the persisted store so it survives a reload.
51
+ // Wrappers accept either a value or an updater to match the previous
52
+ // useState-style call sites (e.g. setCurrentPartIndex(prev => prev + 1)).
53
+ const currentPartIndex = store.currentPartIndex;
54
+ const currentQuestionGlobalIndex = store.currentQuestionGlobalIndex;
55
+ const setCurrentPartIndex = useCallback((updater) => {
56
+ const prev = useExamTakingStore.getState().currentPartIndex;
57
+ const next = typeof updater === 'function' ? updater(prev) : updater;
58
+ useExamTakingStore.getState().setCurrentPartIndex(next);
59
+ }, []);
60
+ const setCurrentQuestionGlobalIndex = useCallback((updater) => {
61
+ const prev = useExamTakingStore.getState().currentQuestionGlobalIndex;
62
+ const next = typeof updater === 'function' ? updater(prev) : updater;
63
+ useExamTakingStore.getState().setCurrentQuestionGlobalIndex(next);
64
+ }, []);
65
+ const [parts, setParts] = useState([]);
66
+ const [speakingTheme, setSpeakingTheme] = useState('SUMMER_SKY');
67
+ const [examTheme, setExamTheme] = useState('CAMBRIDGE_YLE');
68
+ const [showSubmitConfirm, setShowSubmitConfirm] = useState(false);
69
+ const [showExitConfirm, setShowExitConfirm] = useState(false);
70
+ const [room, setRoom] = useState(null);
71
+ const [examTemplateTitle, setExamTemplateTitle] = useState('');
72
+ // Track which parts have already played their AI teacher intro video.
73
+ // Stored in the persisted store so a reload doesn't replay them.
74
+ const handleTeacherIntroPlayed = useCallback((partIndex) => {
75
+ useExamTakingStore.getState().markIntroPlayed(partIndex);
76
+ }, []);
77
+ // Use the branded English Certification layout when the exam is a TOEIC exam
78
+ // (detected via template subTitle) OR when the template explicitly selects the
79
+ // ENGLISH_CERTIFICATION theme (shared across TOEIC/IELTS certification exams).
80
+ const isToeicExam = store.examType === 'TOEIC' || examTheme === 'ENGLISH_CERTIFICATION';
81
+ // TOEIC: derive currentPartIndex from store.currentIndex and parts metadata
82
+ // This ensures the nav panel and question renderer stay in sync
83
+ const toeicCurrentPartIndex = useMemo(() => {
84
+ if (!isToeicExam || store.parts.length === 0)
85
+ return currentPartIndex;
86
+ const idx = store.currentIndex;
87
+ // Find which part contains the current question index
88
+ for (let i = 0; i < store.parts.length; i++) {
89
+ const p = store.parts[i];
90
+ const start = p.startIndex ?? 0;
91
+ const end = p.endIndex ?? start;
92
+ if (idx >= start && idx <= end)
93
+ return i;
94
+ }
95
+ return 0;
96
+ }, [isToeicExam, store.currentIndex, store.parts, currentPartIndex]);
97
+ // Load exam data with attempt validation
98
+ useEffect(() => {
99
+ const loadExamData = async () => {
100
+ try {
101
+ setIsLoading(true);
102
+ // Query the IN_PROGRESS attempt for THIS room specifically so a reload
103
+ // resumes the same attempt instead of creating a duplicate.
104
+ let activeAttempt = unwrap(await api.getActiveAttempt(roomId));
105
+ // No IN_PROGRESS attempt for this room → start a new one. (start() is
106
+ // idempotent server-side and rejects with ALREADY_SUBMITTED when the
107
+ // student has already submitted and re-takes aren't allowed.)
108
+ if (!activeAttempt) {
109
+ activeAttempt = unwrapAttempt(unwrap(await api.startAttempt(roomId)));
110
+ }
111
+ // Fetch room data for title/level display
112
+ let fetchedRoom = null;
113
+ try {
114
+ fetchedRoom = api.getRoom ? unwrap(await api.getRoom(roomId)) : null;
115
+ if (fetchedRoom)
116
+ setRoom(fetchedRoom);
117
+ }
118
+ catch {
119
+ // Non-critical: fallback to defaults if room fetch fails
120
+ }
121
+ // Case 4: Valid IN_PROGRESS attempt - load questions
122
+ // Backend returns { examTemplate, questions } — destructure accordingly
123
+ const { examTemplate, questions: rawQuestions } = unwrap(await api.getQuestions(activeAttempt.id));
124
+ // Store examTemplate title for conditional logic (e.g. skip teacher AI video)
125
+ if (examTemplate?.title) {
126
+ setExamTemplateTitle(examTemplate.title);
127
+ }
128
+ // Extract template parts from the questions response (preferred source)
129
+ // Falls back to attempt.exam.template.parts for backward compatibility
130
+ const templateParts = examTemplate?.parts ??
131
+ activeAttempt.exam?.template?.parts;
132
+ // Group questions by part with template metadata
133
+ const groupedParts = groupQuestionsByPart(rawQuestions, templateParts);
134
+ // Detect exam type from template metadata
135
+ const detectedExamType = examTemplate?.subTitle ?? null;
136
+ // Detect speaking exam: all questions have skill type SPEAKING
137
+ // This covers LISTEN_AND_CHOOSE_* and LISTEN_AND_DRAG_* types in Starters Part 1
138
+ // which do NOT start with LISTEN_AND_SPEAK_ but are still part of speaking exams.
139
+ const isSpeakingExam = groupedParts.every((p) => p.questions.every((q) => q.question_skill_type === 'SPEAKING'));
140
+ const hasAnySpeakingParts = groupedParts.some((p) => p.category === 'SPEAKING' || p.questions.every((q) => q.question_skill_type === 'SPEAKING'));
141
+ setParts(groupedParts);
142
+ const resolvedExamTheme = examTemplate?.theme ||
143
+ activeAttempt?.exam?.template?.theme ||
144
+ activeAttempt?.exam?.theme ||
145
+ fetchedRoom?.exam?.template?.theme ||
146
+ fetchedRoom?.exam?.theme ||
147
+ '';
148
+ const normalizedExamTheme = normalizeExamTheme(resolvedExamTheme) ||
149
+ (detectedExamType === 'TOEIC' ? 'ENGLISH_CERTIFICATION' : 'CAMBRIDGE_YLE');
150
+ // Extract speakingTheme if this is a speaking exam
151
+ if (hasAnySpeakingParts) {
152
+ // If the resolved theme isn't a known speaking theme, fallback to SUMMER_SKY
153
+ const isKnownSpeakingTheme = ['SUMMER_SKY'].includes(resolvedExamTheme);
154
+ const theme = isKnownSpeakingTheme ? resolvedExamTheme : 'SUMMER_SKY';
155
+ setSpeakingTheme(theme);
156
+ }
157
+ // Calculate remaining time
158
+ const startedAt = new Date(activeAttempt.startedAt);
159
+ const durationMs = activeAttempt.durationMinutes * 60 * 1000;
160
+ const endTime = startedAt.getTime() + durationMs;
161
+ const now = Date.now();
162
+ const remainingMs = Math.max(0, endTime - now);
163
+ // If time already elapsed before the exam could load, auto-submit the
164
+ // attempt (the server no longer auto-submits on expiry, so we must never
165
+ // leave it IN_PROGRESS/ungraded) and send the student to their results
166
+ // instead of dropping them on the discover page.
167
+ if (remainingMs === 0) {
168
+ toast.info(t('student.exam.take.timeExpired'));
169
+ try {
170
+ await api.submitAttempt(activeAttempt.id);
171
+ }
172
+ catch {
173
+ // ALREADY_SUBMITTED / finalize race is fine — the attempt is
174
+ // finalized either way; still route to the results page.
175
+ }
176
+ useExamTakingStore.persist.clearStorage();
177
+ navigate(resultPath(activeAttempt.id));
178
+ return;
179
+ }
180
+ store.hydrateFromServer({
181
+ attemptId: activeAttempt.id,
182
+ examRoomId: roomId,
183
+ questions: rawQuestions.map((q) => ({
184
+ id: q.id,
185
+ question_type: q.question_type,
186
+ content: q.content,
187
+ is_example: q.is_example,
188
+ total_points: q.total_points,
189
+ part_id: q.part_id,
190
+ part_no: q.part_no,
191
+ question_number: q.question_number,
192
+ })),
193
+ durationMinutes: activeAttempt.durationMinutes,
194
+ startedAt: activeAttempt.startedAt,
195
+ examType: detectedExamType ?? undefined,
196
+ examTitle: examTemplate?.title ?? undefined,
197
+ parts: templateParts,
198
+ });
199
+ if (detectedExamType) {
200
+ store.setExamType(detectedExamType);
201
+ }
202
+ // Store normalized exam theme for layout selection with fallback from room/attempt metadata
203
+ setExamTheme(normalizedExamTheme);
204
+ if (templateParts) {
205
+ store.setParts(templateParts);
206
+ }
207
+ setIsLoading(false);
208
+ }
209
+ catch (error) {
210
+ const errorCode = error?.response?.data?.errorCode;
211
+ if (errorCode === 'ATTEMPT_ALREADY_SUBMITTED') {
212
+ toast.info(t('student.exam.take.submitted'));
213
+ navigate(fallbackPath, true);
214
+ return;
215
+ }
216
+ console.error('Failed to load exam data:', error);
217
+ toast.error(t('student.exam.take.loadError'));
218
+ navigate(fallbackPath, true);
219
+ }
220
+ };
221
+ loadExamData();
222
+ // eslint-disable-next-line react-hooks/exhaustive-deps
223
+ }, [roomId]);
224
+ // Detect speaking exam from loaded parts: all questions must have skill type SPEAKING.
225
+ // This covers Starters Part 1 types (LISTEN_AND_CHOOSE_*, LISTEN_AND_DRAG_*)
226
+ // which are speaking exam parts but don't start with LISTEN_AND_SPEAK_.
227
+ const isSpeakingExam = useMemo(() => parts.length > 0 &&
228
+ parts.every((p) => p.questions.every((q) => q.question_skill_type === 'SPEAKING')), [parts]);
229
+ // Check if current part is a speaking part based on category or question skill type
230
+ // On CAMBRIDGE_YLE these speaking types are routed through CambridgeYleExamLayout instead of the
231
+ // full-page speaking flow, so we exclude them here. On other themes (e.g. SUMMER_SKY) they go
232
+ // through the normal speaking flow.
233
+ const isCurrentPartSpeaking = useMemo(() => {
234
+ const part = parts[currentPartIndex];
235
+ if (!part)
236
+ return false;
237
+ const currentTheme = examTheme || 'CAMBRIDGE_YLE';
238
+ if (isCambridgeYleInlineSpeakingType(part.questionType) && currentTheme === 'CAMBRIDGE_YLE')
239
+ return false;
240
+ if (part.category === 'SPEAKING')
241
+ return true;
242
+ return part.questions.every((q) => q.question_skill_type === 'SPEAKING');
243
+ }, [parts, currentPartIndex, examTheme]);
244
+ // Auto-submit handler
245
+ const handleAutoSubmit = async () => {
246
+ toast.info(t('student.exam.take.autoSubmit'));
247
+ await handleSubmit();
248
+ };
249
+ // Timer hook — always called (React rules).
250
+ // For TOEIC, we use multiple warning thresholds (15min + 5min).
251
+ // For Speaking, we use 5min + 2min warnings then auto-submit.
252
+ const { remainingSeconds, warningLevel } = useExamTimer({
253
+ onTimeExpired: handleAutoSubmit,
254
+ onWarningThreshold: isSpeakingExam || isToeicExam
255
+ ? undefined
256
+ : () => {
257
+ toast.warning(t('student.exam.take.timeWarning'));
258
+ },
259
+ warningThresholdSeconds: isSpeakingExam || isToeicExam ? undefined : 300,
260
+ warningThresholds: isToeicExam
261
+ ? [
262
+ {
263
+ seconds: 900,
264
+ level: 'warning',
265
+ callback: () => toast.warning('Còn 15 phút!', { duration: 3000 }),
266
+ },
267
+ {
268
+ seconds: 300,
269
+ level: 'critical',
270
+ callback: () => toast.warning('Còn 5 phút! Hãy nộp bài ngay!', { duration: 5000 }),
271
+ },
272
+ ]
273
+ : isSpeakingExam
274
+ ? [
275
+ {
276
+ seconds: 300,
277
+ level: 'warning',
278
+ callback: () => toast.warning('Còn 5 phút! Hãy hoàn thành bài thi.', { duration: 5000 }),
279
+ },
280
+ {
281
+ seconds: 120,
282
+ level: 'critical',
283
+ callback: () => toast.warning('Còn 2 phút! Bài thi sẽ được tự động nộp.', { duration: 5000 }),
284
+ },
285
+ ]
286
+ : undefined,
287
+ });
288
+ // Auto-save hook — 15s interval for TOEIC, 30s for others
289
+ const autoSave = useAutoSave({
290
+ api,
291
+ attemptId: store.attemptId || '',
292
+ parts,
293
+ intervalMs: isToeicExam ? 15000 : 30000,
294
+ // No success toast: saves run on every answer change / navigation, so a
295
+ // "Đã lưu" toast each time would disrupt the student. Errors still notify.
296
+ // No error toast: saves run continuously while the student is working, and a
297
+ // red toast mid-exam is disruptive. localStorage + the keepalive flush are the
298
+ // safety net, so a transient save failure is logged silently and retried.
299
+ onSaveError: (error) => {
300
+ console.error('Save error:', error);
301
+ },
302
+ });
303
+ // Flush pending answers to the server when the tab is hidden/closed or the
304
+ // network reconnects, to shrink the up-to-30s auto-save gap. localStorage is
305
+ // the primary safety net; this just minimizes server-side data loss.
306
+ // Refs keep the listeners bound once while always reading the latest values.
307
+ const saveNowRef = useRef(autoSave.saveNow);
308
+ const partsRef = useRef(parts);
309
+ useEffect(() => {
310
+ saveNowRef.current = autoSave.saveNow;
311
+ partsRef.current = parts;
312
+ });
313
+ useEffect(() => {
314
+ // Best-effort delivery that survives page unload (keepalive). Uses the same
315
+ // cookie-authenticated JSON endpoint as the regular auto-save.
316
+ const flushKeepalive = () => {
317
+ const s = useExamTakingStore.getState();
318
+ if (!s.attemptId || !s.isDirty || s.isSubmitting)
319
+ return;
320
+ const answersArray = transformAnswersForSubmission(s.answers, partsRef.current);
321
+ if (answersArray.length === 0)
322
+ return;
323
+ try {
324
+ // Best-effort delivery that survives page unload (keepalive) via the
325
+ // injected adapter's saveAnswers.
326
+ void api.saveAnswers?.({ attemptId: s.attemptId, answers: answersArray, keepalive: true });
327
+ }
328
+ catch {
329
+ /* ignore */
330
+ }
331
+ };
332
+ const handleVisibility = () => {
333
+ if (document.visibilityState === 'hidden')
334
+ flushKeepalive();
335
+ };
336
+ const handleOnline = () => {
337
+ const s = useExamTakingStore.getState();
338
+ if (s.isDirty && !s.isSubmitting)
339
+ saveNowRef.current();
340
+ };
341
+ document.addEventListener('visibilitychange', handleVisibility);
342
+ window.addEventListener('pagehide', flushKeepalive);
343
+ window.addEventListener('online', handleOnline);
344
+ return () => {
345
+ document.removeEventListener('visibilitychange', handleVisibility);
346
+ window.removeEventListener('pagehide', flushKeepalive);
347
+ window.removeEventListener('online', handleOnline);
348
+ };
349
+ }, []);
350
+ // Persist answers on every question navigation (Next / Previous / sidebar jump)
351
+ // so a device crash or network drop doesn't lose answers the student already
352
+ // entered. Speaking exams upload audio per question and are graded per answer,
353
+ // so they're excluded here. Grading itself still runs only on submit.
354
+ //
355
+ // Deps are the navigation position ONLY (not autoSave.saveNow) so this effect
356
+ // fires on navigation rather than on every render — the per-second timer
357
+ // re-render would otherwise reset an interval-based trigger. saveNowRef always
358
+ // holds the latest saveNow, which itself no-ops when there are no dirty answers
359
+ // or a save is already in flight.
360
+ useEffect(() => {
361
+ if (isSpeakingExam)
362
+ return;
363
+ void saveNowRef.current();
364
+ }, [store.currentIndex, currentPartIndex, currentQuestionGlobalIndex, isSpeakingExam]);
365
+ // Submit handler
366
+ const handleSubmit = async () => {
367
+ // Guard: use ref for synchronous mutex (store snapshot can be stale)
368
+ if (submittingRef.current)
369
+ return;
370
+ submittingRef.current = true;
371
+ store.setIsSubmitting(true);
372
+ setShowSubmitConfirm(false);
373
+ try {
374
+ // Save any pending changes first
375
+ if (store.isDirty) {
376
+ await autoSave.saveNow();
377
+ }
378
+ const submitResult = unwrap(await api.submitAttempt(store.attemptId));
379
+ const pendingAnswerIds = submitResult?.pendingAnswerIds ?? [];
380
+ // Stash pending IDs into transient store for the result page poller to pick up
381
+ if (store.attemptId) {
382
+ useGradingTransientStore.getState().setPending(store.attemptId, pendingAnswerIds);
383
+ onSubmitted?.({ attemptId: store.attemptId, pendingAnswerIds });
384
+ }
385
+ if (pendingAnswerIds.length === 0) {
386
+ toast.success(t('student.exam.take.submitSuccess'));
387
+ }
388
+ else {
389
+ toast.success(t('student.exam.take.submittedAIGradingInProgress'));
390
+ }
391
+ // Drop persisted progress so a reload after submit doesn't restore the
392
+ // already-submitted attempt. Clear storage only (keep in-memory state) to
393
+ // avoid flashing an empty exam before navigation completes.
394
+ useExamTakingStore.persist.clearStorage();
395
+ navigate(resultPath(store.attemptId), true);
396
+ }
397
+ catch (error) {
398
+ console.error('Failed to submit exam:', error);
399
+ // ALREADY_SUBMITTED is not a real error: the attempt is already finalized
400
+ // server-side (e.g. submitted in another tab, timer auto-submit, or a first
401
+ // submit that succeeded but the client saw a network blip). Treat it as done
402
+ // — drop persisted progress and send the student to their result page. No
403
+ // red toast to disrupt the exam experience.
404
+ const errorCode = error?.response?.data?.errorCode;
405
+ if (errorCode === 'ATTEMPT_ALREADY_SUBMITTED') {
406
+ useExamTakingStore.persist.clearStorage();
407
+ navigate(resultPath(store.attemptId), true);
408
+ return;
409
+ }
410
+ // Genuine failure (network / 500): keep the student on the page with their
411
+ // answers (auto-save + localStorage preserved them) and let them retry.
412
+ // Use a non-red warning toast so it informs without alarming mid-exam.
413
+ toast.warning(t('student.exam.take.submitError'));
414
+ submittingRef.current = false;
415
+ store.setIsSubmitting(false);
416
+ }
417
+ };
418
+ // Request submit handler — shows confirm dialog for speaking exams
419
+ const handleRequestSubmit = useCallback(() => {
420
+ setShowSubmitConfirm(true);
421
+ }, []);
422
+ const subItemToQuestionMapRef = useRef(new Map());
423
+ const sidebarQuestionsRef = useRef([]);
424
+ // Answer change handler - stores answer by questionId
425
+ // Memoized to prevent child re-renders from timer updates
426
+ const handleAnswerChange = useCallback((questionId, value) => {
427
+ store.setAnswer(questionId, value);
428
+ const actualQuestionId = subItemToQuestionMapRef.current.get(questionId) || questionId;
429
+ const qIndex = sidebarQuestionsRef.current.findIndex((q) => q.questionId === actualQuestionId);
430
+ if (qIndex !== -1) {
431
+ setCurrentQuestionGlobalIndex((prev) => prev !== qIndex ? qIndex : prev);
432
+ }
433
+ // eslint-disable-next-line react-hooks/exhaustive-deps
434
+ }, []);
435
+ // Track navigation direction to initialize the first question when moving backward
436
+ const navDirectionRef = useRef('forward');
437
+ // Navigation handlers
438
+ // Helper: check if a part renders one question per screen (speaking parts in CambridgeYLE)
439
+ const isOneQuestionPerScreen = (partIdx) => {
440
+ const currentTheme = examTheme || 'CAMBRIDGE_YLE';
441
+ if (currentTheme !== 'CAMBRIDGE_YLE')
442
+ return false;
443
+ const part = parts[partIdx];
444
+ return isCambridgeYleInlineSpeakingType(part?.questionType);
445
+ };
446
+ const scrollToTop = () => {
447
+ const doScroll = () => {
448
+ window.scrollTo({ top: 0, behavior: 'smooth' });
449
+ const mainScroll = document.getElementById('exam-main-scroll-area');
450
+ if (mainScroll) {
451
+ mainScroll.scrollTo({ top: 0, behavior: 'smooth' });
452
+ // The actual scrollable element may be a nested child (e.g. overflow-y-auto)
453
+ // when the outer wrapper itself doesn't overflow. We might have multiple (e.g. split layout)
454
+ const nestedScrolls = mainScroll.querySelectorAll('.overflow-y-auto');
455
+ nestedScrolls.forEach(el => {
456
+ el.scrollTo({ top: 0, behavior: 'smooth' });
457
+ });
458
+ }
459
+ };
460
+ // Execute immediately, then after short delays to ensure DOM has updated
461
+ requestAnimationFrame(doScroll);
462
+ setTimeout(doScroll, 100);
463
+ setTimeout(doScroll, 250);
464
+ };
465
+ const handleNext = () => {
466
+ if (examTheme === 'ENGLISH_CERTIFICATION') {
467
+ // English Certification shows a whole part/section per screen — advance to
468
+ // the first question of the next part/section.
469
+ const nextPart = store.parts[toeicCurrentPartIndex + 1];
470
+ if (nextPart)
471
+ store.goToQuestion(nextPart.startIndex ?? 0);
472
+ }
473
+ else if (isToeicExam) {
474
+ store.goToQuestion(Math.min(store.currentIndex + 1, store.questions.length - 1));
475
+ }
476
+ else if (examTheme === 'CAMBRIDGE_YLE' && isOneQuestionPerScreen(currentPartIndex)) {
477
+ // Speaking: navigate question-by-question
478
+ const nextGlobalIdx = currentQuestionGlobalIndex + 1;
479
+ const nextQ = sidebarQuestions[nextGlobalIdx];
480
+ if (nextQ) {
481
+ if (nextQ.partIndex !== currentPartIndex) {
482
+ // Crossed into next part
483
+ navDirectionRef.current = 'forward';
484
+ setCurrentPartIndex(nextQ.partIndex);
485
+ }
486
+ setCurrentQuestionGlobalIndex(nextGlobalIdx);
487
+ }
488
+ else {
489
+ // Last question in the entire exam — treat as submit
490
+ // (handled by isLastQuestion prop on the layout)
491
+ }
492
+ }
493
+ else {
494
+ navDirectionRef.current = 'forward';
495
+ setCurrentPartIndex((prev) => {
496
+ const nextPartIndex = Math.min(prev + 1, parts.length - 1);
497
+ if (examTheme === 'CAMBRIDGE_YLE') {
498
+ const firstQ = findSidebarQuestionForPart(sidebarQuestions, nextPartIndex, 'forward');
499
+ if (firstQ)
500
+ setCurrentQuestionGlobalIndex(firstQ.globalIndex);
501
+ }
502
+ return nextPartIndex;
503
+ });
504
+ }
505
+ scrollToTop();
506
+ };
507
+ const handlePrevious = () => {
508
+ if (examTheme === 'ENGLISH_CERTIFICATION') {
509
+ // Move to the first question of the previous part/section.
510
+ const prevPart = store.parts[toeicCurrentPartIndex - 1];
511
+ if (prevPart)
512
+ store.goToQuestion(prevPart.startIndex ?? 0);
513
+ }
514
+ else if (isToeicExam) {
515
+ store.goToQuestion(Math.max(store.currentIndex - 1, 0));
516
+ }
517
+ else if (examTheme === 'CAMBRIDGE_YLE' && isOneQuestionPerScreen(currentPartIndex)) {
518
+ // Speaking: navigate question-by-question
519
+ const prevGlobalIdx = currentQuestionGlobalIndex - 1;
520
+ const prevQ = sidebarQuestions[prevGlobalIdx];
521
+ if (prevQ) {
522
+ if (prevQ.partIndex !== currentPartIndex) {
523
+ // Crossed into previous part
524
+ navDirectionRef.current = 'backward';
525
+ setCurrentPartIndex(prevQ.partIndex);
526
+ }
527
+ setCurrentQuestionGlobalIndex(prevGlobalIdx);
528
+ }
529
+ }
530
+ else {
531
+ navDirectionRef.current = 'backward';
532
+ setCurrentPartIndex((prev) => {
533
+ const prevPartIndex = Math.max(prev - 1, 0);
534
+ if (examTheme === 'CAMBRIDGE_YLE') {
535
+ const lastQ = findSidebarQuestionForPart(sidebarQuestions, prevPartIndex, 'backward');
536
+ if (lastQ)
537
+ setCurrentQuestionGlobalIndex(lastQ.globalIndex);
538
+ }
539
+ return prevPartIndex;
540
+ });
541
+ }
542
+ scrollToTop();
543
+ };
544
+ const [isCancelling, setIsCancelling] = useState(false);
545
+ // Close handler - show exit confirm modal before going back
546
+ const handleClose = () => {
547
+ setShowExitConfirm(true);
548
+ };
549
+ const handleConfirmExit = async () => {
550
+ if (isCancelling)
551
+ return;
552
+ setIsCancelling(true);
553
+ try {
554
+ const attemptId = store.attemptId;
555
+ if (attemptId && api.cancelAttempt) {
556
+ await api.cancelAttempt(attemptId);
557
+ }
558
+ }
559
+ catch (error) {
560
+ console.error('Failed to cancel attempt:', error);
561
+ }
562
+ finally {
563
+ setIsCancelling(false);
564
+ setShowExitConfirm(false);
565
+ useExamTakingStore.persist.clearStorage();
566
+ navigate(fallbackPath);
567
+ }
568
+ };
569
+ // Calculate totals (exclude example questions unless CAMBRIDGE_YLE)
570
+ const totalQuestions = useMemo(() => {
571
+ return parts.reduce((sum, part) => sum +
572
+ (examTheme === 'CAMBRIDGE_YLE'
573
+ ? getCambridgeYlePartQuestionCount(part)
574
+ : part.questions.filter((q) => !q.is_example).length), 0);
575
+ }, [parts, examTheme]);
576
+ // Build mapping: subItemKey -> questionId for complex question types
577
+ // This maps sub-item keys (grid cells, label IDs, region IDs) back to their parent question
578
+ const subItemToQuestionMap = useMemo(() => {
579
+ const map = new Map();
580
+ parts.forEach((part) => {
581
+ part.questions.forEach((question) => {
582
+ if (question.is_example)
583
+ return;
584
+ const content = question.content;
585
+ // FILL_MISSING_WORDS_IN_GRID: keys are "row-col" like "4-5"
586
+ if (part.questionType === 'FILL_MISSING_WORDS_IN_GRID' && content?.grid) {
587
+ content.grid.forEach((row, rowIdx) => {
588
+ row.forEach((_, colIdx) => {
589
+ map.set(`${rowIdx}-${colIdx}`, question.id);
590
+ });
591
+ });
592
+ }
593
+ // LABEL_THE_PICTURE: keys are label IDs
594
+ if (part.questionType === 'LABEL_THE_PICTURE' && content?.labels) {
595
+ content.labels.forEach((label) => {
596
+ if (label.id)
597
+ map.set(label.id, question.id);
598
+ });
599
+ }
600
+ // READ_AND_COLOR_OBJECTS: keys are region IDs
601
+ if (part.questionType === 'READ_AND_COLOR_OBJECTS') {
602
+ const regions = content?.regions || content?.regionNodes;
603
+ regions?.forEach((region) => {
604
+ if (region.id)
605
+ map.set(region.id, question.id);
606
+ });
607
+ }
608
+ // IMAGE_OBJECT_MATCHING: same structure as READ_AND_COLOR_OBJECTS — keys are region IDs
609
+ if (part.questionType === 'IMAGE_OBJECT_MATCHING') {
610
+ const regions = content?.regions || content?.regionNodes;
611
+ regions?.forEach((region) => {
612
+ if (region.id)
613
+ map.set(region.id, question.id);
614
+ });
615
+ }
616
+ // LOOK_PICTURE_FILL_WORD_HINT: single question with {0}, {1} placeholders → synthetic IDs
617
+ if (part.questionType === 'LOOK_PICTURE_FILL_WORD_HINT') {
618
+ const word = content?.word;
619
+ if (word && typeof word === 'string') {
620
+ const placeholderRegex = /\{(\d+)\}/g;
621
+ let match;
622
+ while ((match = placeholderRegex.exec(word)) !== null) {
623
+ const blankIndex = match[1];
624
+ map.set(`${question.id}-blank-${blankIndex}`, question.id);
625
+ }
626
+ }
627
+ }
628
+ if (part.questionType === 'CROSS_OUT_WORD_GROUP' &&
629
+ Array.isArray(content?.items)) {
630
+ content.items.forEach((_, index) => {
631
+ map.set(`${question.id}-item-${index}`, question.id);
632
+ });
633
+ }
634
+ });
635
+ });
636
+ return map;
637
+ }, [parts]);
638
+ useEffect(() => {
639
+ subItemToQuestionMapRef.current = subItemToQuestionMap;
640
+ }, [subItemToQuestionMap]);
641
+ const answeredQuestionsSet = useMemo(() => {
642
+ if (!store.answers)
643
+ return new Set();
644
+ // Get IDs of non-example questions only
645
+ const countableQuestionIds = new Set();
646
+ parts.forEach((part) => {
647
+ if (part.questionType === 'SPONTANEOUS_QA_GROUP') {
648
+ const parent = part.questions[0];
649
+ if (parent && !parent.is_example) {
650
+ listSpontaneousQaGroupChildIds(parent.id, parent.content).forEach((id) => {
651
+ countableQuestionIds.add(id);
652
+ });
653
+ }
654
+ return;
655
+ }
656
+ part.questions
657
+ .filter((q) => !q.is_example)
658
+ .forEach((q) => countableQuestionIds.add(q.id));
659
+ });
660
+ // Track which questions have been answered
661
+ const answeredQuestions = new Set();
662
+ Object.keys(store.answers).forEach((key) => {
663
+ const answer = store.answers[key];
664
+ if (answer === undefined || answer === '')
665
+ return;
666
+ // Direct question ID match
667
+ if (countableQuestionIds.has(key)) {
668
+ answeredQuestions.add(key);
669
+ return;
670
+ }
671
+ // Check if it's a sub-item key that maps to a question
672
+ const questionId = subItemToQuestionMap.get(key);
673
+ if (questionId && countableQuestionIds.has(questionId)) {
674
+ answeredQuestions.add(questionId);
675
+ }
676
+ });
677
+ return answeredQuestions;
678
+ }, [store.answers, parts, subItemToQuestionMap]);
679
+ const answeredCount = answeredQuestionsSet.size + (examTheme === 'CAMBRIDGE_YLE'
680
+ ? parts.reduce((sum, part) => sum + part.questions.filter(q => q.is_example).length, 0)
681
+ : 0);
682
+ // Compute exam title: always prioritize the real exam name from room details API
683
+ const examTitle = useMemo(() => {
684
+ return room?.exam?.name || room?.name || (isToeicExam ? 'TOEIC Listening and Reading Test' : 'Bài thi');
685
+ }, [room, isToeicExam]);
686
+ // Compute level label from the exam level
687
+ const levelLabel = useMemo(() => {
688
+ return getThemeForLevel(room?.exam?.level).displayName;
689
+ }, [room?.exam?.level]);
690
+ const { sidebarParts, sidebarQuestions } = useMemo(() => {
691
+ const sParts = [];
692
+ const sQuestions = [];
693
+ let globalIndex = 0;
694
+ parts.forEach((part, partIdx) => {
695
+ const parent = part.questions[0];
696
+ const shouldFlattenGroup = examTheme === 'CAMBRIDGE_YLE' &&
697
+ part.questionType === 'SPONTANEOUS_QA_GROUP' &&
698
+ !!parent;
699
+ const questionsToList = shouldFlattenGroup
700
+ ? listSpontaneousQaGroupChildIds(parent.id, parent.content).map((childId, childIndex) => ({
701
+ id: childId,
702
+ is_example: false,
703
+ question_number: parent.question_number,
704
+ sub_question_number: childIndex + 1,
705
+ indexInPart: childIndex,
706
+ }))
707
+ : (examTheme === 'CAMBRIDGE_YLE'
708
+ ? part.questions
709
+ : part.questions.filter((q) => !q.is_example)).map((q) => ({
710
+ id: q.id,
711
+ is_example: q.is_example,
712
+ question_number: q.question_number,
713
+ sub_question_number: undefined,
714
+ indexInPart: part.questions.findIndex((partQ) => partQ.id === q.id),
715
+ }));
716
+ let partAnsweredCount = 0;
717
+ questionsToList.forEach((q) => {
718
+ const isAnswered = examTheme === 'CAMBRIDGE_YLE' && q.is_example
719
+ ? true
720
+ : answeredQuestionsSet.has(q.id);
721
+ if (isAnswered)
722
+ partAnsweredCount++;
723
+ sQuestions.push({
724
+ globalIndex,
725
+ partIndex: partIdx,
726
+ indexInPart: q.indexInPart,
727
+ questionNumber: q.question_number,
728
+ subQuestionNumber: q.sub_question_number,
729
+ isAnswered,
730
+ questionId: q.id,
731
+ isExample: q.is_example,
732
+ });
733
+ globalIndex++;
734
+ });
735
+ sParts.push({
736
+ partIndex: partIdx,
737
+ partNo: part.partNo,
738
+ name: part.name,
739
+ category: part.category,
740
+ points: part.points,
741
+ totalQuestions: examTheme === 'CAMBRIDGE_YLE'
742
+ ? getCambridgeYlePartQuestionCount(part)
743
+ : part.questions.filter((q) => !q.is_example).length,
744
+ answeredCount: partAnsweredCount,
745
+ });
746
+ });
747
+ return { sidebarParts: sParts, sidebarQuestions: sQuestions };
748
+ }, [parts, answeredQuestionsSet, examTheme]);
749
+ useEffect(() => {
750
+ sidebarQuestionsRef.current = sidebarQuestions;
751
+ }, [sidebarQuestions]);
752
+ if (isLoading) {
753
+ return _jsx(FullscreenLoading, {});
754
+ }
755
+ // Determine actual part index based on exam theme
756
+ const activePartIndex = currentPartIndex;
757
+ const currentPart = parts[activePartIndex];
758
+ if (!currentPart) {
759
+ return (_jsx("div", { className: "flex min-h-screen items-center justify-center", children: _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-lg font-semibold text-red-500", children: "Kh\u00C3\u00B4ng t\u00C3\u00ACm th\u00E1\u00BA\u00A5y c\u00C3\u00A2u h\u00E1\u00BB\u008Fi" }), _jsx("button", { type: "button", onClick: () => navigate(fallbackPath), className: "mt-4 rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600", children: "Quay l\u1EA1i" })] }) }));
760
+ }
761
+ // -------------------------------------------------------
762
+ // Speaking exam: renderers manage their own full-page layout.
763
+ // QuestionRenderer delegates to SpeakingQuestionRenderer →
764
+ // SpeakingExamLayout (full-screen, wooden-themed UI).
765
+ // We skip MoversExamLayout and MoversQuestionCard entirely.
766
+ // -------------------------------------------------------
767
+ if (isCurrentPartSpeaking) {
768
+ return (_jsxs(_Fragment, { children: [_jsx(QuestionRenderer, { part: currentPart, answers: store.answers || {}, onAnswerChange: handleAnswerChange, isReviewMode: false, onExit: handleClose, onPartComplete: activePartIndex < parts.length - 1 ? handleNext : handleRequestSubmit, onPartBack: activePartIndex > 0 ? handlePrevious : undefined, speakingTheme: speakingTheme, initialQuestionIndex: getPartInitialQuestionIndex(currentPart, navDirectionRef.current), skipTeacherVideo: store.playedIntroParts.has(activePartIndex) || examTemplateTitle !== achieversExamTitle, onTeacherIntroPlayed: () => handleTeacherIntroPlayed(activePartIndex) }, `${currentPart.partId}-${activePartIndex}`), speakingTheme === 'SUMMER_SKY' ? (showSubmitConfirm && (_jsx(SummerSkySubmitModal, { onClose: () => setShowSubmitConfirm(false), onSubmit: handleSubmit, isSubmitting: store.isSubmitting }))) : (_jsx(AlertDialog, { open: showSubmitConfirm, onOpenChange: setShowSubmitConfirm, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "N\u1ED9p b\u00E0i thi" }), _jsx(AlertDialogDescription, { children: "B\u1EA1n c\u00F3 ch\u1EAFc ch\u1EAFn mu\u1ED1n n\u1ED9p b\u00E0i kh\u00F4ng? Sau khi n\u1ED9p, b\u1EA1n s\u1EBD kh\u00F4ng th\u1EC3 thay \u0111\u1ED5i c\u00E2u tr\u1EA3 l\u1EDDi." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: store.isSubmitting, children: "Quay l\u1EA1i" }), _jsx(AlertDialogAction, { onClick: handleSubmit, disabled: store.isSubmitting, children: store.isSubmitting ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "\u0110ang n\u1ED9p..."] })) : ('Nộp bài') })] })] }) })), _jsxs("div", { className: "fixed top-8 right-6 z-[100] flex flex-col items-end gap-4 sm:gap-6 lg:top-10 lg:right-16", children: [_jsxs("div", { className: "flex items-center gap-4 sm:gap-6", children: [remainingSeconds > 0 && (_jsxs("div", { className: `flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] backdrop-blur-[8px] transition-all duration-300 ease-in-out border-none shadow-[0_4px_20px_rgba(0,0,0,0.25)] ${warningLevel === 'critical' ? 'bg-[rgba(220,38,38,0.85)] shadow-[0_0_24px_rgba(220,38,38,0.5)] animate-pulse' :
769
+ warningLevel === 'warning' ? 'bg-[rgba(180,120,0,0.85)]' :
770
+ 'bg-[#001590]'}`, children: [_jsx(Image, { src: summerSkyClockImg, alt: "clock", width: 75, height: 75, className: "w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10" }), _jsxs("span", { className: "flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]", style: {
771
+ fontFamily: "'SVN-Volte Rounded', 'Quicksand', sans-serif",
772
+ textShadow: "0 2px 4px rgba(0, 0, 0, 0.2)"
773
+ }, children: [Math.floor(remainingSeconds / 60).toString().padStart(2, '0'), ":", (remainingSeconds % 60).toString().padStart(2, '0')] })] })), _jsxs("div", { className: "flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] bg-[#FF9900] shadow-[0_4px_20px_rgba(0,0,0,0.25)] border-none shrink-0", title: "S\u1ED1 c\u00E2u h\u1ECFi \u0111\u00E3 ho\u00E0n th\u00E0nh", children: [_jsx(Image, { src: hasAnsweredImg, alt: "Progress", width: 75, height: 75, className: "w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10" }), _jsxs("span", { className: "flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]", style: { fontFamily: '"SVN-Volte Rounded", "Quicksand", sans-serif', textShadow: '0 2px 4px rgba(0,0,0,0.2)' }, children: [answeredCount, "/", totalQuestions] })] })] }), _jsxs("button", { onClick: handleRequestSubmit, disabled: store.isSubmitting, className: `group cursor-pointer flex items-center h-[40px] sm:h-[50px] w-[130px] sm:w-[170px] rounded-[33.5px] shadow-[0_4px_20px_rgba(0,0,0,0.25)] transition-all duration-300 hover:scale-105 active:scale-95 disabled:pointer-events-none disabled:opacity-60 border-none ${store.isSubmitting ? 'bg-slate-400' : 'bg-[#16CA3B] hover:bg-[#15b736]'}`, children: [_jsx(Image, { src: submitImg, alt: "Submit", width: 75, height: 75, className: "w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none transition-transform duration-300 group-hover:scale-110 z-10" }), store.isSubmitting ? (_jsx("div", { className: "flex-1 flex justify-center pr-[10px] sm:pr-[16px]", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-white" }) })) : (_jsx("span", { className: "flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]", style: { fontFamily: '"SVN-Volte Rounded", "Quicksand", sans-serif', textShadow: '0 2px 4px rgba(0,0,0,0.2)' }, children: "N\u1ED9p b\u00E0i" }))] })] }), store.isSubmitting && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm", children: _jsxs("div", { className: "rounded-xl bg-white p-8 text-center shadow-2xl", children: [_jsx(Loader2, { className: "mx-auto h-12 w-12 animate-spin text-green-500" }), _jsx("p", { className: "mt-4 text-lg font-semibold text-gray-700", children: "\u0110ang n\u1ED9p b\u00E0i..." }), _jsx("p", { className: "mt-2 text-sm text-gray-500", children: "Vui l\u00F2ng \u0111\u1EE3i trong gi\u00E2y l\u00E1t" })] }) }))] }));
774
+ }
775
+ // -------------------------------------------------------
776
+ // TOEIC exam: use ToeicExamLayout with ToeicQuestionRenderer
777
+ // -------------------------------------------------------
778
+ if (isToeicExam) {
779
+ const toeicCurrentPart = parts[toeicCurrentPartIndex];
780
+ const toeicCurrentPartConfig = store.parts[toeicCurrentPartIndex];
781
+ // Index within current part (direct computation — no useMemo needed)
782
+ const toeicQuestionIndexInPart = toeicCurrentPartConfig
783
+ ? Math.max(0, store.currentIndex - (toeicCurrentPartConfig.startIndex ?? 0))
784
+ : 0;
785
+ const toeicPartStart = (toeicCurrentPartConfig?.startIndex ?? 0) + 1;
786
+ const toeicPartEnd = (toeicCurrentPartConfig?.endIndex ?? toeicPartStart - 1) + 1;
787
+ const toeicHeaderProps = {
788
+ examName: examTitle,
789
+ remainingSeconds: store.remainingSeconds,
790
+ totalQuestions: totalQuestions,
791
+ answeredCount,
792
+ isSubmitting: store.isSubmitting,
793
+ onSubmit: handleSubmit,
794
+ onClose: handleClose,
795
+ lastSavedAt: store.lastSavedAt,
796
+ isSaving: autoSave.isSaving,
797
+ partLabel: (toeicCurrentPartConfig?.name || toeicCurrentPart?.name || '').toUpperCase() || undefined,
798
+ partSubLabel: toeicCurrentPartConfig?.category || undefined,
799
+ questionRange: toeicPartStart === toeicPartEnd ? `${toeicPartStart}` : `${toeicPartStart}–${toeicPartEnd}`,
800
+ };
801
+ const toeicCurrentQuestion = toeicCurrentPart?.questions?.[toeicQuestionIndexInPart];
802
+ // English Certification shows a whole part/section per screen: footer keeps
803
+ // only Previous/Next (section navigation); marking happens per question card.
804
+ const isEnglishCert = examTheme === 'ENGLISH_CERTIFICATION';
805
+ const toeicFooterProps = {
806
+ onPrevious: handlePrevious,
807
+ onClear: isEnglishCert
808
+ ? undefined
809
+ : () => {
810
+ if (toeicCurrentQuestion)
811
+ handleAnswerChange(toeicCurrentQuestion.id, { optionId: null });
812
+ },
813
+ onToggleMark: isEnglishCert
814
+ ? undefined
815
+ : () => {
816
+ if (toeicCurrentQuestion)
817
+ store.toggleFlag(toeicCurrentQuestion.id);
818
+ },
819
+ onNext: handleNext,
820
+ isMarked: toeicCurrentQuestion
821
+ ? store.flaggedQuestions.has(toeicCurrentQuestion.id)
822
+ : false,
823
+ showSecondaryActions: !isEnglishCert,
824
+ isPreviousDisabled: isEnglishCert
825
+ ? toeicCurrentPartIndex <= 0
826
+ : store.currentIndex <= 0,
827
+ isNextDisabled: isEnglishCert
828
+ ? toeicCurrentPartIndex >= store.parts.length - 1
829
+ : store.currentIndex >= store.questions.length - 1,
830
+ };
831
+ const toeicNavProps = {
832
+ parts: store.parts,
833
+ questions: store.questions,
834
+ answers: store.answers,
835
+ flaggedQuestions: store.flaggedQuestions,
836
+ currentIndex: store.currentIndex,
837
+ onQuestionClick: (globalIdx) => {
838
+ store.goToQuestion(globalIdx);
839
+ // Scroll the chosen question card into view once the new part renders.
840
+ // Retries handle cross-part mounts; nested-scroll aware helper handles
841
+ // Reading split-view columns and sticky Listening audio clearance.
842
+ const targetQuestion = store.questions[globalIdx];
843
+ if (targetQuestion) {
844
+ scrollToEnglishCertQuestion(targetQuestion.id, {
845
+ behavior: 'smooth',
846
+ block: 'start',
847
+ });
848
+ }
849
+ },
850
+ onPartClick: (partIdx) => {
851
+ const partConfig = store.parts[partIdx];
852
+ if (partConfig) {
853
+ const firstQuestionIdx = partConfig.startIndex ?? 0;
854
+ store.goToQuestion(firstQuestionIdx);
855
+ store.setCurrentPartIndex(partIdx);
856
+ const targetQuestion = store.questions[firstQuestionIdx];
857
+ if (targetQuestion) {
858
+ scrollToEnglishCertQuestion(targetQuestion.id, {
859
+ behavior: 'smooth',
860
+ block: 'start',
861
+ });
862
+ }
863
+ }
864
+ },
865
+ };
866
+ if (toeicCurrentPart && toeicCurrentPartConfig) {
867
+ return (_jsxs(_Fragment, { children: [_jsx(EnglishCertificationExamLayout, { headerProps: toeicHeaderProps, navigationProps: toeicNavProps, footerProps: toeicFooterProps, children: _jsx(EnglishCertificationQuestionRenderer, { part: toeicCurrentPart, partConfig: toeicCurrentPartConfig, currentQuestionIndex: toeicQuestionIndexInPart, answers: store.answers, flaggedQuestions: store.flaggedQuestions, onAnswerChange: handleAnswerChange, onToggleFlag: (questionId) => store.toggleFlag(questionId), isReviewMode: false, isAudioPlayed: store.playedListeningAudioParts.has(toeicCurrentPart.partId), onAudioFirstPlay: () => store.markListeningAudioPlayed(toeicCurrentPart.partId) }) }), store.isSubmitting && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", children: _jsxs("div", { className: "rounded-xl bg-white p-8 text-center shadow-2xl", children: [_jsx(Loader2, { className: "mx-auto h-12 w-12 animate-spin text-green-500" }), _jsx("p", { className: "mt-4 text-lg font-semibold text-gray-700", children: "\u0110ang n\u1ED9p b\u00E0i..." }), _jsx("p", { className: "mt-2 text-sm text-gray-500", children: "Vui l\u00F2ng \u0111\u1EE3i trong gi\u00E2y l\u00E1t" })] }) }))] }));
868
+ }
869
+ }
870
+ // -------------------------------------------------------
871
+ // Standard exam (Reading / Listening / Writing)
872
+ // -------------------------------------------------------
873
+ return (_jsxs(_Fragment, { children: [(() => {
874
+ const { Layout: ExamLayout, QuestionCard } = getStandardExamTheme(examTheme);
875
+ const layoutProps = {
876
+ headerProps: {
877
+ examName: examTitle,
878
+ examType: 'EXAM',
879
+ levelLabel,
880
+ totalQuestions,
881
+ answeredCount,
882
+ remainingSeconds: store.remainingSeconds,
883
+ isSubmitting: store.isSubmitting,
884
+ isCameraActive,
885
+ onSubmit: handleRequestSubmit,
886
+ },
887
+ onPrevious: handlePrevious,
888
+ onNext: handleNext,
889
+ onClose: handleClose,
890
+ isNavDisabled: store.isNavDisabled,
891
+ partName: currentPart?.name || (currentPart?.partNo ? `PART ${currentPart.partNo}` : undefined),
892
+ };
893
+ if (examTheme === 'CAMBRIDGE_YLE') {
894
+ const isOneQ = isOneQuestionPerScreen(activePartIndex);
895
+ layoutProps.isFirstQuestion = isOneQ ? currentQuestionGlobalIndex === 0 : activePartIndex === 0;
896
+ layoutProps.isLastQuestion = isOneQ ? currentQuestionGlobalIndex === sidebarQuestions.length - 1 : activePartIndex === parts.length - 1;
897
+ layoutProps.sidebarProps = {
898
+ parts: sidebarParts,
899
+ questions: sidebarQuestions,
900
+ currentQuestionGlobalIndex,
901
+ isNavDisabled: store.isNavDisabled,
902
+ onPartClick: (idx) => {
903
+ if (store.isNavDisabled)
904
+ return;
905
+ navDirectionRef.current = idx > activePartIndex ? 'forward' : 'backward';
906
+ setCurrentPartIndex(idx);
907
+ const targetQ = findSidebarQuestionForPart(sidebarQuestions, idx, navDirectionRef.current);
908
+ if (targetQ)
909
+ setCurrentQuestionGlobalIndex(targetQ.globalIndex);
910
+ scrollToTop();
911
+ },
912
+ onQuestionClick: (idx) => {
913
+ if (store.isNavDisabled)
914
+ return;
915
+ const question = sidebarQuestions[idx];
916
+ if (question && question.partIndex !== activePartIndex) {
917
+ navDirectionRef.current = question.partIndex > activePartIndex ? 'forward' : 'backward';
918
+ setCurrentPartIndex(question.partIndex);
919
+ }
920
+ setCurrentQuestionGlobalIndex(idx);
921
+ // Scroll to the specific question
922
+ if (question) {
923
+ setTimeout(() => {
924
+ const el = document.getElementById(`question-${question.questionId}`);
925
+ if (el) {
926
+ // Add a small offset if there's a sticky header, but block: 'center' or 'start' usually works
927
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
928
+ }
929
+ }, 100);
930
+ }
931
+ }
932
+ };
933
+ }
934
+ else {
935
+ layoutProps.isFirstQuestion = activePartIndex === 0;
936
+ layoutProps.isLastQuestion = activePartIndex === parts.length - 1;
937
+ }
938
+ // For speaking questions using the standard layout (like SPEAKING_DESCRIBE_IMAGE),
939
+ // we want to render them one-by-one according to the global index.
940
+ const isSpeakingUsingLayout = examTheme === 'CAMBRIDGE_YLE' && isCambridgeYleInlineSpeakingType(currentPart?.questionType);
941
+ const currentQuestionInSidebar = sidebarQuestions[currentQuestionGlobalIndex];
942
+ const currentQuestionIndexInPart = isSpeakingUsingLayout
943
+ ? (currentQuestionInSidebar?.partIndex === activePartIndex ? currentQuestionInSidebar.indexInPart : undefined)
944
+ : undefined; // Force all questions to render for Reading/Writing
945
+ return (_jsxs(ExamLayout, { ...layoutProps, children: [_jsx(QuestionCard, { partName: currentPart?.name || (currentPart?.partNo ? `PART ${currentPart.partNo}` : undefined), children: _jsx(QuestionRenderer, { part: currentPart, answers: store.answers || {}, onAnswerChange: handleAnswerChange, isReviewMode: false, currentQuestionIndex: currentQuestionIndexInPart, onExit: handleClose, onPartComplete: activePartIndex < parts.length - 1 ? handleNext : handleRequestSubmit, onPartBack: activePartIndex > 0 ? handlePrevious : undefined, speakingTheme: examTheme === 'CAMBRIDGE_YLE' ? 'CAMBRIDGE_YLE' : speakingTheme, initialQuestionIndex: getPartInitialQuestionIndex(currentPart, navDirectionRef.current), skipTeacherVideo: store.playedIntroParts.has(activePartIndex) || examTemplateTitle !== achieversExamTitle, onTeacherIntroPlayed: () => handleTeacherIntroPlayed(activePartIndex) }) }), store.isSubmitting && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", children: _jsxs("div", { className: "rounded-xl bg-white p-8 text-center shadow-2xl", children: [_jsx(Loader2, { className: "mx-auto h-12 w-12 animate-spin text-green-500" }), _jsx("p", { className: "mt-4 text-lg font-semibold text-gray-700", children: "\u0110ang n\u1ED9p b\u00E0i..." }), _jsx("p", { className: "mt-2 text-sm text-gray-500", children: "Vui l\u00F2ng \u0111\u1EE3i trong gi\u00E2y l\u00E1t" })] }) }))] }));
946
+ })(), examTheme === 'CAMBRIDGE_YLE' ? (showSubmitConfirm && (_jsx(SummerSkySubmitModal, { onClose: () => setShowSubmitConfirm(false), onSubmit: handleSubmit, isSubmitting: store.isSubmitting }))) : (_jsx(AlertDialog, { open: showSubmitConfirm, onOpenChange: setShowSubmitConfirm, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "N\u1ED9p b\u00E0i thi" }), _jsx(AlertDialogDescription, { children: "B\u1EA1n c\u00F3 ch\u1EAFc ch\u1EAFn mu\u1ED1n n\u1ED9p b\u00E0i kh\u00F4ng? Sau khi n\u1ED9p, b\u1EA1n s\u1EBD kh\u00F4ng th\u1EC3 thay \u0111\u1ED5i c\u00E2u tr\u1EA3 l\u1EDDi." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: store.isSubmitting, children: "Quay l\u1EA1i" }), _jsx(AlertDialogAction, { onClick: handleSubmit, disabled: store.isSubmitting, children: store.isSubmitting ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "\u00C4\u0090ang n\u00E1\u00BB\u2122p..."] })) : ('Nộp bài') })] })] }) })), showExitConfirm && (_jsx(SummerSkyExitModal, { onClose: () => setShowExitConfirm(false), onConfirmExit: handleConfirmExit, isCancelling: isCancelling }))] }));
947
+ }
948
+ /**
949
+ * SDK container for the student exam-taking screen. Wraps the orchestration in an
950
+ * {@link ExamTakingApiProvider} so descendants (e.g. the speaking recorder) can
951
+ * reach the injected API without prop-drilling.
952
+ */
953
+ export function ExamTakingPageContainer(props) {
954
+ return (_jsx(ExamTakingApiProvider, { api: props.api, children: _jsx(ExamTakingPageContainerInner, { ...props }) }));
955
+ }