@tinyweb_dev/oe-exam-sdk 0.1.2 → 0.1.4

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 (328) hide show
  1. package/README.md +58 -4
  2. package/dist/api/contest-rooms.d.ts +13 -0
  3. package/dist/api/contest-rooms.js +1 -0
  4. package/dist/api/contest-rooms.types.d.ts +47 -0
  5. package/dist/api/contest-rooms.types.js +1 -0
  6. package/dist/api/exam-entry.d.ts +14 -0
  7. package/dist/api/exam-entry.js +1 -0
  8. package/dist/api/exam-entry.types.d.ts +93 -0
  9. package/dist/api/exam-entry.types.js +1 -0
  10. package/dist/api/exam-questions.d.ts +1 -1
  11. package/dist/api/exam-questions.js +1 -1
  12. package/dist/api/exam-questions.types.d.ts +45 -1
  13. package/dist/api/index.d.ts +2 -0
  14. package/dist/api/index.js +1 -1
  15. package/dist/components/exams/CompletionDialog.d.ts +8 -0
  16. package/dist/components/exams/CompletionDialog.js +1 -0
  17. package/dist/components/exams/CreateExamInfoForm.d.ts +4 -1
  18. package/dist/components/exams/CreateExamInfoForm.js +1 -1
  19. package/dist/components/exams/CreateExamPageContainer.d.ts +3 -0
  20. package/dist/components/exams/CreateExamPageContainer.js +1 -1
  21. package/dist/components/exams/ExamCreator.d.ts +9 -3
  22. package/dist/components/exams/ExamCreator.js +1 -1
  23. package/dist/components/exams/ExamPartTabs.d.ts +1 -1
  24. package/dist/components/exams/ExamPartTabs.js +1 -1
  25. package/dist/components/exams/ExamPreviewDialog.d.ts +20 -0
  26. package/dist/components/exams/ExamPreviewDialog.js +1 -0
  27. package/dist/components/exams/ExamQuestionGrid.d.ts +1 -1
  28. package/dist/components/exams/ExamQuestionGrid.js +1 -1
  29. package/dist/components/exams/ExamQuestionsPageContainer.d.ts +3 -0
  30. package/dist/components/exams/ExamQuestionsPageContainer.js +1 -1
  31. package/dist/components/exams/ExamViewDialog.d.ts +24 -0
  32. package/dist/components/exams/ExamViewDialog.js +1 -0
  33. package/dist/components/exams/TemplateSelector.js +1 -1
  34. package/dist/components/exams/UnsavedChangesDialog.d.ts +8 -0
  35. package/dist/components/exams/UnsavedChangesDialog.js +1 -0
  36. package/dist/components/exams/api.d.ts +1 -1
  37. package/dist/components/exams/create-exam.texts.d.ts +58 -0
  38. package/dist/components/exams/create-exam.texts.js +1 -0
  39. package/dist/components/exams/create-exam.validation.js +1 -1
  40. package/dist/components/exams/entry/EntryRestartDialog.d.ts +12 -0
  41. package/dist/components/exams/entry/EntryRestartDialog.js +1 -0
  42. package/dist/components/exams/entry/StudentExamEntryPageContainer.d.ts +31 -0
  43. package/dist/components/exams/entry/StudentExamEntryPageContainer.js +1 -0
  44. package/dist/components/exams/entry/exam-entry.states.d.ts +22 -0
  45. package/dist/components/exams/entry/exam-entry.states.js +1 -0
  46. package/dist/components/exams/entry/exam-entry.utils.d.ts +16 -0
  47. package/dist/components/exams/entry/exam-entry.utils.js +1 -0
  48. package/dist/components/exams/entry/fullscreen-loading.d.ts +5 -0
  49. package/dist/components/exams/entry/fullscreen-loading.js +1 -0
  50. package/dist/components/exams/entry/index.d.ts +7 -0
  51. package/dist/components/exams/entry/index.js +1 -0
  52. package/dist/components/exams/exam-question-status.d.ts +9 -0
  53. package/dist/components/exams/exam-question-status.js +1 -0
  54. package/dist/components/exams/index.d.ts +19 -1
  55. package/dist/components/exams/index.js +1 -1
  56. package/dist/components/exams/rooms/StudentContestRoomsPageContainer.d.ts +18 -0
  57. package/dist/components/exams/rooms/StudentContestRoomsPageContainer.js +1 -0
  58. package/dist/components/exams/rooms/contest-rooms.utils.d.ts +32 -0
  59. package/dist/components/exams/rooms/contest-rooms.utils.js +1 -0
  60. package/dist/components/exams/rooms/index.d.ts +3 -0
  61. package/dist/components/exams/rooms/index.js +1 -0
  62. package/dist/components/exams/take/ExamTakingApiContext.d.ts +10 -0
  63. package/dist/components/exams/take/ExamTakingApiContext.js +1 -0
  64. package/dist/components/exams/take/ExamTakingPageContainer.d.ts +28 -4
  65. package/dist/components/exams/take/ExamTakingPageContainer.js +1 -1
  66. package/dist/components/exams/take/components/QuestionRenderer.d.ts +25 -0
  67. package/dist/components/exams/take/components/QuestionRenderer.js +1 -0
  68. package/dist/components/exams/take/components/index.d.ts +10 -0
  69. package/dist/components/exams/take/components/index.js +1 -0
  70. package/dist/components/exams/take/components/question-renderers/ArticlesViewer.d.ts +7 -0
  71. package/dist/components/exams/take/components/question-renderers/ArticlesViewer.js +1 -0
  72. package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.d.ts +22 -0
  73. package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +1 -0
  74. package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.d.ts +14 -0
  75. package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.js +1 -0
  76. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.d.ts +2 -0
  77. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +1 -0
  78. package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.d.ts +3 -0
  79. package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.js +1 -0
  80. package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.d.ts +2 -0
  81. package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.js +1 -0
  82. package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.d.ts +2 -0
  83. package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.js +1 -0
  84. package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.d.ts +2 -0
  85. package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.js +1 -0
  86. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +21 -0
  87. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +1 -0
  88. package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.d.ts +19 -0
  89. package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.js +1 -0
  90. package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.d.ts +2 -0
  91. package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.js +1 -0
  92. package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.d.ts +2 -0
  93. package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.js +1 -0
  94. package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.d.ts +2 -0
  95. package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.js +1 -0
  96. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.d.ts +20 -0
  97. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.js +1 -0
  98. package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.d.ts +32 -0
  99. package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.js +1 -0
  100. package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.d.ts +18 -0
  101. package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +1 -0
  102. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.d.ts +19 -0
  103. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.js +1 -0
  104. package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.d.ts +22 -0
  105. package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +1 -0
  106. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +18 -0
  107. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +1 -0
  108. package/dist/components/exams/take/components/question-renderers/index.d.ts +19 -0
  109. package/dist/components/exams/take/components/question-renderers/index.js +1 -0
  110. package/dist/components/exams/take/components/speaking/FindObjectInPicture.module.css +133 -0
  111. package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.d.ts +9 -0
  112. package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.js +1 -0
  113. package/dist/components/exams/take/components/speaking/SpeakingCompareImages.module.css +153 -0
  114. package/dist/components/exams/take/components/speaking/SpeakingContentFrame.d.ts +7 -0
  115. package/dist/components/exams/take/components/speaking/SpeakingContentFrame.js +1 -0
  116. package/dist/components/exams/take/components/speaking/SpeakingDragObjects.module.css +238 -0
  117. package/dist/components/exams/take/components/speaking/SpeakingExamLayout.d.ts +27 -0
  118. package/dist/components/exams/take/components/speaking/SpeakingExamLayout.js +1 -0
  119. package/dist/components/exams/take/components/speaking/SpeakingHeader.d.ts +6 -0
  120. package/dist/components/exams/take/components/speaking/SpeakingHeader.js +1 -0
  121. package/dist/components/exams/take/components/speaking/SpeakingInfoExchange.module.css +193 -0
  122. package/dist/components/exams/take/components/speaking/SpeakingInfoExchangeSummerSky.module.css +238 -0
  123. package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.d.ts +5 -0
  124. package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.js +1 -0
  125. package/dist/components/exams/take/components/speaking/SpeakingMicButton.d.ts +14 -0
  126. package/dist/components/exams/take/components/speaking/SpeakingMicButton.js +1 -0
  127. package/dist/components/exams/take/components/speaking/SpeakingNavButton.d.ts +7 -0
  128. package/dist/components/exams/take/components/speaking/SpeakingNavButton.js +1 -0
  129. package/dist/components/exams/take/components/speaking/SpeakingPartBadge.d.ts +6 -0
  130. package/dist/components/exams/take/components/speaking/SpeakingPartBadge.js +1 -0
  131. package/dist/components/exams/take/components/speaking/SpeakingReadDisplayedContent.module.css +115 -0
  132. package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.d.ts +11 -0
  133. package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.js +1 -0
  134. package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.d.ts +1 -0
  135. package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.js +1 -0
  136. package/dist/components/exams/take/components/speaking/SpeakingStoryImages.module.css +231 -0
  137. package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.d.ts +13 -0
  138. package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.js +1 -0
  139. package/dist/components/exams/take/components/speaking/index.d.ts +1 -0
  140. package/dist/components/exams/take/components/speaking/index.js +1 -0
  141. package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.d.ts +16 -0
  142. package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.js +1 -0
  143. package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.d.ts +16 -0
  144. package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.js +1 -0
  145. package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.d.ts +17 -0
  146. package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.js +1 -0
  147. package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.d.ts +16 -0
  148. package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.js +1 -0
  149. package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.d.ts +17 -0
  150. package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.js +1 -0
  151. package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.d.ts +18 -0
  152. package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.js +1 -0
  153. package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.d.ts +16 -0
  154. package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.js +1 -0
  155. package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.d.ts +16 -0
  156. package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.js +1 -0
  157. package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.d.ts +16 -0
  158. package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.js +1 -0
  159. package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.d.ts +16 -0
  160. package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.js +1 -0
  161. package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.d.ts +16 -0
  162. package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.js +1 -0
  163. package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.d.ts +26 -0
  164. package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.js +1 -0
  165. package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.d.ts +32 -0
  166. package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.js +1 -0
  167. package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.d.ts +34 -0
  168. package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.js +1 -0
  169. package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.d.ts +17 -0
  170. package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.js +1 -0
  171. package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.d.ts +17 -0
  172. package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.js +1 -0
  173. package/dist/components/exams/take/components/speaking/renderers/index.d.ts +13 -0
  174. package/dist/components/exams/take/components/speaking/renderers/index.js +1 -0
  175. package/dist/components/exams/take/hooks/index.d.ts +2 -0
  176. package/dist/components/exams/take/hooks/index.js +1 -0
  177. package/dist/components/exams/take/hooks/useAnswerAutosave.d.ts +27 -0
  178. package/dist/components/exams/take/hooks/useAnswerAutosave.js +1 -0
  179. package/dist/components/exams/take/hooks/useAttemptSubmit.d.ts +29 -0
  180. package/dist/components/exams/take/hooks/useAttemptSubmit.js +1 -0
  181. package/dist/components/exams/take/hooks/useAutoSave.d.ts +20 -0
  182. package/dist/components/exams/take/hooks/useAutoSave.js +1 -0
  183. package/dist/components/exams/take/hooks/useExamCountdown.d.ts +22 -0
  184. package/dist/components/exams/take/hooks/useExamCountdown.js +1 -0
  185. package/dist/components/exams/take/hooks/useExamTimer.d.ts +21 -0
  186. package/dist/components/exams/take/hooks/useExamTimer.js +1 -0
  187. package/dist/components/exams/take/hooks/useProctoringCamera.d.ts +19 -0
  188. package/dist/components/exams/take/hooks/useProctoringCamera.js +1 -0
  189. package/dist/components/exams/take/hooks/useProctoringSnapshot.d.ts +25 -0
  190. package/dist/components/exams/take/hooks/useProctoringSnapshot.js +1 -0
  191. package/dist/components/exams/take/hooks/useSpeakingConversationAudio.d.ts +19 -0
  192. package/dist/components/exams/take/hooks/useSpeakingConversationAudio.js +1 -0
  193. package/dist/components/exams/take/hooks/useSpeakingNavigation.d.ts +37 -0
  194. package/dist/components/exams/take/hooks/useSpeakingNavigation.js +1 -0
  195. package/dist/components/exams/take/hooks/useSpeakingRecorder.d.ts +45 -5
  196. package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -1
  197. package/dist/components/exams/take/hooks/useSpeakingRendererSetup.d.ts +39 -0
  198. package/dist/components/exams/take/hooks/useSpeakingRendererSetup.js +1 -0
  199. package/dist/components/exams/take/hooks/useToeicAudio.d.ts +17 -0
  200. package/dist/components/exams/take/hooks/useToeicAudio.js +1 -0
  201. package/dist/components/exams/take/index.d.ts +9 -1
  202. package/dist/components/exams/take/index.js +1 -1
  203. package/dist/components/exams/take/types/speaking-cue-card.types.d.ts +10 -0
  204. package/dist/components/exams/take/types/speaking-cue-card.types.js +1 -0
  205. package/dist/components/exams/take/types/speaking.types.d.ts +153 -0
  206. package/dist/components/exams/take/types/speaking.types.js +1 -0
  207. package/dist/components/exams/take/types.d.ts +286 -1
  208. package/dist/components/exams/take/utils/answer-transformers.d.ts +26 -0
  209. package/dist/components/exams/take/utils/answer-transformers.js +1 -0
  210. package/dist/components/exams/take/utils/coloring-assignments.d.ts +5 -0
  211. package/dist/components/exams/take/utils/coloring-assignments.js +1 -0
  212. package/dist/components/exams/take/utils/question-transformers.d.ts +362 -0
  213. package/dist/components/exams/take/utils/question-transformers.js +1 -0
  214. package/dist/components/features/exam-entry/entry-state-cards.d.ts +19 -0
  215. package/dist/components/features/exam-entry/entry-state-cards.js +1 -0
  216. package/dist/components/features/exam-entry/index.d.ts +2 -0
  217. package/dist/components/features/exam-entry/index.js +1 -0
  218. package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.d.ts +12 -0
  219. package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.js +1 -0
  220. package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.d.ts +14 -0
  221. package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.js +1 -0
  222. package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.d.ts +16 -0
  223. package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.js +1 -0
  224. package/dist/components/features/exam-entry/themed/components/CloseButton.d.ts +9 -0
  225. package/dist/components/features/exam-entry/themed/components/CloseButton.js +1 -0
  226. package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.d.ts +13 -0
  227. package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.js +1 -0
  228. package/dist/components/features/exam-entry/themed/components/ModeSelector.d.ts +7 -0
  229. package/dist/components/features/exam-entry/themed/components/ModeSelector.js +1 -0
  230. package/dist/components/features/exam-entry/themed/components/StartButton.d.ts +11 -0
  231. package/dist/components/features/exam-entry/themed/components/StartButton.js +1 -0
  232. package/dist/components/features/exam-entry/themed/components/index.d.ts +4 -0
  233. package/dist/components/features/exam-entry/themed/components/index.js +1 -0
  234. package/dist/components/features/exam-entry/themed/index.d.ts +1 -0
  235. package/dist/components/features/exam-entry/themed/index.js +1 -0
  236. package/dist/components/results/ResultReviewPageContainer.d.ts +3 -1
  237. package/dist/components/results/ResultReviewPageContainer.js +1 -1
  238. package/dist/components/results/hooks/useResultReviewPolling.d.ts +27 -0
  239. package/dist/components/results/hooks/useResultReviewPolling.js +1 -0
  240. package/dist/components/results/index.d.ts +3 -0
  241. package/dist/components/results/index.js +1 -1
  242. package/dist/components/results/review-status.utils.d.ts +12 -0
  243. package/dist/components/results/review-status.utils.js +1 -0
  244. package/dist/components/shared/audio/SpeakingAudioPlayer.d.ts +12 -0
  245. package/dist/components/shared/audio/SpeakingAudioPlayer.js +1 -0
  246. package/dist/components/shared/audio/SpeakingAudioPlayer.module.css +115 -0
  247. package/dist/components/shared/audio/assets/fr-image-part1-border.png +0 -0
  248. package/dist/components/shared/audio/assets/ic-speaker.webp +0 -0
  249. package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -1
  250. package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.d.ts +11 -3
  251. package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -1
  252. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.d.ts +5 -1
  253. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -1
  254. package/dist/components/themes/index.d.ts +11 -0
  255. package/dist/components/themes/index.js +1 -1
  256. package/dist/components/themes/spaceship/SpaceshipAudioPlayer.d.ts +8 -0
  257. package/dist/components/themes/spaceship/SpaceshipAudioPlayer.js +1 -0
  258. package/dist/components/themes/spaceship/SpaceshipLayout.d.ts +2 -0
  259. package/dist/components/themes/spaceship/SpaceshipLayout.js +1 -0
  260. package/dist/components/themes/spaceship/assets/images/X-sunset.png +0 -0
  261. package/dist/components/themes/spaceship/assets/images/X.webp +0 -0
  262. package/dist/components/themes/spaceship/assets/images/audio-bar.webp +0 -0
  263. package/dist/components/themes/spaceship/assets/images/background.webp +0 -0
  264. package/dist/components/themes/spaceship/assets/images/bg_sunset.png +0 -0
  265. package/dist/components/themes/spaceship/assets/images/big-blue-badge-sunset.png +0 -0
  266. package/dist/components/themes/spaceship/assets/images/big-blue-badge.png +0 -0
  267. package/dist/components/themes/spaceship/assets/images/big-blue-badge.webp +0 -0
  268. package/dist/components/themes/spaceship/assets/images/bingo.webp +0 -0
  269. package/dist/components/themes/spaceship/assets/images/blue-badge-sunset.png +0 -0
  270. package/dist/components/themes/spaceship/assets/images/blue-badge.png +0 -0
  271. package/dist/components/themes/spaceship/assets/images/blue-wrapper-sunset.png +0 -0
  272. package/dist/components/themes/spaceship/assets/images/blue-wrapper.webp +0 -0
  273. package/dist/components/themes/spaceship/assets/images/board_red.png +0 -0
  274. package/dist/components/themes/spaceship/assets/images/button_feedback_vi.png +0 -0
  275. package/dist/components/themes/spaceship/assets/images/circle-btn-right-sunset.png +0 -0
  276. package/dist/components/themes/spaceship/assets/images/circle-btn-right.png +0 -0
  277. package/dist/components/themes/spaceship/assets/images/circle-btn-sunset.png +0 -0
  278. package/dist/components/themes/spaceship/assets/images/circle-btn.webp +0 -0
  279. package/dist/components/themes/spaceship/assets/images/focus-vector.webp +0 -0
  280. package/dist/components/themes/spaceship/assets/images/head-phone_red.png +0 -0
  281. package/dist/components/themes/spaceship/assets/images/ic-mute-sunset.png +0 -0
  282. package/dist/components/themes/spaceship/assets/images/ic-mute.png +0 -0
  283. package/dist/components/themes/spaceship/assets/images/ic-play-sunset.png +0 -0
  284. package/dist/components/themes/spaceship/assets/images/ic-play-violet.webp +0 -0
  285. package/dist/components/themes/spaceship/assets/images/ic-play.webp +0 -0
  286. package/dist/components/themes/spaceship/assets/images/micro.png +0 -0
  287. package/dist/components/themes/spaceship/assets/images/next-small.webp +0 -0
  288. package/dist/components/themes/spaceship/assets/images/next-sunset.png +0 -0
  289. package/dist/components/themes/spaceship/assets/images/next.webp +0 -0
  290. package/dist/components/themes/spaceship/assets/images/purple-circle.webp +0 -0
  291. package/dist/components/themes/spaceship/assets/images/question-badge-sunset.png +0 -0
  292. package/dist/components/themes/spaceship/assets/images/question-badge.webp +0 -0
  293. package/dist/components/themes/spaceship/assets/images/red-badge-sunset.png +0 -0
  294. package/dist/components/themes/spaceship/assets/images/red-badge.webp +0 -0
  295. package/dist/components/themes/spaceship/assets/images/red-wrapper-sunset.png +0 -0
  296. package/dist/components/themes/spaceship/assets/images/red-wrapper.webp +0 -0
  297. package/dist/components/themes/spaceship/assets/images/robot-avatar.png +0 -0
  298. package/dist/components/themes/spaceship/assets/images/sound_red.png +0 -0
  299. package/dist/components/themes/spaceship/assets/images/student-avatar.png +0 -0
  300. package/dist/components/themes/spaceship/assets/images/teacher-play-sunset.png +0 -0
  301. package/dist/components/themes/spaceship/assets/images/teacher-play.webp +0 -0
  302. package/dist/components/themes/spaceship/assets/images/timer-badge.png +0 -0
  303. package/dist/components/themes/spaceship/assets/styles/SpaceshipLayout.module.css +347 -0
  304. package/dist/components/themes/spaceship/assets/styles/SpaceshipWaitingRoom.module.css +395 -0
  305. package/dist/components/ui/badge.d.ts +1 -1
  306. package/dist/index.d.ts +4 -4
  307. package/dist/index.js +1 -1
  308. package/dist/shared/index.d.ts +3 -0
  309. package/dist/shared/index.js +1 -1
  310. package/dist/shared/lib/stores/examTakingStore.d.ts +54 -0
  311. package/dist/shared/lib/stores/examTakingStore.js +1 -0
  312. package/dist/shared/lib/stores/gradingTransientStore.d.ts +23 -0
  313. package/dist/shared/lib/stores/gradingTransientStore.js +1 -0
  314. package/dist/shared/lib/themes/config/starters.config.d.ts +3 -0
  315. package/dist/shared/lib/themes/config/starters.config.js +1 -0
  316. package/dist/shared/lib/themes/index.d.ts +18 -0
  317. package/dist/shared/lib/themes/index.js +1 -0
  318. package/dist/shared/lib/themes/types.d.ts +39 -0
  319. package/dist/shared/lib/themes/types.js +1 -0
  320. package/dist/shared/lib/toast.d.ts +19 -0
  321. package/dist/shared/lib/toast.js +1 -0
  322. package/dist/shared/lib/utils/question-reverse-transform.d.ts +6 -0
  323. package/dist/shared/lib/utils/question-reverse-transform.js +1 -0
  324. package/dist/shared/lib/utils/question-transform-validation.d.ts +6 -0
  325. package/dist/shared/lib/utils/question-transform-validation.js +1 -0
  326. package/dist/shared/lib/utils/question-transform.d.ts +6 -2
  327. package/dist/shared/lib/utils/question-transform.js +1 -1
  328. package/package.json +19 -4
@@ -0,0 +1 @@
1
+ "use client";import{jsxs as e,jsx as t}from"react/jsx-runtime";import{useState as r,useRef as n,useEffect as i,useCallback as a}from"react";import{usePresignedFileUrl as o}from"../../../../../../shared/lib/hooks";import{useSpeakingRendererSetup as s}from"../../../hooks/useSpeakingRendererSetup";import{getSpeakingLayout as d}from"../../../../../../components/themes";import l from"../SpeakingReadDisplayedContent.module.css";import c from"../../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner";import u from"next/image";import h from"../../../../../../components/themes/summer-sky/assets/images/mic_ver2_icon.png";import m from"../../../../../../components/themes/summer-sky/assets/images/stop_audio_ver2_icon.png";import p from"../../../../../../components/themes/summer-sky/assets/images/audio_ver2_icon.png";import g from"../../../../../../components/themes/summer-sky/assets/images/pause_audio_ver2_icon.png";import f from"../../../../../../components/themes/summer-sky/assets/images/lock_mic_ver2_icon.png";function x({total:r,currentIndex:n,answeredIds:i,questionIds:a}){return e("div",{className:l.taskIndicator,style:{color:"#78350f",textShadow:"none",fontWeight:800},children:[e("span",{children:["Question ",n+1,"/ ",r]}),t("div",{className:"flex gap-2",children:Array.from({length:r},(e,r)=>{const o=r===n,s=i.has(a[r]);return t("div",{className:`${l.taskDot} ${o?l.taskDotActive:""} ${s&&!o?l.taskDotCompleted:""}`},r)})})]})}function v({audioUrl:o,autoPlay:s,onEnded:d,disabled:l}){const c=n(null),[u,h]=r(!1),[m,p]=r(0),[g,f]=r(0);i(()=>{const e=c.current;if(!e)return;const t=()=>p(e.currentTime),r=()=>f(e.duration),n=()=>{h(!1),d?.()},i=()=>h(!0),a=()=>h(!1);return e.addEventListener("timeupdate",t),e.addEventListener("loadedmetadata",r),e.addEventListener("ended",n),e.addEventListener("play",i),e.addEventListener("pause",a),()=>{e.removeEventListener("timeupdate",t),e.removeEventListener("loadedmetadata",r),e.removeEventListener("ended",n),e.removeEventListener("play",i),e.removeEventListener("pause",a)}},[o,d]),i(()=>{const e=c.current;e&&(e.pause(),e.currentTime=0,h(!1),p(0),f(0))},[o]),i(()=>{const e=c.current;if(!e||!s)return;let t=!1;const r=()=>{t||e.play().catch(()=>{})};return e.readyState>=3?r():e.addEventListener("canplay",r,{once:!0}),()=>{t=!0,e.removeEventListener("canplay",r)}},[o,s]);const x=a(()=>{const e=c.current;e&&(u?e.pause():((e.ended||e.currentTime>=e.duration)&&(e.currentTime=0,p(0)),e.play().catch(console.error)))},[u]),v=a(e=>{const t=c.current;if(!t)return;const r=parseFloat(e.target.value);t.currentTime=r,p(r)},[]),N=e=>`${Math.floor(e/60)}:${Math.floor(e%60).toString().padStart(2,"0")}`,w=g>0?m/g*100:0;return e("div",{className:"w-full flex items-center gap-4 rounded-full px-3 py-3",style:{maxWidth:"650px",background:"linear-gradient(135deg, #B4F0FF 0%, #7EEAFF 50%, #5FE0F5 100%)",border:"5px solid white",boxShadow:"0 4px 15px rgba(0,0,0,0.08)",borderRadius:"20px"},children:[t("audio",{ref:c,src:o,preload:"metadata"}),t("button",{onClick:l?void 0:x,disabled:l,className:"flex-shrink-0 flex items-center justify-center rounded-xl transition-all "+(l?"opacity-50 cursor-not-allowed grayscale":"hover:scale-105 active:scale-95"),style:{width:"40px",height:"40px",background:u?"linear-gradient(135deg, #FF5B7F, #FF3D6E)":"linear-gradient(135deg, #1565C0, #1E88E5)",boxShadow:u?"0 3px 8px rgba(255,61,110,0.4)":"0 3px 8px rgba(21,101,192,0.4)"},children:u?e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"white",children:[t("rect",{x:"5",y:"3",width:"5",height:"18",rx:"2"}),t("rect",{x:"14",y:"3",width:"5",height:"18",rx:"2"})]}):t("svg",{width:"18",height:"18",viewBox:"0 0 24 24",fill:"white",children:t("path",{d:"M8 5v14l11-7z"})})}),e("span",{className:"flex-shrink-0 font-bold tabular-nums",style:{color:"#1A3D6D",fontSize:"16px",fontFamily:'"Quicksand", sans-serif',minWidth:"85px"},children:[N(m)," / ",N(g)]}),e("div",{className:"flex-1 relative h-2 rounded-full overflow-hidden",style:{background:"rgba(255,255,255,0.6)"},children:[t("div",{className:"absolute top-0 left-0 h-full rounded-full transition-all",style:{width:`${w}%`,background:"linear-gradient(90deg, #1565C0, #42A5F5)"}}),t("input",{type:"range",min:0,max:g||0,step:.1,value:m,onChange:l?void 0:v,disabled:l,className:"absolute inset-0 w-full h-full opacity-0 "+(l?"cursor-not-allowed":"cursor-pointer")}),t("div",{className:"absolute top-1/2 -translate-y-1/2 w-4 h-4 rounded-full border-2 border-white shadow-md pointer-events-none",style:{left:`calc(${w}% - 8px)`,background:"#1565C0"}})]})]})}function N({total:r,currentIndex:n,answeredIds:i,questionIds:a}){return e("div",{className:"flex items-center gap-2 font-bold text-[#78350f] font-['Quicksand'] z-10",children:[e("span",{className:"text-sm",children:["Question ",n+1,"/ ",r]}),t("div",{className:"flex gap-1.5",children:Array.from({length:r},(e,r)=>{const o=r===n,s=i.has(a[r]);return t("div",{className:"w-2.5 h-2.5 rounded-full "+(o?"bg-[#FF9900] scale-125 shadow-sm":s?"bg-[#16CA3B]":"bg-white/60")},r)})})]})}export function SpeakingSpontaneousQARenderer(l){const w=s({...l,maxDurationMs:6e4}),y=d(l.speakingTheme),b=w.currentQuestion?.content,{previewUrl:E}=o(b?.audioUrl??""),k=0===w.nav.currentIndex,I=w.isQuestionLocked,L=a(()=>I?"ALLOW_RECORDING":k&&!l.skipTeacherVideo&&l.waitForTeacherVideo?"WAITING_TEACHER":"DELAYING",[k,l.skipTeacherVideo,l.waitForTeacherVideo,I]),[A,T]=r(L),F=I||"ALLOW_RECORDING"===A,S=n(null),[C,_]=r(!1),[,P]=r(0),[,R]=r(0);i(()=>{const e=S.current;if(!e)return;const t=()=>{_(!0),w.onAudioPlayingChange?.(!0)},r=()=>{_(!1),w.onAudioPlayingChange?.(!1)},n=()=>{_(!1),P(0),w.onAudioPlayingChange?.(!1)},i=()=>{P(e.currentTime),e.duration&&isFinite(e.duration)&&R(e.duration)},a=()=>{e.duration&&isFinite(e.duration)&&R(e.duration)};return e.addEventListener("play",t),e.addEventListener("pause",r),e.addEventListener("ended",n),e.addEventListener("timeupdate",i),e.addEventListener("loadedmetadata",a),e.addEventListener("durationchange",a),()=>{e.removeEventListener("play",t),e.removeEventListener("pause",r),e.removeEventListener("ended",n),e.removeEventListener("timeupdate",i),e.removeEventListener("loadedmetadata",a),e.removeEventListener("durationchange",a),w.onAudioPlayingChange?.(!1)}},[w.recorder?.previewUrl]),i(()=>{T(L())},[L,w.nav.currentIndex]),i(()=>{if("DELAYING"===A&&!I){const e=setTimeout(()=>{T("PLAYING_QUESTION")},3e3);return()=>clearTimeout(e)}},[A,I]);const G=a(()=>{T("DELAYING"),l.onTeacherIntroPlayed?.()},[l.onTeacherIntroPlayed]),D=a(()=>{"PLAYING_QUESTION"!==A||I||T("ALLOW_RECORDING")},[A,I]);if(!w.currentQuestion||!b)return null;const O=new Set(l.part.questions.map(e=>e.id).filter(e=>!!l.answers[e])),W=l.part.questions.map(e=>e.id);if(l.speakingTheme&&"CAMBRIDGE_YLE"!==l.speakingTheme){const r=t(x,{total:l.part.questions.length,currentIndex:w.nav.currentIndex,answeredIds:O,questionIds:W});return t(y,{onExit:l.onExit,partName:l.part.name||`Part ${l.part.partNo}`,instruction:l.part.instructions||"LISTEN AND ANSWER",audioUrl:b.audioUrl??"",questionNumber:w.nav.questionNumberInPart,onPrev:w.handlePrev,onNext:w.handleNext,showPrev:w.nav.showPrev,showNext:w.nav.showNext,canGoPrev:w.nav.canGoPrev,canGoNext:w.nav.canGoNext,onAudioPlayingChange:w.onAudioPlayingChange,recorder:l.isReviewMode?null:w.recorder,showExistingBadge:w.showExistingBadge,headerRight:r,hideAudioBar:!0,teacherVideoUrl:l.teacherVideoUrl,waitForTeacherVideo:!(!k||l.skipTeacherVideo)&&l.waitForTeacherVideo,onTeacherVideoEnded:G,skipTeacherVideo:!k||l.skipTeacherVideo,showTeacherImage:F,isMicLockedOverride:!I&&"ALLOW_RECORDING"!==A,micLockedText:I?void 0:"PLAYING_QUESTION"===A?"Nghe câu hỏi trước khi trả lời":"DELAYING"===A?"Chuẩn bị phát audio câu hỏi...":"Hãy chờ giáo viên hoàn thành hướng dẫn",children:e("div",{className:"flex flex-col items-center justify-center h-full w-full gap-8 px-8 pb-40",children:[w.showExistingBadge&&e("div",{className:"rounded-full bg-green-500 px-4 py-1 flex items-center gap-2 text-sm font-bold text-white shadow-md border-2 border-white",children:[t("span",{className:"h-2.5 w-2.5 rounded-full bg-white animate-pulse"}),"Đã ghi âm"]}),e("h2",{className:"text-center",style:{color:"#2C38B6",fontSize:"30px",fontFamily:'"SVN-Volte Rounded", "Quicksand", sans-serif',fontWeight:600,lineHeight:"45.74px",letterSpacing:"-0.01em",maxWidth:"700px",WebkitTextStroke:"1px #ffffff",paintOrder:"stroke fill"},children:["Nghe câu hỏi, suy nghĩ và trả lời tự do",t("br",{}),"bằng tiếng Anh."]}),E&&t(v,{audioUrl:E,autoPlay:!I&&"PLAYING_QUESTION"===A,onEnded:D,disabled:"WAITING_TEACHER"===A})]})})}const Q=!I&&"ALLOW_RECORDING"!==A;return e("div",{className:"flex flex-col w-full h-full relative",style:{background:"linear-gradient(180deg, #E6F3FF 0%, #FFFFFF 100%)"},children:[e("div",{className:"relative w-full flex justify-between items-start pt-6 px-6 z-20 shrink-0",children:[t("div",{className:"flex-1"}),t("div",{className:"flex-none drop-shadow-md",children:t(c,{partNumber:l.part.partNo,questionCount:l.part.questions.length,partName:l.part.name||`Part ${l.part.partNo}`})}),t("div",{className:"flex-1 flex justify-end mt-2 z-20",children:t(N,{total:l.part.questions.length,currentIndex:w.nav.currentIndex,answeredIds:O,questionIds:W})})]}),e("div",{className:"flex-1 w-full flex flex-col items-center min-h-0 px-4 pb-6 pt-12",children:[e("div",{className:"flex-1 w-full flex flex-col justify-center items-center gap-12 min-h-0 relative mb-6",children:[w.showExistingBadge&&e("div",{className:"absolute right-4 top-0 z-10 rounded-full bg-green-500 px-4 py-1.5 flex items-center gap-2 text-sm font-bold text-white shadow-lg border-2 border-white transform -translate-y-1/2",children:[t("span",{className:"h-2.5 w-2.5 rounded-full bg-white animate-pulse"}),"Đã ghi âm"]}),e("h2",{className:"text-center z-10",style:{color:"#2C38B6",fontSize:"30px",fontFamily:'"SVN-Volte Rounded", "Quicksand", sans-serif',fontWeight:600,lineHeight:"45.74px",letterSpacing:"-0.01em",maxWidth:"700px",WebkitTextStroke:"1px #ffffff",paintOrder:"stroke fill"},children:["Nghe câu hỏi, suy nghĩ và trả lời tự do",t("br",{}),"bằng tiếng Anh."]}),E&&t(v,{audioUrl:E,autoPlay:!I&&"PLAYING_QUESTION"===A,onEnded:D,disabled:"WAITING_TEACHER"===A})]}),w.recorder&&!l.isReviewMode&&e("div",{className:"shrink-0 flex flex-col items-center z-50 gap-3",children:[t("div",{className:"bg-white rounded-[100px] shadow-[0_4px_10px_rgba(0,0,0,0.1)] whitespace-nowrap flex items-center justify-center",style:{padding:"10px 30px",color:Q?"#888":"recording"===w.recorder.status?"#FF4D4D":["recorded","uploading","uploaded"].includes(w.recorder.status)?"#16CA3B":"#343C55",fontSize:"18px",fontFamily:'"Quicksand", sans-serif',fontWeight:700,lineHeight:"100%",letterSpacing:"-0.01em"},children:Q?I?"Ấn biểu tượng Micro để trả lời":"PLAYING_QUESTION"===A?"Nghe câu hỏi trước khi trả lời":"DELAYING"===A?"Chuẩn bị phát audio câu hỏi...":"Hãy chờ giáo viên hoàn thành hướng dẫn":"recording"===w.recorder.status?"Ấn lần nữa để lưu kết quả":["recorded","uploading","uploaded"].includes(w.recorder.status)?"Câu trả lời đã được ghi nhận !":"Ấn biểu tượng Micro để trả lời"}),t("div",{className:"relative flex flex-col items-center",children:"recording"===w.recorder.status?e("div",{className:"relative flex flex-col justify-center items-center cursor-pointer drop-shadow-[0_0_15px_rgba(255,100,100,0.8)] transition-transform hover:scale-[1.05] active:scale-[0.95]",onClick:w.recorder.stopRecording,children:[t(u,{src:m,alt:"stop audio",className:"animate-pulse",style:{width:"120px",height:"120px"}}),e("span",{className:"absolute text-white text-[16px] font-bold font-['Quicksand']",style:{bottom:"30px"},children:[Math.floor(w.recorder.remainingSeconds/60),":",(w.recorder.remainingSeconds%60).toString().padStart(2,"0")]})]}):t(u,Q?{src:f,alt:"micro locked",className:"opacity-80 cursor-not-allowed",style:{width:"120px",height:"120px"}}:{src:["recorded","uploading","uploaded"].includes(w.recorder.status)?C?g:p:h,alt:"micro",className:"transition-transform cursor-pointer hover:scale-[1.05] active:scale-[0.95]",style:{width:"120px",height:"120px"},onClick:()=>{if(!Q)if("recording"===w.recorder?.status)w.recorder.stopRecording();else if("idle"===w.recorder?.status||"error"===w.recorder?.status)w.recorder.startRecording();else if("uploaded"===w.recorder?.status||"recorded"===w.recorder?.status){const e=S.current;e&&(C?e.pause():e.play().catch(console.error))}}})}),w.recorder.previewUrl&&t("audio",{ref:S,src:w.recorder.previewUrl})]})]})]})}
@@ -0,0 +1,17 @@
1
+ import type { QuestionPart } from '../../../utils/question-transformers';
2
+ interface SpeakingStoryImagesRendererProps {
3
+ part: QuestionPart;
4
+ answers: Record<string, unknown>;
5
+ onAnswerChange: (questionId: string, value: unknown) => void;
6
+ isReviewMode?: boolean;
7
+ onExit: () => void;
8
+ onPartComplete?: () => void;
9
+ onPartBack?: () => void;
10
+ speakingTheme?: string;
11
+ teacherVideoUrl?: string;
12
+ waitForTeacherVideo?: boolean;
13
+ onTeacherIntroPlayed?: () => void;
14
+ skipTeacherVideo?: boolean;
15
+ }
16
+ export declare function SpeakingStoryImagesRenderer(props: SpeakingStoryImagesRendererProps): import("react").JSX.Element;
17
+ export {};
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useCallback as a,useEffect as n,useState as o}from"react";import{ResolvedImage as t}from"../../../../../../components/common/ResolvedImage";import{useSpeakingRendererSetup as i}from"../../../hooks/useSpeakingRendererSetup";import{getSpeakingLayout as s}from"../../../../../../components/themes";import l from"../SpeakingStoryImages.module.css";export function SpeakingStoryImagesRenderer(c){const m=i({...c,maxDurationMs:3e4}),h=s(c.speakingTheme),u=m.currentQuestion?.content,[g,p]=o(null),x=a(()=>p(null),[]);if(n(()=>{if(!g)return;const e=e=>{"Escape"===e.key&&x()};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[g,x]),!m.currentQuestion||!u)return null;const N=u.audioUrl??"",v=u.images??[];return e(h,{onExit:c.onExit,partName:c.part.name||`Part ${c.part.partNo}`,instruction:c.part.instructions||"LOOK AT THE PICTURES. LISTEN AND TELL THE STORY.",audioUrl:N,questionNumber:m.nav.questionNumberInPart,onPrev:m.handlePrev,onNext:m.handleNext,showPrev:m.nav.showPrev,showNext:m.nav.showNext,canGoPrev:m.nav.canGoPrev,canGoNext:m.nav.canGoNext,onAudioPlayingChange:m.onAudioPlayingChange,recorder:c.isReviewMode?null:m.recorder,showExistingBadge:m.showExistingBadge,teacherVideoUrl:c.teacherVideoUrl,waitForTeacherVideo:c.waitForTeacherVideo,onTeacherVideoEnded:c.onTeacherIntroPlayed,skipTeacherVideo:c.skipTeacherVideo,children:r("div",{className:l.part3Container,children:[m.showExistingBadge&&r("div",{className:"absolute right-4 top-4 z-10 rounded-full bg-green-500 px-4 py-1 flex items-center gap-2 text-sm font-bold text-white shadow-md border-2 border-white",children:[e("span",{className:"h-2.5 w-2.5 rounded-full bg-white animate-pulse"}),"Đã ghi âm"]}),0===v.length?e("div",{className:l.emptyState,children:e("span",{children:"No images available"})}):e("div",{className:l.storyGrid,"data-count":v.length,children:v.map((r,a)=>e(d,{src:r,index:a,onEnlarge:()=>p(r)},`${r}-${a}`))}),g&&r("div",{className:l.lightboxBackdrop,onClick:x,role:"dialog","aria-modal":"true",children:[e("button",{type:"button",className:l.lightboxClose,onClick:x,"aria-label":"Đóng",children:"✕"}),e(t,{src:g,alt:"Story image enlarged",className:l.lightboxImage,draggable:!1})]})]})})}function d({src:a,index:n,onEnlarge:o}){return r("div",{className:l.storyFrame,children:[e("span",{className:l.storyNumber,"aria-hidden":"true",children:n+1}),e("button",{type:"button",className:l.imageBorder,onClick:o,"aria-label":`Phóng to ảnh số ${n+1}`,children:e(t,{src:a,alt:`Story image ${n+1}`,draggable:!1})})]})}
@@ -0,0 +1,13 @@
1
+ export { SpeakingCompareImagesRenderer } from './SpeakingCompareImagesRenderer';
2
+ export { SpeakingStoryImagesRenderer } from './SpeakingStoryImagesRenderer';
3
+ export { SpeakingOddOneOutRenderer } from './SpeakingOddOneOutRenderer';
4
+ export { SpeakingQuestionListRenderer } from './SpeakingQuestionListRenderer';
5
+ export { SpeakingChooseObjectsRenderer } from './SpeakingChooseObjectsRenderer';
6
+ export { SpeakingChooseAnswerGroupRenderer } from './SpeakingChooseAnswerGroupRenderer';
7
+ export { SpeakingDragObjectsRenderer } from './SpeakingDragObjectsRenderer';
8
+ export { SpeakingListenAndSpeakAnswerRenderer } from './SpeakingListenAndSpeakAnswerRenderer';
9
+ export { SpeakingDescribeImageRenderer } from './SpeakingDescribeImageRenderer';
10
+ export { SpeakingInfoExchangeRenderer } from './SpeakingInfoExchangeRenderer';
11
+ export { SpeakingReadDisplayedContentRenderer } from './SpeakingReadDisplayedContentRenderer';
12
+ export { SpeakingSpontaneousQARenderer } from './SpeakingSpontaneousQARenderer';
13
+ export { SpeakingQuestionRenderer } from './SpeakingQuestionRenderer';
@@ -0,0 +1 @@
1
+ export{SpeakingCompareImagesRenderer}from"./SpeakingCompareImagesRenderer";export{SpeakingStoryImagesRenderer}from"./SpeakingStoryImagesRenderer";export{SpeakingOddOneOutRenderer}from"./SpeakingOddOneOutRenderer";export{SpeakingQuestionListRenderer}from"./SpeakingQuestionListRenderer";export{SpeakingChooseObjectsRenderer}from"./SpeakingChooseObjectsRenderer";export{SpeakingChooseAnswerGroupRenderer}from"./SpeakingChooseAnswerGroupRenderer";export{SpeakingDragObjectsRenderer}from"./SpeakingDragObjectsRenderer";export{SpeakingListenAndSpeakAnswerRenderer}from"./SpeakingListenAndSpeakAnswerRenderer";export{SpeakingDescribeImageRenderer}from"./SpeakingDescribeImageRenderer";export{SpeakingInfoExchangeRenderer}from"./SpeakingInfoExchangeRenderer";export{SpeakingReadDisplayedContentRenderer}from"./SpeakingReadDisplayedContentRenderer";export{SpeakingSpontaneousQARenderer}from"./SpeakingSpontaneousQARenderer";export{SpeakingQuestionRenderer}from"./SpeakingQuestionRenderer";
@@ -0,0 +1,2 @@
1
+ export { useExamTimer } from './useExamTimer';
2
+ export { useAutoSave } from './useAutoSave';
@@ -0,0 +1 @@
1
+ export{useExamTimer}from"./useExamTimer";export{useAutoSave}from"./useAutoSave";
@@ -0,0 +1,27 @@
1
+ import type { ExamTakingAnswerValue, ExamTakingSdkApi } from '../../../../api/exam-taking';
2
+ export interface UseAnswerAutosaveOptions {
3
+ /** API adapter; autosave is skipped when `saveAnswers` is undefined. */
4
+ api: ExamTakingSdkApi;
5
+ /** Active attempt id, or null while loading. */
6
+ attemptId: string | null;
7
+ /** Whether autosave is active (attempt loaded & not submitting). */
8
+ enabled: boolean;
9
+ /** Debounce window before a pending change is flushed to the server. */
10
+ debounceMs?: number;
11
+ /** Notified when a save fails so the host can surface a toast. */
12
+ onSaveError?: (error: unknown) => void;
13
+ }
14
+ export interface UseAnswerAutosaveResult {
15
+ /** Queue a single answer change; debounced + flushed on tab hide/unload. */
16
+ queueAnswer: (questionId: string, answer: ExamTakingAnswerValue) => void;
17
+ /** Force an immediate flush of all pending answers (e.g. before submit). */
18
+ flushNow: () => Promise<void>;
19
+ /** `true` when there are unsaved answers buffered locally. */
20
+ hasPendingRef: React.MutableRefObject<boolean>;
21
+ }
22
+ /**
23
+ * Buffers answer changes and persists them in debounced batches. Ports the host
24
+ * app's resilience: a `keepalive` flush on tab hide / page unload and a retry on
25
+ * the `online` event so answers survive refreshes and flaky networks.
26
+ */
27
+ export declare function useAnswerAutosave({ api, attemptId, enabled, debounceMs, onSaveError, }: UseAnswerAutosaveOptions): UseAnswerAutosaveResult;
@@ -0,0 +1 @@
1
+ "use client";import{useCallback as e,useEffect as r,useRef as n}from"react";export function useAnswerAutosave({api:t,attemptId:i,enabled:s,debounceMs:u=800,onSaveError:c}){const a=n(new Map),o=n(!1),d=n(null),w=n(c);w.current=c;const v=e(async e=>{if(!i||!t.saveAnswers||0===a.current.size)return;const r=Array.from(a.current.entries()).map(([e,r])=>({questionId:e,answer:r}));a.current.clear(),o.current=!1;try{await t.saveAnswers({attemptId:i,answers:r,keepalive:e})}catch(e){r.forEach(({questionId:e,answer:r})=>a.current.set(e,r)),o.current=!0,w.current?.(e)}},[t,i]),m=e(()=>v(!1),[v]),l=e((e,r)=>{s&&(a.current.set(e,r),o.current=!0,d.current&&clearTimeout(d.current),d.current=setTimeout(()=>{v(!1)},u))},[u,v,s]);return r(()=>{if(!s)return;const e=()=>{v(!0)},r=()=>{"hidden"===document.visibilityState&&e()},n=()=>{o.current&&v(!1)};return document.addEventListener("visibilitychange",r),window.addEventListener("pagehide",e),window.addEventListener("online",n),()=>{document.removeEventListener("visibilitychange",r),window.removeEventListener("pagehide",e),window.removeEventListener("online",n),d.current&&clearTimeout(d.current)}},[v,s]),{queueAnswer:l,flushNow:m,hasPendingRef:o}}
@@ -0,0 +1,29 @@
1
+ import type { ExamTakingSdkApi } from '../../../../api/exam-taking';
2
+ export interface UseAttemptSubmitOptions {
3
+ /** API adapter. */
4
+ api: ExamTakingSdkApi;
5
+ /** Flush pending answers before submitting (e.g. from `useAnswerAutosave`). */
6
+ flushAnswers?: () => Promise<void>;
7
+ /** Notified on success with the submit result + pending grading answer ids. */
8
+ onSubmitted?: (result: SubmittedSummary) => void;
9
+ /** Notified when submission fails. */
10
+ onError?: (error: unknown) => void;
11
+ }
12
+ export interface SubmittedSummary {
13
+ attemptId: string;
14
+ pendingAnswerIds: string[];
15
+ manualReviewAnswerIds: string[];
16
+ autoGradedCount: number;
17
+ }
18
+ export interface UseAttemptSubmitResult {
19
+ /** Submit the attempt (flushes answers first); guards against double submit. */
20
+ submit: (attemptId: string) => Promise<void>;
21
+ /** `true` while a submit request is in flight. */
22
+ isSubmitting: boolean;
23
+ }
24
+ /**
25
+ * Submits an attempt with the host app's guarantees: flush any buffered answers
26
+ * first, prevent concurrent submits, and surface the pending/auto-graded answer
27
+ * ids so the caller can route to the review screen with the right context.
28
+ */
29
+ export declare function useAttemptSubmit({ api, flushAnswers, onSubmitted, onError, }: UseAttemptSubmitOptions): UseAttemptSubmitResult;
@@ -0,0 +1 @@
1
+ "use client";import{useCallback as t,useState as e}from"react";export function useAttemptSubmit({api:n,flushAnswers:a,onSubmitted:i,onError:s}){const[r,u]=e(!1);return{submit:t(async t=>{if(!r){u(!0);try{a&&await a();const s="object"==typeof(e=await n.submitAttempt(t))&&null!==e&&"data"in e?e.data:e??{};i?.({attemptId:t,pendingAnswerIds:s.pendingAnswerIds??[],manualReviewAnswerIds:s.manualReviewAnswerIds??[],autoGradedCount:s.autoGradedCount??0})}catch(t){s?.(t),u(!1)}var e}},[n,a,r,s,i]),isSubmitting:r}}
@@ -0,0 +1,20 @@
1
+ import type { ExamTakingSdkApi } from '../../../../api/exam-taking';
2
+ import type { QuestionPart } from '../utils/question-transformers';
3
+ interface UseAutoSaveOptions {
4
+ /** Injected API adapter (replaces the host app's `attemptsApi`). */
5
+ api: ExamTakingSdkApi;
6
+ attemptId: string;
7
+ parts: QuestionPart[];
8
+ intervalMs?: number;
9
+ onSaveStart?: () => void;
10
+ onSaveSuccess?: () => void;
11
+ onSaveError?: (error: Error) => void;
12
+ }
13
+ interface UseAutoSaveReturn {
14
+ isSaving: boolean;
15
+ lastSavedAt: Date | null;
16
+ lastError: Error | null;
17
+ saveNow: () => Promise<void>;
18
+ }
19
+ export declare function useAutoSave(options: UseAutoSaveOptions): UseAutoSaveReturn;
20
+ export {};
@@ -0,0 +1 @@
1
+ import{useState as t,useEffect as r,useRef as e,useCallback as a}from"react";import{useExamTakingStore as n}from"../../../../shared/lib/stores/examTakingStore";import{transformAnswersForSubmission as s}from"../utils/answer-transformers";export function useAutoSave(i){const o=n(),[l,u]=t(!1),[c,S]=t(null),v=e(null),m=i.intervalMs??3e4,p=a(async()=>{if(l||o.isSubmitting||!o.isDirty)return;if(0!==Object.keys(o.answers).length){u(!0),i.onSaveStart?.();try{const t=s(o.answers,i.parts);if(0===t.length)return void o.markSaved();await(i.api.saveAnswers?.({attemptId:i.attemptId,answers:t})),o.markSaved(),S(null),i.onSaveSuccess?.()}catch(t){const r=t;S(r),i.onSaveError?.(r)}finally{u(!1)}}},[i.api,i.attemptId,i.onSaveStart,i.onSaveSuccess,i.onSaveError,l,o.isSubmitting,o.isDirty,o.answers]);return r(()=>(v.current&&clearInterval(v.current),v.current=setInterval(async()=>{!o.isDirty||o.isSubmitting||l||await p()},m),()=>{v.current&&(clearInterval(v.current),v.current=null)}),[m,o.isDirty,o.isSubmitting,l,p]),{isSaving:l,lastSavedAt:o.lastSavedAt,lastError:c,saveNow:p}}
@@ -0,0 +1,22 @@
1
+ export interface UseExamCountdownOptions {
2
+ /** Total duration of the attempt in minutes. */
3
+ durationMinutes: number;
4
+ /** ISO timestamp marking when the attempt started. */
5
+ startedAt: string | null | undefined;
6
+ /** Whether the countdown is active (e.g. attempt loaded & not submitted). */
7
+ enabled: boolean;
8
+ /** Fired exactly once when remaining time reaches zero. */
9
+ onExpire: () => void;
10
+ }
11
+ export interface UseExamCountdownResult {
12
+ /** Whole seconds remaining (clamped at 0). */
13
+ remainingSeconds: number;
14
+ /** `true` once the countdown has reached zero. */
15
+ isExpired: boolean;
16
+ }
17
+ /**
18
+ * Drives the exam timer. Computes remaining seconds from `startedAt + duration`
19
+ * so the value stays correct across tab refreshes, and invokes `onExpire` once
20
+ * when time runs out (mirrors the host app's auto-submit trigger).
21
+ */
22
+ export declare function useExamCountdown({ durationMinutes, startedAt, enabled, onExpire, }: UseExamCountdownOptions): UseExamCountdownResult;
@@ -0,0 +1 @@
1
+ "use client";import{useEffect as e,useRef as t,useState as r}from"react";export function useExamCountdown({durationMinutes:n,startedAt:o,enabled:c,onExpire:u}){const[a,i]=r(0),s=t(!1),d=t(u);return d.current=u,e(()=>{if(!c||!o)return;const e=new Date(o).getTime()+6e4*n;s.current=!1;const t=()=>{const t=Math.max(0,Math.floor((e-Date.now())/1e3));i(t),t<=0&&!s.current&&(s.current=!0,d.current())};t();const r=setInterval(t,1e3);return()=>clearInterval(r)},[n,c,o]),{remainingSeconds:a,isExpired:s.current}}
@@ -0,0 +1,21 @@
1
+ export type WarningLevel = 'none' | 'warning' | 'critical';
2
+ export interface WarningThreshold {
3
+ seconds: number;
4
+ level: WarningLevel;
5
+ callback?: () => void;
6
+ }
7
+ interface UseExamTimerOptions {
8
+ onTimeExpired: () => void;
9
+ /** @deprecated Use warningThresholds[] instead */
10
+ onWarningThreshold?: () => void;
11
+ /** @deprecated Use warningThresholds[] instead */
12
+ warningThresholdSeconds?: number;
13
+ /** Multiple warning thresholds for TOEIC (15min + 5min). Highest severity wins. */
14
+ warningThresholds?: WarningThreshold[];
15
+ }
16
+ interface UseExamTimerReturn {
17
+ remainingSeconds: number;
18
+ warningLevel: WarningLevel;
19
+ }
20
+ export declare function useExamTimer(options: UseExamTimerOptions): UseExamTimerReturn;
21
+ export {};
@@ -0,0 +1 @@
1
+ import{useEffect as n,useRef as e}from"react";import{useExamTakingStore as r}from"../../../../shared/lib/stores/examTakingStore";export function useExamTimer(t){const i=r(),c=e(null),o=e(!1),s=(()=>{const n=t.warningThresholds??[];if(0===n.length&&void 0!==t.warningThresholdSeconds){const n=[];return t.onWarningThreshold&&n.push({seconds:t.warningThresholdSeconds,level:"critical",callback:t.onWarningThreshold}),n}return n})(),l=e(new Set),a=e(t.onTimeExpired);a.current=t.onTimeExpired,n(()=>{if(!i.isInitialized)return;c.current&&clearInterval(c.current),o.current=!1,l.current=new Set;const n=()=>{const n=i.syncRemainingSeconds();for(const e of s)n<=e.seconds&&!l.current.has(e.seconds)&&(l.current.add(e.seconds),e.callback?.());n<=0&&(c.current&&(clearInterval(c.current),c.current=null),o.current||(o.current=!0,a.current()))};c.current=setInterval(n,1e3);const e=()=>{"visible"===document.visibilityState&&n()};return document.addEventListener("visibilitychange",e),window.addEventListener("focus",e),()=>{c.current&&(clearInterval(c.current),c.current=null),document.removeEventListener("visibilitychange",e),window.removeEventListener("focus",e)}},[i.isInitialized]);const u=function(n,e){let r="none";for(const t of e)if(n<=t.seconds&&"none"!==t.level){if("critical"===t.level)return"critical";"warning"===t.level&&(r="warning")}return r}(i.remainingSeconds,s);return{remainingSeconds:i.remainingSeconds,warningLevel:u}}
@@ -0,0 +1,19 @@
1
+ import type { ExamTakingSdkApi } from '../../../../api/exam-taking';
2
+ interface UseProctoringCameraProps {
3
+ /** Injected API adapter (replaces the host app's `attemptsApi`). */
4
+ api: ExamTakingSdkApi;
5
+ attemptId: string | null;
6
+ intervalMs?: number;
7
+ /**
8
+ * Gate camera capture. The host app previously gated on
9
+ * `NEXT_PUBLIC_ENABLE_PROCTORING_CAMERA`; expose it as a prop so the SDK does
10
+ * not read host env vars. Defaults to `false` (disabled).
11
+ */
12
+ enabled?: boolean;
13
+ }
14
+ export declare function useProctoringCamera({ api, attemptId, intervalMs, enabled, }: UseProctoringCameraProps): {
15
+ isCameraActive: boolean;
16
+ cameraError: string;
17
+ snapshotCount: number;
18
+ };
19
+ export {};
@@ -0,0 +1 @@
1
+ import{useEffect as e,useRef as r,useState as t}from"react";export function useProctoringCamera({api:n,attemptId:a,intervalMs:o=1e4,enabled:c=!1}){const[i,u]=t(!1),[l,s]=t(null),[d,m]=t(0),p=r(null),h=r(null),g=r(null);return e(()=>{if("undefined"==typeof window)return;if(!c)return;let e=!0;return(async()=>{try{const r=await navigator.mediaDevices.getUserMedia({video:{facingMode:"user"},audio:!1});if(!e)return void r.getTracks().forEach(e=>e.stop());h.current=r;const t=document.createElement("video");t.srcObject=r,t.muted=!0,t.playsInline=!0,t.onloadedmetadata=()=>{e&&(t.play().catch(console.error),u(!0),s(null))},p.current=t}catch(r){console.error("Camera permission denied or error:",r),e&&(u(!1),s(r.message||"Camera access denied"))}})(),()=>{e=!1,h.current&&(h.current.getTracks().forEach(e=>e.stop()),h.current=null),u(!1),p.current=null}},[c]),e(()=>{if(!a||!i||!p.current)return void(g.current&&(clearInterval(g.current),g.current=null));return g.current=setInterval(async()=>{const e=p.current;if(e&&e.videoWidth)try{const r=document.createElement("canvas");r.width=e.videoWidth,r.height=e.videoHeight;const t=r.getContext("2d");if(!t)return;t.drawImage(e,0,0,r.width,r.height),r.toBlob(async e=>{if(e)try{await(n.uploadProctoringSnapshot?.({attemptId:a,imageBlob:e})),m(e=>e+1)}catch(e){console.error("Failed to upload proctoring snapshot:",e)}},"image/jpeg",.6)}catch(e){console.error("Error in capture process:",e)}},o),()=>{g.current&&(clearInterval(g.current),g.current=null)}},[n,a,i,o]),{isCameraActive:i,cameraError:l,snapshotCount:d}}
@@ -0,0 +1,25 @@
1
+ import type { ExamTakingSdkApi } from '../../../../api/exam-taking';
2
+ export interface UseProctoringSnapshotOptions {
3
+ /** API adapter; snapshots are skipped when `uploadProctoringSnapshot` is undefined. */
4
+ api: ExamTakingSdkApi;
5
+ /** Active attempt id, or null while loading. */
6
+ attemptId: string | null;
7
+ /** Whether proctoring is active (attempt loaded, room requires proctoring). */
8
+ enabled: boolean;
9
+ /** Interval between snapshot uploads in milliseconds. */
10
+ intervalMs?: number;
11
+ /** Notified when a snapshot capture/upload fails (best-effort logging). */
12
+ onError?: (error: unknown) => void;
13
+ }
14
+ export interface UseProctoringSnapshotResult {
15
+ /** Attach to a `<video>` element rendering the proctoring webcam stream. */
16
+ videoRef: React.RefObject<HTMLVideoElement | null>;
17
+ /** `true` once the webcam stream is live and snapshots are being captured. */
18
+ isActiveRef: React.MutableRefObject<boolean>;
19
+ }
20
+ /**
21
+ * Captures periodic webcam snapshots and uploads them for proctoring. Owns the
22
+ * camera stream lifecycle (request on enable, stop on cleanup) and encodes
23
+ * frames off a hidden canvas, mirroring the host app's proctoring loop.
24
+ */
25
+ export declare function useProctoringSnapshot({ api, attemptId, enabled, intervalMs, onError, }: UseProctoringSnapshotOptions): UseProctoringSnapshotResult;
@@ -0,0 +1 @@
1
+ "use client";import{useEffect as t,useRef as e}from"react";export function useProctoringSnapshot({api:r,attemptId:n,enabled:a,intervalMs:i=6e4,onError:o}){const c=e(null),s=e(!1),u=e(o);return u.current=o,t(()=>{if(!a||!n||!r.uploadProctoringSnapshot)return;if("undefined"==typeof navigator||!navigator.mediaDevices?.getUserMedia)return;let t=null,e=null,o=!1;const d=async()=>{const t=c.current;if(!t||0===t.videoWidth||!r.uploadProctoringSnapshot)return;const e=document.createElement("canvas");e.width=t.videoWidth,e.height=t.videoHeight;const a=e.getContext("2d");if(!a)return;a.drawImage(t,0,0,e.width,e.height);const i=await new Promise(t=>e.toBlob(t,"image/jpeg",.7));if(i)try{await r.uploadProctoringSnapshot({attemptId:n,imageBlob:i})}catch(t){u.current?.(t)}};return(async()=>{try{if(t=await navigator.mediaDevices.getUserMedia({video:!0,audio:!1}),o)return void t.getTracks().forEach(t=>t.stop());c.current&&(c.current.srcObject=t,await c.current.play().catch(()=>{})),s.current=!0,d(),e=setInterval(()=>{d()},i)}catch(t){u.current?.(t)}})(),()=>{o=!0,s.current=!1,e&&clearInterval(e),t&&t.getTracks().forEach(t=>t.stop())}},[r,n,a,i]),{videoRef:c,isActiveRef:s}}
@@ -0,0 +1,19 @@
1
+ export type ConversationPlaybackPhase = 'idle' | 'playing-interlocutor' | 'playing-partner' | 'ready';
2
+ export interface UseSpeakingConversationAudioOptions {
3
+ /** S3 key or direct URL for the interlocutor's audio */
4
+ interlocutorAudioUrl?: string;
5
+ /** S3 key or direct URL for the partner's audio */
6
+ partnerAudioUrl?: string;
7
+ /** Called once when the full sequence has finished */
8
+ onSequenceComplete: () => void;
9
+ }
10
+ export interface UseSpeakingConversationAudioReturn {
11
+ phase: ConversationPlaybackPhase;
12
+ isPlaying: boolean;
13
+ replayAll: () => void;
14
+ togglePlayPause: () => void;
15
+ currentAudioUrl: string;
16
+ currentTime: number;
17
+ duration: number;
18
+ }
19
+ export declare function useSpeakingConversationAudio({ interlocutorAudioUrl, partnerAudioUrl, onSequenceComplete, }: UseSpeakingConversationAudioOptions): UseSpeakingConversationAudioReturn;
@@ -0,0 +1 @@
1
+ import{useCallback as e,useEffect as r,useRef as t,useState as n}from"react";import{usePresignedFileUrl as u}from"../../../../shared/lib/hooks/usePresignedFileUrl";export function useSpeakingConversationAudio({interlocutorAudioUrl:a,partnerAudioUrl:i,onSequenceComplete:o}){const[c,l]=n("idle"),[d,s]=n(0),[p,y]=n(0),{previewUrl:m}=u(a??""),{previewUrl:g}=u(i??""),v=t(null),A=t(o);r(()=>{A.current=o},[o]);const E=t(m),L=t(g);r(()=>{E.current=m},[m]),r(()=>{L.current=g},[g]);const U=e(()=>{const e=v.current;e&&(e.pause(),e.src="")},[]),h=e(e=>{let r=v.current;r||(r=new Audio,v.current=r),r.src=e,r.play().catch(()=>{})},[]);r(()=>{let e=v.current;e||(e=new Audio,v.current=e);const r=()=>{l(e=>{if("playing-interlocutor"===e){const e=L.current;e?setTimeout(()=>{l("playing-partner"),h(e)},0):setTimeout(()=>{l("ready"),A.current()},0)}else"playing-partner"===e&&setTimeout(()=>{l("ready"),A.current()},0);return e})},t=()=>{e&&s(e.currentTime)},n=()=>{e&&(y(e.duration),s(0))};return e.addEventListener("ended",r),e.addEventListener("timeupdate",t),e.addEventListener("loadedmetadata",n),()=>{e.removeEventListener("ended",r),e.removeEventListener("timeupdate",t),e.removeEventListener("loadedmetadata",n)}},[h]),r(()=>(U(),l("idle"),m||g?(m?(l("playing-interlocutor"),h(m)):g&&(l("playing-partner"),h(g)),()=>{U()}):(l("ready"),void A.current())),[m,g]),r(()=>()=>{U()},[]);const T=e(()=>{U(),E.current?(l("playing-interlocutor"),h(E.current)):L.current?(l("playing-partner"),h(L.current)):(l("ready"),A.current())},[U,h]),f=e(()=>{const e=v.current;e&&(e.paused?e.play().catch(()=>{}):e.pause())},[]);return{phase:c,isPlaying:Boolean(v.current&&!v.current.paused),replayAll:T,togglePlayPause:f,currentAudioUrl:"playing-interlocutor"===c?m??"":"playing-partner"===c?g??"":"",currentTime:d,duration:p}}
@@ -0,0 +1,37 @@
1
+ import type { ApiQuestion } from '../utils/question-transformers';
2
+ /**
3
+ * A speaking question enriched with part metadata.
4
+ * Extends ApiQuestion with part display info and a stable exam-wide index.
5
+ */
6
+ export interface SpeakingQuestion extends ApiQuestion {
7
+ partName: string;
8
+ /** 0-based index within the full flattened exam question list */
9
+ questionIndex: number;
10
+ }
11
+ export interface UseSpeakingNavigationOptions {
12
+ questions: SpeakingQuestion[];
13
+ initialIndex?: number;
14
+ }
15
+ export interface UseSpeakingNavigationReturn {
16
+ /** 0-based index of the currently displayed question */
17
+ currentIndex: number;
18
+ /** Currently displayed question, or null if questions list is empty */
19
+ currentQuestion: SpeakingQuestion | null;
20
+ /** Total number of questions across all parts */
21
+ totalQuestions: number;
22
+ /** part_no of the current question */
23
+ currentPartNo: number;
24
+ /** Display name of the current part (e.g. "Part 1") */
25
+ currentPartName: string;
26
+ /**
27
+ * 1-based question number within the current part.
28
+ * Used for "CÂU HỎI SỐ [N]" display label.
29
+ */
30
+ questionNumberInPart: number;
31
+ canGoPrev: boolean;
32
+ canGoNext: boolean;
33
+ goNext: () => void;
34
+ goPrev: () => void;
35
+ goTo: (index: number) => void;
36
+ }
37
+ export declare function useSpeakingNavigation(options: UseSpeakingNavigationOptions): UseSpeakingNavigationReturn;
@@ -0,0 +1 @@
1
+ import{useState as t,useCallback as n,useMemo as r}from"react";export function useSpeakingNavigation(e){const{questions:o,initialIndex:a=0}=e,[i,u]=t(a),c=o.length,s=o[i]??null,l=s?.part_no??1,g=s?.partName??"",m=r(()=>{if(!s)return 1;return o.slice(0,i).filter(t=>t.part_no===s.part_no).length+1},[i,s,o]),p=i>0,N=i<c-1,x=n(()=>{u(t=>Math.min(t+1,c-1))},[c]),P=n(()=>{u(t=>Math.max(t-1,0))},[]),f=n(t=>{t>=0&&t<c&&u(t)},[c]);return{currentIndex:i,currentQuestion:s,totalQuestions:c,currentPartNo:l,currentPartName:g,questionNumberInPart:m,canGoPrev:p,canGoNext:N,goNext:x,goPrev:P,goTo:f}}
@@ -1,8 +1,48 @@
1
- export type SpeakingRecorderStatus = 'idle' | 'recording' | 'recorded' | 'uploading' | 'uploaded' | 'error';
2
- export declare function useSpeakingRecorder(_options?: unknown): {
3
- status: SpeakingRecorderStatus;
4
- previewUrl: string;
1
+ export type RecordingStatus = 'idle' | 'recording' | 'recorded' | 'uploading' | 'uploaded' | 'error';
2
+ export interface UseSpeakingRecorderOptions {
3
+ /** Attempt ID — needed for upload endpoint */
4
+ attemptId: string;
5
+ /** Question ID — needed for upload endpoint */
6
+ questionId: string;
7
+ /** Max recording duration in ms (default: 30000) */
8
+ maxDurationMs?: number;
9
+ /** Called with S3 key when upload completes */
10
+ onRecordingComplete: (audioKey: string, forQuestionId?: string) => void;
11
+ /** Called on any non-retryable error */
12
+ onError?: (error: string) => void;
13
+ /** Existing S3 key from a previous recording (used to restore playback on navigation) */
14
+ existingAnswerKey?: string;
15
+ }
16
+ export interface UseSpeakingRecorderReturn {
17
+ /** Current recording status */
18
+ status: RecordingStatus;
19
+ /** Countdown while recording (counts down from maxDurationMs/1000) */
5
20
  remainingSeconds: number;
21
+ /** True when remainingSeconds <= 5 */
22
+ isWarning: boolean;
23
+ /** Elapsed seconds while recording */
24
+ durationSeconds: number;
25
+ /** Local blob URL for playback preview (available after recording stops) */
26
+ previewUrl: string | null;
27
+ /** Uploaded S3 key (available after upload completes) */
28
+ uploadedKey: string | null;
29
+ /** Error message if status === 'error' */
30
+ error: string | null;
31
+ /** Whether the browser supports MediaRecorder API */
32
+ isSupported: boolean;
33
+ /** True when attempt has expired — caller should redirect */
34
+ isAttemptExpired: boolean;
35
+ /** Live transcript text (STREAMING mode only — null in RECORDING mode) */
36
+ liveTranscript: string | null;
37
+ /** Start recording. Requests microphone permission if not granted. */
6
38
  startRecording: () => Promise<void>;
39
+ /** Stop recording. Automatically triggers upload. */
7
40
  stopRecording: () => void;
8
- };
41
+ /** Discard current recording and reset to idle */
42
+ resetRecording: () => void;
43
+ /** Re-record: discard current recording and start a new one */
44
+ reRecord: () => Promise<void>;
45
+ /** Retry upload with the existing blob (no re-record required) */
46
+ retryUpload: () => Promise<void>;
47
+ }
48
+ export declare function useSpeakingRecorder(options: UseSpeakingRecorderOptions): UseSpeakingRecorderReturn;
@@ -1 +1 @@
1
- "use client";import{useRef as e,useState as r}from"react";export function useSpeakingRecorder(t){const n=e(null),a=e([]),[o,c]=r("idle"),[i,s]=r(null);return{status:o,previewUrl:i,remainingSeconds:0,startRecording:async()=>{const e=await navigator.mediaDevices.getUserMedia({audio:!0});a.current=[];const r=new MediaRecorder(e);n.current=r,r.ondataavailable=e=>a.current.push(e.data),r.onstop=()=>{e.getTracks().forEach(e=>e.stop()),s(URL.createObjectURL(new Blob(a.current,{type:"audio/webm"}))),c("recorded")},r.start(),c("recording")},stopRecording:()=>n.current?.stop()}}
1
+ import{useState as r,useRef as e,useEffect as n,useCallback as t}from"react";import{usePresignedFileUrl as u}from"../../../../shared/lib/hooks";import{useExamTakingApi as o}from"../ExamTakingApiContext";export function useSpeakingRecorder(i){const c=i.maxDurationMs??3e4,a=Math.floor(c/1e3),d=o(),s=e(d);s.current=d;const l=e(i.onRecordingComplete),p=e(i.onError);l.current=i.onRecordingComplete,p.current=i.onError;const g=e(i.attemptId),m=e(i.questionId);g.current=i.attemptId,m.current=i.questionId;const[h,y]=r("idle"),[w,f]=r(a),[A,v]=r(0),[R,T]=r(null),[E,x]=r(null),[I,b]=r(null),[K,M]=r(!1),k=e(null),P=e(null),D=e([]),S=e(null),U=e(null),O=e(""),_=e(0),q=e(null),L=e(void 0),B=e(0),V=e(!0),C=e(""),G="undefined"!=typeof MediaRecorder,{previewUrl:j}=u(i.existingAnswerKey),N=t(()=>{S.current&&(clearInterval(S.current),S.current=null)},[]),F=t(()=>{U.current&&(U.current.startsWith("blob:")&&URL.revokeObjectURL(U.current),U.current=null)},[]),W=t(()=>{P.current&&(P.current.getTracks().forEach(r=>r.stop()),P.current=null)},[]),X=t(async(r,e,n,t)=>{if(!V.current)return;const u=t||C.current||m.current;m.current===u&&y("uploading");const o=g.current;if("mock-attempt-speaking"===o){if(await new Promise(r=>setTimeout(r,500)),!V.current)return;const r=`mock-audio-key-${u}`;return m.current===u&&(y("uploaded"),x(r)),l.current(r,u),q.current=null,void(L.current=void 0)}try{if(!s.current.uploadAudioAnswer)throw new Error("uploadAudioAnswer is not implemented by the provided API adapter.");const n=(i=await s.current.uploadAudioAnswer({attemptId:o,questionId:u,audioBlob:r,durationMs:e}))&&"object"==typeof i&&"data"in i?i.data:i;if(!V.current)return;const t=n.audioKey??"";m.current===u&&(y("uploaded"),x(t)),l.current(t,u),q.current=null,L.current=void 0,B.current=0,s.current.realtimeGradeSingle?.({attemptId:o,questionId:u,audioKey:t}).catch(r=>{console.debug("[Realtime Grading] Skipped or failed:",r?.response?.status??r?.message)})}catch(u){if(!V.current)return;const o=u,i=o?.response?.data?.code;if("ATTEMPT_EXPIRED"===i||"ATTEMPT_NOT_IN_PROGRESS"===i){const r="ATTEMPT_EXPIRED"===i?"Bài thi đã hết giờ":"Bài thi đã được nộp.";return y("error"),b(r),M(!0),void p.current?.(r)}if("AUDIO_FILE_TOO_LARGE"===i){const r="Bài ghi âm quá dài. Vui lòng ghi âm ngắn hơn.";return y("error"),b(r),void p.current?.(r)}if(n<3){const u=1e3*Math.pow(2,n);if(await new Promise(r=>setTimeout(r,u)),!V.current)return;B.current=n+1,await X(r,e,n+1,t)}else{const r="Tải lên bản ghi âm thất bại. Vui lòng thử lại.";y("error"),b(r),p.current?.(r)}}var i},[]),z=t(async(r,e,n)=>{q.current=r,L.current=e,B.current=0,await X(r,e,0,n)},[X]),$=t(async()=>{if(i.existingAnswerKey)return y("uploaded"),void x(i.existingAnswerKey);if(!G){const r="Trình duyệt này không hỗ trợ ghi âm. Vui lòng dùng Chrome, Firefox hoặc Edge.";return y("error"),b(r),void p.current?.(r)}const r=function(){if("undefined"==typeof MediaRecorder)return null;const r=["audio/webm;codecs=opus","audio/webm","audio/mp4","audio/ogg;codecs=opus","audio/ogg"];for(const e of r)if(MediaRecorder.isTypeSupported(e))return e;return null}();if(!r){const r="Trình duyệt không hỗ trợ định dạng ghi âm.";return y("error"),b(r),void p.current?.(r)}try{const e=await navigator.mediaDevices.getUserMedia({audio:!0});P.current=e,O.current=r;const n=new MediaRecorder(e,{mimeType:r});k.current=n,D.current=[],n.ondataavailable=r=>{r.data.size>0&&D.current.push(r.data)},n.onstop=()=>{const r=Date.now()-_.current,e=new Blob(D.current,{type:O.current}),n=URL.createObjectURL(e),t=C.current;F(),U.current=n,T(n),y("recorded"),z(e,r,t)},n.start(),_.current=Date.now(),C.current=m.current,y("recording"),v(0),f(a),b(null),x(null),T(null),M(!1),S.current=setInterval(()=>{v(r=>r+1),f(r=>{const e=Math.max(0,r-1);return 0===e&&(N(),k.current&&"recording"===k.current.state&&k.current.stop(),W()),e})},1e3)}catch(r){W();const e=r instanceof DOMException&&("NotAllowedError"===r.name||"PermissionDeniedError"===r.name)?"Vui lòng cấp quyền truy cập microphone trong trình duyệt.":"Không thể truy cập microphone. Vui lòng thử lại.";y("error"),b(e),p.current?.(e)}},[G,a,i.existingAnswerKey,N,W,z,F]),H=t(()=>{N(),k.current&&"recording"===k.current.state&&k.current.stop(),W()},[N,W]),J=t(()=>{N(),W(),k.current&&"recording"===k.current.state&&k.current.stop(),k.current=null,D.current=[],q.current=null,L.current=void 0,B.current=0,F(),T(null),x(null),v(0),f(a),b(null),y("idle"),M(!1)},[N,W,F,a]),Q=t(async()=>{if(i.existingAnswerKey)return y("uploaded"),void x(i.existingAnswerKey);J(),await $()},[i.existingAnswerKey,J,$]),Y=t(async()=>{const r=q.current;r&&(B.current=0,await X(r,L.current,0,C.current||void 0))},[X]);n(()=>{J()},[i.questionId]),n(()=>{"idle"===h&&j&&i.existingAnswerKey&&!R&&(T(j),U.current=j,x(i.existingAnswerKey),y("uploaded"))},[h,j,i.existingAnswerKey,R]),n(()=>(V.current=!0,()=>{V.current=!1,N(),k.current&&"recording"===k.current.state&&k.current.stop(),W(),F()}),[]);return{status:h,remainingSeconds:w,isWarning:w<=5&&"recording"===h,durationSeconds:A,previewUrl:R,uploadedKey:E,error:I,isSupported:G,isAttemptExpired:K,liveTranscript:null,startRecording:$,stopRecording:H,resetRecording:J,reRecord:Q,retryUpload:Y}}
@@ -0,0 +1,39 @@
1
+ import { type SpeakingQuestion } from './useSpeakingNavigation';
2
+ import type { QuestionPart } from '../utils/question-transformers';
3
+ export interface UseSpeakingRendererSetupOptions {
4
+ part: QuestionPart;
5
+ answers: Record<string, unknown>;
6
+ onAnswerChange: (questionId: string, value: unknown) => void;
7
+ onPartComplete?: () => void;
8
+ onPartBack?: () => void;
9
+ maxDurationMs?: number;
10
+ hasMic?: boolean;
11
+ initialQuestionIndex?: number;
12
+ currentQuestionIndex?: number;
13
+ }
14
+ export declare function useSpeakingRendererSetup({ part, answers, onAnswerChange, onPartComplete, onPartBack, maxDurationMs, hasMic, initialQuestionIndex, currentQuestionIndex, }: UseSpeakingRendererSetupOptions): {
15
+ nav: {
16
+ showPrev: boolean;
17
+ showNext: boolean;
18
+ canGoPrev: boolean;
19
+ canGoNext: boolean;
20
+ currentIndex: number;
21
+ currentQuestion: SpeakingQuestion | null;
22
+ totalQuestions: number;
23
+ currentPartNo: number;
24
+ currentPartName: string;
25
+ questionNumberInPart: number;
26
+ goNext: () => void;
27
+ goPrev: () => void;
28
+ goTo: (index: number) => void;
29
+ };
30
+ recorder: import("./useSpeakingRecorder").UseSpeakingRecorderReturn;
31
+ currentQuestion: SpeakingQuestion;
32
+ handleNext: () => void;
33
+ handlePrev: () => void;
34
+ showExistingBadge: boolean;
35
+ isRecordingInProgress: boolean;
36
+ isQuestionLocked: boolean;
37
+ isPostRecordingDelayActive: boolean;
38
+ onAudioPlayingChange: (playing: boolean) => void;
39
+ };
@@ -0,0 +1 @@
1
+ import{useEffect as e,useMemo as r,useCallback as t,useRef as n,useState as o}from"react";import{toast as i}from"../../../../shared/lib/toast";import{useExamTakingStore as s}from"../../../../shared/lib/stores/examTakingStore";import{useSpeakingNavigation as a}from"./useSpeakingNavigation";import{useSpeakingRecorder as c}from"./useSpeakingRecorder";export function useSpeakingRendererSetup({part:u,answers:d,onAnswerChange:m,onPartComplete:l,onPartBack:p,maxDurationMs:x=3e4,hasMic:g=!0,initialQuestionIndex:v=0,currentQuestionIndex:h}){const P=r(()=>u.questions.map((e,r)=>({...e,partName:u.name||`Part ${e.part_no}`,questionIndex:r})),[u.questions,u.name]),I=a({questions:P,initialIndex:v});e(()=>{void 0!==h&&h>=0&&h!==I.currentIndex&&I.goTo(h)},[h,I.currentIndex,I]);const N=I.currentQuestion,{attemptId:G}=s(),f=N?d[N.id]:void 0,k=!!f,w=c({attemptId:G??"",questionId:N?.id??"",maxDurationMs:x,existingAnswerKey:g?f:void 0,onRecordingComplete:(e,r)=>{const t=r||N?.id;t&&m(t,e)},onError:e=>i.error(e)});e(()=>{g&&w.isAttemptExpired&&i.error("Bài thi đã hết giờ hoặc đã được nộp.")},[g,w.isAttemptExpired]);const A=g&&"recording"===w.status,T=n(A),q=n(null),[Q,R]=o(!1);e(()=>{g?(T.current&&!A&&(R(!0),q.current&&clearTimeout(q.current),q.current=setTimeout(()=>{R(!1),q.current=null},500)),A&&q.current&&(clearTimeout(q.current),q.current=null,R(!1)),T.current=A):R(!1)},[g,A]),e(()=>()=>{q.current&&clearTimeout(q.current)},[]);const[S,C]=o(!1),D=t(e=>{C(e)},[]);e(()=>{C(!1)},[N?.id]);const E=A||Q||S,{setIsNavDisabled:b}=s();e(()=>(b(E),()=>b(!1)),[E,b]);const y=t(()=>{E||(I.canGoNext?I.goNext():l?.())},[I,E,l]),B=t(()=>{E||(I.canGoPrev?I.goPrev():p?.())},[I,E,p]),M=r(()=>({...I,showPrev:I.canGoPrev||!!p,showNext:I.canGoNext||!!l,canGoPrev:!E&&(I.canGoPrev||!!p),canGoNext:!E&&(I.canGoNext||!!l)}),[I,E,p,l]),K=!(!f||g&&"idle"!==w.status);return{nav:M,recorder:g?w:null,currentQuestion:N,handleNext:y,handlePrev:B,showExistingBadge:K,isRecordingInProgress:A,isQuestionLocked:k,isPostRecordingDelayActive:Q,onAudioPlayingChange:D}}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Hook to manage TOEIC audio playback state across questions
3
+ *
4
+ * Features:
5
+ * - Track current audio URL for the part
6
+ * - Pause audio when switching questions
7
+ * - Reset audio state when part changes
8
+ * - Provide audio loading state
9
+ */
10
+ export declare function useToeicAudio(): {
11
+ currentAudioUrl: string;
12
+ isPlayingPartAudio: boolean;
13
+ isAudioLoading: boolean;
14
+ setPartAudio: (audioUrl: string | null) => void;
15
+ setAudioLoading: (loading: boolean) => void;
16
+ setAudioPlaying: (playing: boolean) => void;
17
+ };
@@ -0,0 +1 @@
1
+ import{useState as i,useCallback as o}from"react";export function useToeicAudio(){const[t,u]=i(null),[r,n]=i(!1),[d,e]=i(!1);return{currentAudioUrl:t,isPlayingPartAudio:r,isAudioLoading:d,setPartAudio:o(i=>{u(i),n(!1),e(!1)},[]),setAudioLoading:o(i=>{e(i)},[]),setAudioPlaying:o(i=>{n(i)},[])}}
@@ -3,4 +3,12 @@ export type { ExamTakingPageContainerProps } from './ExamTakingPageContainer';
3
3
  export { GenericQuestionRenderer } from './GenericQuestionRenderer';
4
4
  export type { GenericQuestionRendererProps } from './GenericQuestionRenderer';
5
5
  export type { ExamTakingPart } from './exam-taking.utils';
6
- export type { ExamTakingAnswerValue, ExamTakingAttempt, ExamTakingQuestion, ExamTakingQuestionsResult, ExamTakingSdkApi, ExamTakingTemplatePart, SaveExamTakingAnswersInput } from './types';
6
+ export type { ExamTakingAnswerValue, ExamTakingAttempt, ExamTakingQuestion, ExamTakingQuestionsResult, ExamTakingSdkApi, ExamTakingTemplatePart, SaveExamTakingAnswersInput } from '../../../api/exam-taking';
7
+ export { useExamCountdown } from './hooks/useExamCountdown';
8
+ export type { UseExamCountdownOptions, UseExamCountdownResult } from './hooks/useExamCountdown';
9
+ export { useAnswerAutosave } from './hooks/useAnswerAutosave';
10
+ export type { UseAnswerAutosaveOptions, UseAnswerAutosaveResult } from './hooks/useAnswerAutosave';
11
+ export { useProctoringSnapshot } from './hooks/useProctoringSnapshot';
12
+ export type { UseProctoringSnapshotOptions, UseProctoringSnapshotResult } from './hooks/useProctoringSnapshot';
13
+ export { useAttemptSubmit } from './hooks/useAttemptSubmit';
14
+ export type { UseAttemptSubmitOptions, UseAttemptSubmitResult, SubmittedSummary } from './hooks/useAttemptSubmit';
@@ -1 +1 @@
1
- export{ExamTakingPageContainer}from"./ExamTakingPageContainer";export{GenericQuestionRenderer}from"./GenericQuestionRenderer";
1
+ export{ExamTakingPageContainer}from"./ExamTakingPageContainer";export{GenericQuestionRenderer}from"./GenericQuestionRenderer";export{useExamCountdown}from"./hooks/useExamCountdown";export{useAnswerAutosave}from"./hooks/useAnswerAutosave";export{useProctoringSnapshot}from"./hooks/useProctoringSnapshot";export{useAttemptSubmit}from"./hooks/useAttemptSubmit";
@@ -0,0 +1,10 @@
1
+ export interface SpeakingCueCardContent {
2
+ /** Rich text HTML content of the cue card — generated by Tiptap Editor */
3
+ cueCardHtml: string;
4
+ /** Preparation time in seconds (default: 60) */
5
+ prepTimeSeconds?: number;
6
+ /** Max speaking time in seconds (default: 120) */
7
+ speakingTimeSeconds?: number;
8
+ /** Backup question flag */
9
+ isBackup?: boolean;
10
+ }