@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
package/README.md CHANGED
@@ -18,9 +18,11 @@ import {
18
18
  ExamQuestionsPageContainer,
19
19
  ExamTakingPageContainer,
20
20
  ResultReviewPageContainer,
21
+ StudentExamEntryPageContainer,
21
22
  } from '@tinyweb_dev/oe-exam-sdk';
22
23
  import {
23
24
  createExamCreateApi,
25
+ createExamEntryApi,
24
26
  createExamQuestionsApi,
25
27
  createExamTakingApi,
26
28
  createResultReviewApi,
@@ -47,12 +49,26 @@ export function ExamQuestionsPage({ examId, router }) {
47
49
  );
48
50
  }
49
51
 
50
- export function ExamTakePage({ examId, router }) {
52
+ export function ExamEnterPage({ roomId, inviteToken, router }) {
53
+ return (
54
+ <StudentExamEntryPageContainer
55
+ roomId={roomId}
56
+ inviteToken={inviteToken}
57
+ api={createExamEntryApi({ baseUrl })}
58
+ onNavigate={(href) => router.push(href)}
59
+ />
60
+ );
61
+ }
62
+
63
+ export function ExamTakePage({ roomId, router }) {
51
64
  return (
52
65
  <ExamTakingPageContainer
53
- examId={examId}
66
+ roomId={roomId}
54
67
  api={createExamTakingApi({ baseUrl })}
55
68
  onNavigate={(href) => router.replace(href)}
69
+ resultPath={(attemptId) => `/student/results/${attemptId}/review`}
70
+ onSubmitted={({ attemptId }) => router.replace(`/student/results/${attemptId}/review`)}
71
+ proctoringEnabled
56
72
  />
57
73
  );
58
74
  }
@@ -77,8 +93,9 @@ export function ResultReviewPage({ attemptId, token, router }) {
77
93
  @tinyweb_dev/oe-exam-sdk/shared # shared constants/types/helpers
78
94
  @tinyweb_dev/oe-exam-sdk/mcp # MCP server entry và tools
79
95
  @tinyweb_dev/oe-exam-sdk/components # shared UI components
80
- @tinyweb_dev/oe-exam-sdk/components/exams # màn tạo đề / câu hỏi / làm bài
81
- @tinyweb_dev/oe-exam-sdk/components/exams/take # màn làm bài + renderer generic
96
+ @tinyweb_dev/oe-exam-sdk/components/exams # màn tạo đề / câu hỏi / làm bài / vào phòng
97
+ @tinyweb_dev/oe-exam-sdk/components/exams/entry # màn vào phòng (entry) + state machine
98
+ @tinyweb_dev/oe-exam-sdk/components/exams/take # màn làm bài + renderer generic + hooks
82
99
  @tinyweb_dev/oe-exam-sdk/components/questions # hệ thống tạo câu hỏi
83
100
  @tinyweb_dev/oe-exam-sdk/components/themes # cambridge-yle, english-certification, summer-sky
84
101
  @tinyweb_dev/oe-exam-sdk/components/results # màn xem chi tiết kết quả
@@ -109,6 +126,40 @@ Các adapter mặc định nhận:
109
126
 
110
127
  `createExamTakingApi` đã bao gồm các API cần cho màn làm bài: start/resume attempt, questions, save answers, submit, upload audio, proctoring snapshot, realtime grading, grading summary/status/detail, room/attempt detail và `keepalive` save.
111
128
 
129
+ `createExamEntryApi` phục vụ màn vào phòng: room detail, active attempt theo room, start/delete attempt, join qua invite token và đăng ký contest (registration form).
130
+
131
+ ## Màn vào phòng (entry)
132
+
133
+ `StudentExamEntryPageContainer` port toàn bộ luồng `/student/exams/:roomId/enter`: join qua invite, gating quyền truy cập / đề thi / form đăng ký, state machine theo cửa sổ thời gian (BEFORE_START / WITHIN_WINDOW / EXPIRED / ALREADY_SUBMITTED), resume attempt đang dở và dialog "làm tiếp / làm lại từ đầu".
134
+
135
+ ```tsx
136
+ <StudentExamEntryPageContainer
137
+ roomId={roomId}
138
+ inviteToken={inviteToken}
139
+ api={createExamEntryApi({ baseUrl })}
140
+ onNavigate={(href) => router.push(href)}
141
+ takePath={(id) => `/student/exams/${id}/take`}
142
+ renderRegistrationForm={({ room, onSubmit, isSubmitting }) => (
143
+ <MyRegistrationDialog room={room} onSubmit={onSubmit} isSubmitting={isSubmitting} />
144
+ )}
145
+ />
146
+ ```
147
+
148
+ Themed waiting room (TOEIC / Summer Sky / Cambridge YLE) và registration form là phần phụ thuộc host app — truyền qua `renderRegistrationForm` hoặc bọc container.
149
+
150
+ ## Hooks màn làm bài
151
+
152
+ `@tinyweb_dev/oe-exam-sdk/components/exams/take` export các hook tái sử dụng:
153
+
154
+ ```txt
155
+ useExamCountdown # đếm ngược theo startedAt + duration, auto-submit khi hết giờ
156
+ useAnswerAutosave # lưu debounce theo batch + flush keepalive khi ẩn tab/unload + retry khi online
157
+ useProctoringSnapshot # chụp webcam định kỳ và upload snapshot
158
+ useAttemptSubmit # nộp bài (flush trước), chống double-submit, trả pending/auto-graded answer ids
159
+ ```
160
+
161
+ `ExamTakingPageContainer` đã nối sẵn các hook trên. `onSubmitted` nhận `{ attemptId, pendingAnswerIds }` để điều hướng sang màn review đúng ngữ cảnh; bật proctoring qua prop `proctoringEnabled` và truyền `achieversExamTitle` nếu cần gate video intro của giáo viên.
162
+
112
163
  ## Theme làm bài
113
164
 
114
165
  `ExamTakingPageContainer` tự chọn layout theo `examTemplate.theme`:
@@ -129,12 +180,15 @@ Renderer câu hỏi trong màn làm bài hiện là `GenericQuestionRenderer`.
129
180
  attemptId={attemptId}
130
181
  token={token}
131
182
  api={createResultReviewApi({ baseUrl })}
183
+ isPrintMode={isPrintMode}
132
184
  topActionsSlot={null}
133
185
  beforeReportSlot={null}
134
186
  afterReportSlot={null}
135
187
  />
136
188
  ```
137
189
 
190
+ Container tự poll lại kết quả khi còn câu đang chấm hoặc LLM assessment chưa sẵn sàng, dừng poll ở `isPrintMode` và phát tín hiệu `body[data-pdf-ready="true"]` cho headless PDF renderer. Lỗi 403 "kết quả chưa công khai" được nhận diện qua helper `isResultNotPublic` và hiển thị màn thông báo riêng thay vì toast lỗi.
191
+
138
192
  Các phần phụ thuộc app chính như gift dialog, upgrade modal, summary video/remotion nên truyền qua slot từ host app.
139
193
 
140
194
  ## MCP server
@@ -0,0 +1,13 @@
1
+ import type { ContestRoomListItem, ContestRoomsContestDetail, ContestRoomsFetchApiOptions, ContestRoomsSdkApi } from './contest-rooms.types';
2
+ /**
3
+ * Default fetch-based adapter for the student contest rooms-list flow. Mirrors
4
+ * the host app endpoints used by the original `/student/contests/:contestId/rooms`
5
+ * page: contest detail, room list and room registration.
6
+ */
7
+ export declare function createContestRoomsApi(options?: ContestRoomsFetchApiOptions): ContestRoomsSdkApi;
8
+ /** In-memory mock adapter for demo/storybook/tests. */
9
+ export declare function createMockContestRoomsApi(seed: {
10
+ contest: ContestRoomsContestDetail;
11
+ rooms: ContestRoomListItem[];
12
+ }): ContestRoomsSdkApi;
13
+ export type { ContestRoomListItem, ContestRoomRegistrationStatus, ContestRoomVisibility, ContestRoomsContestDetail, ContestRoomsFetchApiOptions, ContestRoomsSdkApi, } from './contest-rooms.types';
@@ -0,0 +1 @@
1
+ import{API_BASE_URL as t,API_ENDPOINTS as e}from"../shared/constants/ApiConstant";export function createContestRoomsApi(n={}){const o=function({baseUrl:e=t??"",fetcher:n=fetch,headers:o}){return async function(t,s){const r=await n(`${e}${t}`,{credentials:"include",...s,headers:{"Content-Type":"application/json",...o,...s?.headers}});if(!r.ok)throw new Error(await async function(t){const e=await t.json().catch(()=>{});if("object"==typeof e&&null!==e&&"message"in e&&"string"==typeof e.message)return e.message;return t.statusText}(r));if(204!==r.status)return r.json()}}(n);return{getContestDetail:t=>o(e.STUDENT.CONTEST_DETAIL(t)),getContestRooms:t=>o(e.STUDENT.CONTEST_ROOMS(t)),registerRoom:(t,n)=>o(e.STUDENT.CONTEST_ROOM_REGISTER(t,n),{method:"POST"}),cancelRoomRegistration:(t,n)=>o(e.STUDENT.CONTEST_ROOM_REGISTER(t,n),{method:"DELETE"})}}export function createMockContestRoomsApi(t){const e=[...t.rooms];return{getContestDetail:async()=>t.contest,getContestRooms:async()=>e,registerRoom:async(t,n)=>{const o=e.find(t=>t.id===n);o&&(o.registrationStatus="PENDING")},cancelRoomRegistration:async(t,n)=>{const o=e.find(t=>t.id===n);o&&(o.registrationStatus=null)}}}
@@ -0,0 +1,47 @@
1
+ import type { ApiData } from './exam-entry.types';
2
+ export type ContestRoomVisibility = 'public' | 'internal' | 'private';
3
+ export type ContestRoomRegistrationStatus = 'PENDING' | 'APPROVED' | 'REJECTED';
4
+ /** Contest summary needed by the rooms list header/gating. */
5
+ export interface ContestRoomsContestDetail {
6
+ id: string;
7
+ name: string;
8
+ status?: 'UPCOMING' | 'IN_PROGRESS' | 'COMPLETED';
9
+ allowMultipleSubmissions?: boolean | null;
10
+ }
11
+ /** Room item as listed on `/student/contests/:contestId/rooms`. */
12
+ export interface ContestRoomListItem {
13
+ id: string;
14
+ name: string;
15
+ code: string;
16
+ examName: string;
17
+ status: 'UPCOMING' | 'IN_PROGRESS' | 'COMPLETED';
18
+ startDatetime?: string;
19
+ endDatetime?: string;
20
+ isLocked: boolean;
21
+ hasAttempt: boolean;
22
+ attemptStatus?: string;
23
+ isCompleted?: boolean;
24
+ visibility?: ContestRoomVisibility;
25
+ registrationStatus?: ContestRoomRegistrationStatus | null;
26
+ registrationCount?: number;
27
+ allowMultipleSubmissions?: boolean | null;
28
+ }
29
+ /**
30
+ * API surface the rooms-list container depends on. Methods may return either
31
+ * the raw value or an `{ data }` envelope; the container unwraps both.
32
+ */
33
+ export interface ContestRoomsSdkApi {
34
+ /** Fetch contest detail (used for header + multi-submission gating). */
35
+ getContestDetail: (contestId: string) => Promise<ApiData<ContestRoomsContestDetail> | ContestRoomsContestDetail>;
36
+ /** Fetch the list of rooms for a contest. */
37
+ getContestRooms: (contestId: string) => Promise<ApiData<ContestRoomListItem[]> | ContestRoomListItem[]>;
38
+ /** Register for an internal-visibility room. */
39
+ registerRoom: (contestId: string, roomId: string) => Promise<unknown>;
40
+ /** Cancel a pending/approved room registration. */
41
+ cancelRoomRegistration?: (contestId: string, roomId: string) => Promise<unknown>;
42
+ }
43
+ export interface ContestRoomsFetchApiOptions {
44
+ baseUrl?: string;
45
+ fetcher?: typeof fetch;
46
+ headers?: HeadersInit;
47
+ }
@@ -0,0 +1 @@
1
+ export{};
@@ -0,0 +1,14 @@
1
+ import type { ExamEntryAttempt, ExamEntryFetchApiOptions, ExamEntryRoom, ExamEntrySdkApi } from './exam-entry.types';
2
+ /**
3
+ * Default fetch-based adapter for the student exam entry flow. Mirrors the host
4
+ * app endpoints used by the original `/student/exams/:roomId/enter` page:
5
+ * room detail, active attempt by room, start/delete attempt, invite join and
6
+ * contest registration.
7
+ */
8
+ export declare function createExamEntryApi(options?: ExamEntryFetchApiOptions): ExamEntrySdkApi;
9
+ /** In-memory mock adapter for demo/storybook/tests. */
10
+ export declare function createMockExamEntryApi(seed: {
11
+ room: ExamEntryRoom;
12
+ attempt?: ExamEntryAttempt | null;
13
+ }): ExamEntrySdkApi;
14
+ export type { ExamEntryAttempt, ExamEntryContest, ExamEntryExam, ExamEntryFetchApiOptions, ExamEntryRegistrationField, ExamEntryRegistrationFormConfig, ExamEntryRoom, ExamEntrySdkApi, } from './exam-entry.types';
@@ -0,0 +1 @@
1
+ import{API_BASE_URL as t,API_ENDPOINTS as e}from"../shared/constants/ApiConstant";export function createExamEntryApi(n={}){const o=function({baseUrl:e=t??"",fetcher:n=fetch,headers:o}){return async function(t,a){const r=await n(`${e}${t}`,{credentials:"include",...a,headers:{"Content-Type":"application/json",...o,...a?.headers}});if(!r.ok)throw new Error(await async function(t){const e=await t.json().catch(()=>{});if("object"==typeof e&&null!==e&&"message"in e&&"string"==typeof e.message)return e.message;return t.statusText}(r));if(204!==r.status)return r.json()}}(n);return{getRoom:t=>o(e.STUDENT.ROOM_DETAIL(t)),getActiveAttemptByRoom:t=>o(e.STUDENT.ATTEMPTS_ACTIVE_BY_ROOM(t)),startAttempt:t=>o(e.STUDENT.ATTEMPTS_START,{method:"POST",body:JSON.stringify({examRoomId:t})}),deleteAttempt:t=>o(e.STUDENT.ATTEMPT_DELETE(t),{method:"DELETE"}),joinViaInvite:(t,n)=>o(e.STUDENT.ROOM_JOIN_INVITE(t,n),{method:"POST"}),registerContest:(t,n)=>o(e.STUDENT.CONTEST_REGISTER(t),{method:"POST",body:JSON.stringify(n)})}}export function createMockExamEntryApi(t){let e=t.attempt??null;return{getRoom:async()=>t.room,getActiveAttemptByRoom:async()=>e,startAttempt:async()=>(e={id:`mock-attempt-${Date.now()}`,startedAt:(new Date).toISOString(),durationMinutes:t.room.exam?.durationMinutes??30,examRoomId:t.room.id,status:"IN_PROGRESS"},e),deleteAttempt:async()=>{e=null},joinViaInvite:async()=>{},registerContest:async()=>{}}}
@@ -0,0 +1,93 @@
1
+ import type { ExamTakingAttempt } from './exam-taking.types';
2
+ /** Generic envelope used by the OE Exam API ({ data: ... }). */
3
+ export interface ApiData<T> {
4
+ data: T;
5
+ }
6
+ /** Registration form field config for contest rooms. */
7
+ export interface ExamEntryRegistrationField {
8
+ id: string;
9
+ label: string;
10
+ type: string;
11
+ required: boolean;
12
+ placeholder?: string;
13
+ options?: string[];
14
+ userPropertyMapping?: string;
15
+ }
16
+ /** Registration form config attached to a room. */
17
+ export interface ExamEntryRegistrationFormConfig {
18
+ enabled: boolean;
19
+ fields: ExamEntryRegistrationField[];
20
+ }
21
+ /** Exam summary nested inside a room. */
22
+ export interface ExamEntryExam {
23
+ id: string;
24
+ name?: string;
25
+ description?: string;
26
+ durationMinutes?: number;
27
+ totalScore?: number;
28
+ passScore?: number;
29
+ level?: string;
30
+ theme?: string;
31
+ template?: {
32
+ theme?: string;
33
+ subTitle?: string;
34
+ title?: string;
35
+ };
36
+ }
37
+ /** Contest summary nested inside a room. */
38
+ export interface ExamEntryContest {
39
+ id: string;
40
+ code?: string | null;
41
+ name?: string;
42
+ status?: string;
43
+ }
44
+ /**
45
+ * Room as needed by the entry screen. Mirrors the fields the entry flow reads
46
+ * from `studentRoomsApi.getById` in the host app.
47
+ */
48
+ export interface ExamEntryRoom {
49
+ id: string;
50
+ name: string;
51
+ description?: string;
52
+ code?: string;
53
+ examId?: string;
54
+ startDatetime?: string;
55
+ endDatetime?: string;
56
+ isLocked?: boolean;
57
+ hasJoined?: boolean;
58
+ canAccess?: boolean;
59
+ allowMultipleSubmissions?: boolean;
60
+ hasSubmittedFormData?: boolean;
61
+ registrationFormConfig?: ExamEntryRegistrationFormConfig;
62
+ exam?: ExamEntryExam;
63
+ contest?: ExamEntryContest;
64
+ }
65
+ /** Attempt as needed by the entry screen. */
66
+ export type ExamEntryAttempt = ExamTakingAttempt;
67
+ /**
68
+ * API surface the entry container depends on. All methods may return either the
69
+ * raw value or an `{ data }` envelope; the container unwraps both.
70
+ */
71
+ export interface ExamEntrySdkApi {
72
+ /** Fetch room detail by id. */
73
+ getRoom: (roomId: string) => Promise<ApiData<ExamEntryRoom> | ExamEntryRoom>;
74
+ /** Fetch the active (IN_PROGRESS) attempt for a specific room, or null. */
75
+ getActiveAttemptByRoom: (roomId: string) => Promise<ApiData<ExamEntryAttempt | null> | ExamEntryAttempt | null>;
76
+ /** Start a new attempt for the room. */
77
+ startAttempt: (roomId: string) => Promise<ApiData<ExamEntryAttempt | {
78
+ attempt: ExamEntryAttempt;
79
+ }> | ExamEntryAttempt | {
80
+ attempt: ExamEntryAttempt;
81
+ }>;
82
+ /** Delete an attempt (used by "restart from scratch"). */
83
+ deleteAttempt: (attemptId: string) => Promise<unknown>;
84
+ /** Join a room via an invite token (best-effort; may already be a member). */
85
+ joinViaInvite?: (roomId: string, inviteToken: string) => Promise<unknown>;
86
+ /** Register for the contest backing the room (submits registration form data). */
87
+ registerContest?: (contestId: string, formData: Record<string, unknown>) => Promise<unknown>;
88
+ }
89
+ export interface ExamEntryFetchApiOptions {
90
+ baseUrl?: string;
91
+ fetcher?: typeof fetch;
92
+ headers?: HeadersInit;
93
+ }
@@ -0,0 +1 @@
1
+ export{};
@@ -6,4 +6,4 @@ export declare function createMockExamQuestionsApi(seed: {
6
6
  exam: ExamWithTemplate;
7
7
  questions?: Partial<ExamQuestion>[];
8
8
  }): ExamQuestionsSdkApi;
9
- export type { ExamQuestionsSdkApi, ExamWithTemplate, FetchApiOptions, SaveExamInput, SaveQuestionInput, UpdatePartAudioInput } from './exam-questions.types';
9
+ export type { ExamQuestionsSdkApi, ExamWithTemplate, ExistingQuestionBank, FetchApiOptions, QuestionSearchItem, SaveExamInput, SaveQuestionInput, SaveQuestionResult, SearchQuestionBankInput, SearchQuestionBankResult, UpdatePartAudioInput, } from './exam-questions.types';
@@ -1 +1 @@
1
- import{API_BASE_URL as t,API_ENDPOINTS as e}from"../shared/constants/ApiConstant";import{transformQuestionToApiFormat as a}from"../shared/lib/utils/question-transform";export function createExamQuestionsApi(s={}){const n=function({baseUrl:e=t??"",fetcher:a=fetch,headers:s,apiKey:n}){return async function(t,r){const i=n?{"X-API-Key":n}:void 0,o=await a(`${e}${t}`,{...n?{}:{credentials:"include"},...r,headers:{"Content-Type":"application/json",...i,...s,...r?.headers}});if(!o.ok){const t=await o.json().catch(()=>({}));throw new Error(t.message||o.statusText)}return o.json()}}(s);return{getExam:t=>n(e.MANAGE.EXAM_DETAIL(t)),getQuestions:async t=>{const a=await n(e.MANAGE.EXAM_QUESTIONS(t));return Array.isArray(a.data)?a.data:a.data.questions??[]},saveQuestion:async({examId:t,question:s,template:r})=>{if(!t)throw new Error("examId is required");const i=a(s,function(t,e){const a=e?.parts.findIndex(e=>e.id===t.partId)??-1,s=a>=0?a+1:void 0,{partId:n,questionNumber:r}=t,i=t.sectionId;return{partId:n,partNo:s,questionNumber:r,sectionId:i}}(s,r)),o=await n(e.MANAGE.EXAM_QUESTIONS(t),{method:"POST",body:JSON.stringify({questions:[i]})}),u=o.data.results?.[0]?.data?.id;return{...s,id:u??s.id,isCompleted:!0}},saveExam:async()=>{},updatePartAudio:t=>async function(t,{examId:a,partId:s,audioUrl:n,template:r}){await t(e.MANAGE.EXAM_TEMPLATE(a),{method:"PATCH",body:JSON.stringify({id:r.id,title:r.name,totalQuestions:r.totalQuestions,totalParts:r.parts.length,description:r.description||"",examLevel:r.level||"",type:r.type,parts:r.parts.map(t=>({...t,audioUrl:t.id===s?n:t.audioUrl}))})})}(n,t)}}export const createExamQuestionsFetchApi=createExamQuestionsApi;export function createMockExamQuestionsApi(t){const e=new Map((t.questions??[]).map(t=>[(t.questionNumber??1)-1,t]));let a=t.exam;return{getExam:async()=>({data:a}),getQuestions:async()=>Array.from(e.values()),saveQuestion:async({question:t,questionIndex:a})=>{const s={...t,questionNumber:t.questionNumber??a+1,isCompleted:!0};return e.set(a,s),s},saveExam:async()=>{},updatePartAudio:async({partId:t,audioUrl:e})=>{a.template&&"parts"in a.template&&(a={...a,template:{...a.template,parts:a.template.parts.map(a=>a.id===t?{...a,audioUrl:e}:a)}})}}}
1
+ import{API_BASE_URL as t,API_ENDPOINTS as e}from"../shared/constants/ApiConstant";import{transformQuestionToApiFormat as s}from"../shared/lib/utils/question-transform";import{transformApiQuestionsToFrontend as a}from"../shared/lib/utils/question-reverse-transform";export function createExamQuestionsApi(n={}){const i=function({baseUrl:e=t??"",fetcher:s=fetch,headers:a,apiKey:n}){return async function(t,i){const o=n?{"X-API-Key":n}:void 0,r=await s(`${e}${t}`,{...n?{}:{credentials:"include"},...i,headers:{"Content-Type":"application/json",...o,...a,...i?.headers}});if(!r.ok){const t=await r.json().catch(()=>({}));throw new Error(t.message||r.statusText)}return r.json()}}(n);return{getExam:t=>i(e.MANAGE.EXAM_DETAIL(t)),getQuestions:async t=>{const s=await i(e.MANAGE.EXAM_QUESTIONS(t)),n=Array.isArray(s.data)?s.data:s.data.questions??[];return a(n)},saveQuestion:async t=>{const{examId:a,question:n,template:o}=t;if(!a)throw new Error("examId is required");const r=s(n,function(t){const{question:e,template:s,addToQuestionBank:a,questionBankCode:n,questionBankTitle:i,sectionId:o,examLevel:r,subjectId:u}=t,d=s?.parts.findIndex(t=>t.id===e.partId)??-1,p=d>=0?d+1:void 0,c=e.sectionId;return{partId:e.partId,partNo:p,questionNumber:e.questionNumber,sectionId:o??c,addToQuestionBank:a,questionBankCode:n,questionBankTitle:i,examLevel:r,subjectId:u}}(t)),u=await i(e.MANAGE.EXAM_QUESTIONS(a),{method:"POST",body:JSON.stringify({examId:a,questions:[r]})});if((u.data.summary?.errorCount??0)>0)throw new Error("Unable to save question");const d=u.data.results?.[0]?.data,p={...n,id:d?.id??n.id,...d?.questionGroup?{questionGroup:d.questionGroup}:{},isCompleted:!0},c=(m=d?.questionBank,"object"!=typeof m||null===m||Array.isArray(m)?void 0:m);var m;return{question:p,questionBank:c?{id:String(c.id??""),code:String(c.code??""),title:String(c.title??"")}:void 0}},saveExam:async()=>{},updatePartAudio:t=>async function(t,{examId:s,partId:a,audioUrl:n,template:i}){await t(e.MANAGE.EXAM_TEMPLATE(s),{method:"PATCH",body:JSON.stringify({id:i.id,title:i.name,subTitle:"",theme:i.theme||"",totalQuestions:i.totalQuestions,totalParts:i.parts.length,description:i.description||"",examLevel:i.level||"",type:i.type,parts:i.parts.map(t=>({...t,audioUrl:t.id===a?n:t.audioUrl}))})})}(i,t),searchQuestions:async({questionType:t,keyword:s,page:a,size:n})=>{const o=new URLSearchParams;t&&o.set("questionType",t),s&&o.set("keyword",s),o.set("page",String(a)),o.set("size",String(n));const r=o.toString()?`?${o.toString()}`:"";return i(`${e.MANAGE.QUESTION_BANKS}${r}`)}}}export const createExamQuestionsFetchApi=createExamQuestionsApi;export function createMockExamQuestionsApi(t){const e=new Map((t.questions??[]).map(t=>[(t.questionNumber??1)-1,t]));let s=t.exam;return{getExam:async()=>({data:s}),getQuestions:async()=>Array.from(e.values()),saveQuestion:async({question:t,questionIndex:s})=>{const a={...t,questionNumber:t.questionNumber??s+1,isCompleted:!0};return e.set(s,a),{question:a}},saveExam:async()=>{},updatePartAudio:async({partId:t,audioUrl:e})=>{s.template&&"parts"in s.template&&(s={...s,template:{...s.template,parts:s.template.parts.map(s=>s.id===t?{...s,audioUrl:e}:s)}})},searchQuestions:async({questionType:t})=>({data:Array.from(e.values()).filter(e=>!t||e.type===t).map(t=>({id:t.id??String(t.questionNumber??""),questionType:t.type??"",content:t.content,correctAnswer:t.answer,points:t.points,difficulty:t.difficulty,skill:t.skill,explanation:t.explanation})),meta:{totalItems:e.size}})}}
@@ -1,10 +1,12 @@
1
+ import type { QuestionType } from '../shared/types/common.types';
1
2
  import type { ExamData, ExamPart, ExamQuestion, ExamTemplate } from '../shared/types/entities/exam.types';
2
3
  export interface ExamQuestionsSdkApi {
3
4
  getExam: (examId: string) => Promise<ExamQuestionsApiResponse<ExamWithTemplate> | ExamWithTemplate>;
4
5
  getQuestions?: (examId: string) => Promise<ExamQuestionsApiResponse<Partial<ExamQuestion>[]> | Partial<ExamQuestion>[]>;
5
- saveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | void>;
6
+ saveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | SaveQuestionResult | void>;
6
7
  saveExam?: (input: SaveExamInput) => Promise<void>;
7
8
  updatePartAudio?: (input: UpdatePartAudioInput) => Promise<void>;
9
+ searchQuestions?: (input: SearchQuestionBankInput) => Promise<SearchQuestionBankResult>;
8
10
  }
9
11
  export interface ExamQuestionsApiResponse<T> {
10
12
  data: T;
@@ -24,6 +26,7 @@ export interface EmbeddedExamTemplate extends Omit<ExamTemplate, 'name' | 'level
24
26
  title: string;
25
27
  subTitle?: string;
26
28
  examLevel: string;
29
+ totalParts?: number;
27
30
  parts: Array<ExamPart & {
28
31
  title?: string;
29
32
  }>;
@@ -36,6 +39,20 @@ export interface SaveQuestionInput {
36
39
  examDifficulty?: string;
37
40
  examLevel?: string;
38
41
  subjectId?: string;
42
+ sectionId?: number;
43
+ addToQuestionBank?: boolean;
44
+ questionBankCode?: string;
45
+ questionBankTitle?: string;
46
+ existingQuestionBankId?: string;
47
+ }
48
+ export interface SaveQuestionResult {
49
+ question: Partial<ExamQuestion>;
50
+ questionBank?: ExistingQuestionBank | null;
51
+ }
52
+ export interface ExistingQuestionBank {
53
+ id: string;
54
+ code: string;
55
+ title: string;
39
56
  }
40
57
  export interface SaveExamInput {
41
58
  examId: string;
@@ -49,6 +66,33 @@ export interface UpdatePartAudioInput {
49
66
  audioUrl: string;
50
67
  template: ExamTemplate;
51
68
  }
69
+ export interface SearchQuestionBankInput {
70
+ questionType?: QuestionType;
71
+ keyword?: string;
72
+ page: number;
73
+ size: number;
74
+ }
75
+ export interface QuestionSearchItem {
76
+ id: string;
77
+ questionType: QuestionType | string;
78
+ content: unknown;
79
+ correctAnswer: unknown;
80
+ points?: string | number;
81
+ difficulty?: string;
82
+ skill?: string;
83
+ explanation?: string | null;
84
+ createdAt?: string;
85
+ updatedAt?: string;
86
+ code?: string;
87
+ title?: string | null;
88
+ usageCount?: number;
89
+ }
90
+ export interface SearchQuestionBankResult {
91
+ data: QuestionSearchItem[];
92
+ meta?: {
93
+ totalItems?: number;
94
+ };
95
+ }
52
96
  export interface FetchApiOptions {
53
97
  baseUrl?: string;
54
98
  fetcher?: typeof fetch;
@@ -1,6 +1,8 @@
1
1
  export * from './exam-create';
2
2
  export * from './exam-questions';
3
3
  export * from './exam-taking';
4
+ export * from './exam-entry';
5
+ export * from './contest-rooms';
4
6
  export * from './result-review';
5
7
  export * from '../shared/constants/ApiConstant';
6
8
  export * from '../shared/constants/QueryKeyConstant';
package/dist/api/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./exam-create";export*from"./exam-questions";export*from"./exam-taking";export*from"./result-review";export*from"../shared/constants/ApiConstant";export*from"../shared/constants/QueryKeyConstant";
1
+ export*from"./exam-create";export*from"./exam-questions";export*from"./exam-taking";export*from"./exam-entry";export*from"./contest-rooms";export*from"./result-review";export*from"../shared/constants/ApiConstant";export*from"../shared/constants/QueryKeyConstant";
@@ -0,0 +1,8 @@
1
+ export interface CompletionDialogProps {
2
+ open: boolean;
3
+ onOpenChange: (open: boolean) => void;
4
+ completedCount: number;
5
+ totalQuestions: number;
6
+ onConfirm: () => void;
7
+ }
8
+ export declare function CompletionDialog({ open, onOpenChange, completedCount, totalQuestions, onConfirm, }: CompletionDialogProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as n}from"react/jsx-runtime";import{AlertDialog as t,AlertDialogAction as h,AlertDialogCancel as c,AlertDialogContent as o,AlertDialogDescription as i,AlertDialogFooter as r,AlertDialogHeader as l,AlertDialogTitle as s}from"../../components/ui/alert-dialog";import{AlertTriangle as a,CheckCircle as d}from"lucide-react";export function CompletionDialog({open:m,onOpenChange:p,completedCount:g,totalQuestions:u,onConfirm:x}){const C=g===u;return e(t,{open:m,onOpenChange:p,children:n(o,{children:[n(l,{children:[n(s,{className:"flex items-center gap-2",children:[C?e(d,{className:"h-5 w-5 text-green-500"}):e(a,{className:"h-5 w-5 text-amber-500"}),C?"Lưu đề thi thành công":"Đề thi chưa hoàn thành"]}),e(i,{asChild:!0,children:n("div",{className:"space-y-2 text-sm text-muted-foreground",children:[n("p",{children:["Bạn đã hoàn thành ",n("strong",{children:[g,"/",u]})," câu hỏi."]}),C?e("p",{children:"Đề thi đã sẵn sàng để gán vào phòng thi."}):n("p",{children:["Còn ",e("strong",{children:u-g})," câu chưa được tạo. Bạn có thể tiếp tục tạo sau hoặc thoát và quay lại sau."]})]})})]}),n(r,{children:[e(c,{children:C?"Tiếp tục chỉnh sửa":"Tiếp tục tạo"}),e(h,{onClick:x,className:C?"bg-green-600 hover:bg-green-700":"bg-blue-600 hover:bg-blue-700",children:C?"Xác nhận":"Lưu và thoát"})]})]})})}
@@ -1,8 +1,11 @@
1
1
  import { type UseFormReturn } from 'react-hook-form';
2
2
  import type { ExamFormData, ExamLevelOption } from '../../api/exam-create';
3
+ import type { CreateExamTexts } from './create-exam.texts';
3
4
  export interface CreateExamInfoFormProps {
4
5
  form: UseFormReturn<ExamFormData>;
5
6
  examLevels: ExamLevelOption[];
6
7
  isLoadingExamLevels?: boolean;
8
+ /** Optional label overrides (e.g. to reuse this form for homework). */
9
+ texts?: CreateExamTexts;
7
10
  }
8
- export declare function CreateExamInfoForm({ form, examLevels, isLoadingExamLevels }: CreateExamInfoFormProps): import("react").JSX.Element;
11
+ export declare function CreateExamInfoForm({ form, examLevels, isLoadingExamLevels, texts, }: CreateExamInfoFormProps): import("react").JSX.Element;
@@ -1 +1 @@
1
- "use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useCallback as a,useEffect as l,useRef as o}from"react";import{Controller as n}from"react-hook-form";import{FileText as i,Loader2 as d}from"lucide-react";import{Card as m,CardContent as t,CardDescription as c,CardHeader as s,CardTitle as u}from"../../components/ui/card";import{Input as h}from"../../components/ui/input";import{Label as f}from"../../components/ui/label";import{Select as p,SelectContent as g,SelectItem as x,SelectTrigger as b,SelectValue as v}from"../../components/ui/select";import{Textarea as N}from"../../components/ui/textarea";import{useT as y}from"../../shared/lib/i18n";export function CreateExamInfoForm({form:f,examLevels:L,isLoadingExamLevels:E=!1}){const q=y(),w=o(!1),V=f.watch("name"),j=o(f.setValue);j.current=f.setValue;const M=a(e=>e.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/đ/g,"d").replace(/Đ/g,"D").toLowerCase().trim().replace(/[^a-z0-9\s-]/g,"").replace(/[\s]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").substring(0,100),[]);l(()=>{w.current||j.current("code",V?M(V):"",{shouldValidate:!1})},[V,M]);const z=e=>!!f.formState.errors[e],B=e=>f.formState.errors[e]?.message;return r(m,{className:"border border-gray-200",children:[r(s,{className:"border-b border-gray-100 bg-gray-50/50",children:[r(u,{className:"flex items-center gap-2 text-base font-semibold",children:[e(i,{className:"h-4 w-4 text-blue-600"}),q("admin.exams.form.basicInfo")]}),e(c,{children:q("admin.exams.form.requiredHint")})]}),r(t,{className:"space-y-4 pt-6",children:[r("div",{className:"grid grid-cols-1 gap-4 md:grid-cols-2",children:[e(P,{label:q("admin.exams.form.nameLabel"),error:B("name"),required:!0,children:e(h,{id:"name",maxLength:255,className:z("name")?"border-red-500":"",placeholder:q("admin.exams.form.namePlaceholder"),...f.register("name")})}),e(P,{label:q("admin.exams.form.codeLabel"),error:B("code"),required:!0,children:e(C,{form:f,hasError:z("code"),onManualEdit:e=>{w.current=e.length>0}})}),e(P,{label:q("admin.exams.form.totalPoints"),error:B("totalPoints"),required:!0,children:e(n,{name:"totalPoints",control:f.control,render:({field:r})=>e(h,{id:"totalPoints",inputMode:"numeric",value:r.value,onBlur:r.onBlur,onChange:e=>r.onChange(e.target.value.replace(/[^0-9]/g,"")),className:z("totalPoints")?"border-red-500":""})})}),e(P,{label:q("admin.exams.form.level"),error:B("level"),children:e(n,{name:"level",control:f.control,render:({field:a})=>r(p,{value:a.value,onValueChange:a.onChange,disabled:E,children:[e(b,{children:e(v,{placeholder:q(E?"admin.exams.form.loading":"admin.exams.form.levelPlaceholder")})}),e(g,{children:E?e("div",{className:"flex justify-center p-4",children:e(d,{className:"h-4 w-4 animate-spin"})}):L.map(r=>e(x,{value:r.id,children:r.name},r.id))})]})})}),e(P,{label:q("admin.exams.form.difficulty"),error:B("difficulty"),children:e(n,{name:"difficulty",control:f.control,render:({field:a})=>r(p,{value:a.value,onValueChange:a.onChange,children:[e(b,{children:e(v,{placeholder:q("admin.exams.form.difficultyPlaceholder")})}),r(g,{children:[e(x,{value:"easy",children:q("admin.exams.difficulty.easy")}),e(x,{value:"medium",children:q("admin.exams.difficulty.medium")}),e(x,{value:"hard",children:q("admin.exams.difficulty.hard")})]})]})})}),e(P,{label:q("admin.exams.form.duration"),error:B("duration"),children:e(h,{id:"duration",inputMode:"numeric",placeholder:"60",className:z("duration")?"border-red-500":"",...f.register("duration")})})]}),e(P,{label:q("admin.exams.form.description"),error:B("description"),children:e(N,{id:"description",rows:4,maxLength:2e3,placeholder:q("admin.exams.form.descriptionPlaceholder"),...f.register("description")})})]})]})}function C({form:r,hasError:a,onManualEdit:l}){const o=y(),{onChange:n,...i}=r.register("code");return e(h,{id:"code",maxLength:100,className:a?"border-red-500":"",placeholder:o("admin.exams.form.codePlaceholder"),...i,onChange:e=>{n(e),l(e.target.value)}})}function P({label:a,error:l,required:o,children:n}){return r("div",{children:[r(f,{children:[a," ",o&&e("span",{className:"text-red-500",children:"*"})]}),e("div",{className:"mt-1",children:n}),l&&e("p",{className:"mt-1 text-sm text-red-600",children:l})]})}
1
+ "use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useCallback as a,useEffect as n,useRef as o}from"react";import{Controller as l}from"react-hook-form";import{FileText as i,Loader2 as t}from"lucide-react";import{Card as d,CardContent as c,CardDescription as m,CardHeader as s,CardTitle as u}from"../../components/ui/card";import{Input as h}from"../../components/ui/input";import{Label as f}from"../../components/ui/label";import{Select as p,SelectContent as g,SelectItem as x,SelectTrigger as b,SelectValue as N}from"../../components/ui/select";import{Textarea as v}from"../../components/ui/textarea";import{useT as y}from"../../shared/lib/i18n";const C=["EASY","MEDIUM","HARD"];export function CreateExamInfoForm({form:f,examLevels:S,isLoadingExamLevels:w=!1,texts:M}){const q=y(),D=o(!1),I=f.watch("name"),V=o(f.setValue);V.current=f.setValue;const j=a(e=>e.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/đ/g,"d").replace(/Đ/g,"D").toLowerCase().trim().replace(/[^a-z0-9\s-]/g,"").replace(/[\s]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").substring(0,100),[]);n(()=>{D.current||V.current("code",I?j(I):"",{shouldValidate:!1})},[I,j]);const A=e=>Boolean(f.formState.errors[e]),H=e=>f.formState.errors[e]?.message;return r(d,{className:"border border-gray-200",children:[r(s,{className:"border-b border-gray-100 bg-gray-50/50",children:[r(u,{className:"flex items-center gap-2 text-base font-semibold",children:[e(i,{className:"h-4 w-4 text-blue-600"}),M?.basicInfo??q("admin.exams.form.basicInfo")]}),e(m,{children:M?.requiredHint??q("admin.exams.form.requiredHint")})]}),e(c,{className:"space-y-4 pt-6",children:r("div",{className:"grid grid-cols-1 gap-4 md:grid-cols-2",children:[e(P,{label:M?.nameLabel??q("admin.exams.form.nameLabel"),error:H("name"),required:!0,hint:q("admin.exams.form.charCount",{count:String(I?.length||0),max:String(255)}),children:e(h,{id:"name",maxLength:255,className:A("name")?"border-red-500 focus:ring-red-500":"",placeholder:M?.namePlaceholder??q("admin.exams.form.namePlaceholder"),...f.register("name")})}),e(P,{label:M?.codeLabel??q("admin.exams.form.codeLabel"),error:H("code"),required:!0,hint:q("admin.exams.form.charCount",{count:String(f.watch("code")?.length||0),max:String(100)}),children:e(L,{form:f,hasError:A("code"),placeholder:M?.codePlaceholder??q("admin.exams.form.codePlaceholder"),onManualEdit:e=>{D.current=e.length>0}})}),e(P,{label:q("admin.exams.form.totalPoints"),error:H("totalPoints"),required:!0,children:e(l,{name:"totalPoints",control:f.control,render:({field:r})=>e(h,{id:"totalPoints",type:"text",inputMode:"numeric",placeholder:"100",value:r.value,onBlur:r.onBlur,onChange:e=>r.onChange(e.target.value.replace(/[^0-9]/g,"")),className:A("totalPoints")?"border-red-500 focus:ring-red-500":""})})}),e(P,{label:q("admin.exams.form.level"),error:H("level"),children:e(l,{name:"level",control:f.control,render:({field:a})=>r(p,{value:a.value,onValueChange:a.onChange,disabled:w,children:[e(b,{className:A("level")?"border-red-500 focus:ring-red-500":"",children:e(N,{placeholder:q(w?"admin.exams.form.loading":"admin.exams.form.levelPlaceholder")})}),e(g,{children:w?e("div",{className:"flex items-center justify-center p-4",children:e(t,{className:"h-4 w-4 animate-spin text-gray-400"})}):S.map(r=>e(x,{value:r.id,children:r.name},r.id))})]})})}),e(P,{label:q("admin.exams.form.difficulty"),error:H("difficulty"),children:e(l,{name:"difficulty",control:f.control,render:({field:a})=>r(p,{value:a.value,onValueChange:a.onChange,children:[e(b,{className:A("difficulty")?"border-red-500 focus:ring-red-500":"",children:e(N,{placeholder:q("admin.exams.form.difficultyPlaceholder")})}),e(g,{children:C.map(r=>e(x,{value:r,children:E(r,q)},r))})]})})}),e(P,{label:M?.durationLabel??q("admin.exams.form.duration"),error:H("duration"),children:e(h,{id:"duration",type:"number",inputMode:"numeric",min:5,max:300,placeholder:"45",className:A("duration")?"border-red-500 focus:ring-red-500":"",...f.register("duration")})}),e(P,{className:"md:col-span-2",label:M?.descriptionLabel??q("admin.exams.form.description"),error:H("description"),hint:q("admin.exams.form.charCount",{count:String(f.watch("description")?.length||0),max:String(2e3)}),children:e(v,{id:"description",rows:3,maxLength:2e3,placeholder:M?.descriptionPlaceholder??q("admin.exams.form.descriptionPlaceholder"),className:A("description")?"border-red-500 focus:ring-red-500":"",...f.register("description")})})]})})]})}function L({form:r,hasError:a,placeholder:n,onManualEdit:o}){const{onChange:l,...i}=r.register("code");return e(h,{id:"code",maxLength:100,className:a?"border-red-500 focus:ring-red-500":"",placeholder:n,...i,onChange:e=>{l(e),o(e.target.value)}})}function P({label:a,error:n,hint:o,required:l,className:i,children:t}){return r("div",{className:i,children:[r(f,{children:[a," ",l&&e("span",{className:"text-red-500",children:"*"})]}),e("div",{className:"mt-1",children:t}),n&&e("p",{className:"mt-1 text-sm text-red-600",children:n}),o&&e("p",{className:"mt-1 text-xs text-gray-500",children:o})]})}function E(e,r){switch(e){case"EASY":return r("admin.exams.form.easy");case"MEDIUM":return r("admin.exams.form.medium");case"HARD":return r("admin.exams.form.hard");default:return e}}
@@ -1,9 +1,12 @@
1
1
  import { UserRole } from '../../shared/types/common.types';
2
2
  import type { CreateExamSdkApi } from '../../api/exam-create';
3
+ import type { CreateExamTexts } from './create-exam.texts';
3
4
  export interface CreateExamPageContainerProps {
4
5
  role?: UserRole.ADMIN | UserRole.TEACHER;
5
6
  api: CreateExamSdkApi;
6
7
  listPath?: string;
7
8
  onNavigate?: (href: string) => void;
9
+ /** Optional label overrides (e.g. to reuse this flow for homework). */
10
+ texts?: CreateExamTexts;
8
11
  }
9
12
  export declare function CreateExamPageContainer(props: CreateExamPageContainerProps): import("react").JSX.Element;
@@ -1 +1 @@
1
- "use client";import{jsx as e,jsxs as a,Fragment as t}from"react/jsx-runtime";import{Suspense as n,useEffect as i,useState as r}from"react";import{zodResolver as s}from"@hookform/resolvers/zod";import{useForm as o}from"react-hook-form";import{ArrowLeft as l,ArrowRight as m,LayoutTemplate as c,Loader2 as d,X as h}from"lucide-react";import{Button as p}from"../../components/ui/button";import{Card as u,CardContent as f,CardHeader as x,CardTitle as g}from"../../components/ui/card";import{Dialog as v,DialogContent as b,DialogDescription as N,DialogHeader as y,DialogTitle as w}from"../../components/ui/dialog";import{useToast as C}from"../../shared/lib/hooks/useToast";import{useT as k}from"../../shared/lib/i18n";import{buildExamTemplatePayload as E}from"../../api/exam-create";import{CreateExamInfoForm as I}from"./CreateExamInfoForm";import{TemplateSelector as T}from"./TemplateSelector";import{examFormSchema as j}from"./create-exam.validation";function L({api:n,listPath:c="/manage/exams",onNavigate:h}){const u=k(),{success:f,warning:x,error:g}=C(),[L,z]=r(!1),[D,P]=r(null),[q,M]=r([]),[O,F]=r(!1),[V,$]=r(!1),B=o({resolver:s(j),mode:"onBlur",defaultValues:{code:"",name:"",level:"",difficulty:"",duration:"",totalPoints:"100",description:""}});i(()=>{if(!n.getExamLevels)return;let e=!1;return F(!0),n.getExamLevels().then(a=>{e||M(a)}).catch(e=>g({message:u("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})).finally(()=>{e||F(!1)}),()=>{e=!0}},[n,g,u]);const H=e=>{h?h(e):"undefined"!=typeof window&&(window.location.href=e)},Q=B.handleSubmit(async e=>{if(D){$(!0);try{const t=function(e){return"object"==typeof e&&null!==e&&"data"in e}(a=await n.createExam({name:e.name,code:e.code||void 0,examLevel:e.level||void 0,difficulty:A(e.difficulty),totalScore:Number.parseInt(e.totalPoints,10),durationMinutes:e.duration?Number.parseInt(e.duration,10):void 0,description:e.description||void 0,isComplete:!1,isActive:!1,examTemplateId:D.id}))?a.data:a;await n.updateExamTemplate({examId:t.id,template:E(D)}),f({message:u("admin.exams.create.success"),description:u("admin.exams.create.successDesc",{name:t.name})}),H(`${c}/${t.id}/questions`)}catch(e){g({message:u("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})}finally{$(!1)}var a}else x({message:u("admin.exams.create.incompleteInfo"),description:"Vui lòng chọn template đề thi trước khi tiếp tục."})},()=>x({message:u("admin.exams.create.incompleteInfo"),description:u("admin.exams.create.incompleteInfoDesc")}));return a("div",{className:"space-y-6",children:[a("div",{className:"flex items-center gap-4",children:[e(p,{variant:"ghost",size:"icon",onClick:()=>H(c),className:"h-8 w-8",children:e(l,{className:"h-4 w-4"})}),a("div",{children:[e("h1",{className:"text-3xl font-bold tracking-tight text-gray-900",children:u("admin.exams.create.title")}),e("p",{className:"mt-1 text-gray-600",children:u("admin.exams.create.step1")})]})]}),e("form",{id:"exam-form",onSubmit:Q,children:e(I,{form:B,examLevels:q,isLoadingExamLevels:O})}),e(S,{selectedTemplate:D,onOpen:()=>z(!0),onClear:()=>P(null)}),e(v,{open:L,onOpenChange:z,children:a(b,{className:"flex max-h-[92vh] min-h-[92vh] max-w-5xl flex-col overflow-hidden",children:[a(y,{children:[e(w,{children:"Chọn template đề thi"}),e(N,{children:"Chọn loại đề thi bạn muốn tạo."})]}),e(T,{api:n,onTemplateSelect:e=>{P(e),z(!1)},onCancel:()=>z(!1)})]})}),a("div",{className:"flex justify-end gap-3",children:[e(p,{variant:"outline",onClick:()=>H(c),disabled:V,children:u("admin.exams.create.cancel")}),e(p,{type:"submit",form:"exam-form",className:"bg-blue-600 hover:bg-blue-700",disabled:V||!D,children:a(t,V?{children:[e(d,{className:"mr-2 h-4 w-4 animate-spin"}),u("admin.exams.create.saving")]}:{children:[u("admin.exams.create.saveAndContinue"),e(m,{className:"ml-2 h-4 w-4"})]})})]})]})}export function CreateExamPageContainer(a){return e(n,{fallback:e(z,{}),children:e(L,{...a})})}function S({selectedTemplate:t,onOpen:n,onClear:i}){return a(u,{className:"border border-gray-200",children:[e(x,{className:"border-b border-gray-100 bg-gray-50/50",children:a(g,{className:"flex items-center gap-2 text-base font-semibold",children:[e(c,{className:"h-4 w-4 text-blue-600"}),"Chọn Template ",e("span",{className:"text-red-500",children:"*"})]})}),e(f,{className:"pt-6",children:a("div",t?{className:"flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3",children:[a("div",{children:[e("p",{className:"text-sm font-medium text-green-800",children:t.name}),a("p",{className:"text-xs text-green-600",children:[t.totalQuestions," câu hỏi • ",t.parts.length," phần"]})]}),a("div",{className:"flex gap-2",children:[e(p,{type:"button",variant:"outline",size:"sm",onClick:n,children:"Đổi template"}),e(p,{type:"button",variant:"ghost",size:"icon",onClick:i,children:e(h,{className:"h-3.5 w-3.5"})})]})]}:{className:"flex flex-col items-center gap-3 rounded-lg border-2 border-dashed border-gray-300 py-8",children:[e(c,{className:"h-8 w-8 text-gray-400"}),e("p",{className:"text-sm text-gray-500",children:"Chưa chọn template đề thi"}),a(p,{type:"button",variant:"outline",onClick:n,children:[e(c,{className:"mr-2 h-4 w-4"}),"Chọn template"]})]})})]})}function z(){return e("div",{className:"flex h-screen items-center justify-center",children:e(d,{className:"h-8 w-8 animate-spin text-blue-600"})})}function A(e){return"easy"===e?"EASY":"medium"===e?"MEDIUM":"hard"===e?"HARD":void 0}
1
+ "use client";import{jsx as e,jsxs as t,Fragment as a}from"react/jsx-runtime";import{Suspense as n,useEffect as i,useState as r}from"react";import{zodResolver as s}from"@hookform/resolvers/zod";import{useForm as l}from"react-hook-form";import{ArrowLeft as o,ArrowRight as m,LayoutTemplate as c,Loader2 as d,X as p}from"lucide-react";import{Button as h}from"../../components/ui/button";import{Card as u,CardContent as x,CardHeader as f,CardTitle as g}from"../../components/ui/card";import{Dialog as b,DialogContent as v,DialogDescription as N,DialogHeader as y,DialogTitle as w}from"../../components/ui/dialog";import{useToast as C}from"../../shared/lib/hooks/useToast";import{useT as T}from"../../shared/lib/i18n";import{buildExamTemplatePayload as E}from"../../api/exam-create";import{CreateExamInfoForm as k}from"./CreateExamInfoForm";import{TemplateSelector as D}from"./TemplateSelector";import{examFormSchema as L}from"./create-exam.validation";function I({api:n,listPath:c="/manage/exams",onNavigate:p,texts:u}){const x=T(),{success:f,warning:g,error:I}=C(),[S,M]=r(!1),[z,P]=r(null),[q,O]=r([]),[F,H]=r(!1),[R,U]=r(!1),V=l({resolver:s(L),mode:"onBlur",defaultValues:{code:"",name:"",level:"",difficulty:"",duration:"",totalPoints:"100",description:""}});i(()=>{if(!n.getExamLevels)return;let e=!1;return H(!0),n.getExamLevels().then(t=>{e||O(t)}).catch(e=>I({message:x("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})).finally(()=>{e||H(!1)}),()=>{e=!0}},[n,I,x]);const Y=e=>{p?p(e):"undefined"!=typeof window&&(window.location.href=e)},$=V.handleSubmit(async e=>{if(z){U(!0);try{const a=function(e){return"object"==typeof e&&null!==e&&"data"in e}(t=await n.createExam({name:e.name,code:e.code||void 0,examLevel:e.level||void 0,difficulty:A(e.difficulty),totalScore:Number.parseInt(e.totalPoints,10),durationMinutes:e.duration?Number.parseInt(e.duration,10):void 0,description:e.description||void 0,isComplete:!1,isActive:!1,examTemplateId:z.id}))?t.data:t;await n.updateExamTemplate({examId:a.id,template:E(z)}),f({message:x("admin.exams.create.success"),description:u?.successDescription?.replace("{name}",a.name)??x("admin.exams.create.successDesc",{name:a.name})}),Y(`${c}/${a.id}/questions`)}catch(e){I({message:x("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})}finally{U(!1)}var t}else g({message:x("admin.exams.create.incompleteInfo"),description:u?.missingTemplateDescription??"Vui lòng chọn template đề thi trước khi tiếp tục."})},()=>g({message:x("admin.exams.create.incompleteInfo"),description:x("admin.exams.create.incompleteInfoDesc")}));return t("div",{className:"space-y-6",children:[t("div",{className:"flex items-center gap-4",children:[e(h,{variant:"ghost",size:"icon",onClick:()=>Y(c),className:"h-8 w-8",children:e(o,{className:"h-4 w-4"})}),t("div",{children:[e("h1",{className:"text-3xl font-bold tracking-tight text-gray-900",children:u?.title??x("admin.exams.create.title")}),e("p",{className:"mt-1 text-gray-600",children:u?.step1??x("admin.exams.create.step1")})]})]}),e("form",{id:"exam-form",onSubmit:$,children:e(k,{form:V,examLevels:q,isLoadingExamLevels:F,texts:u})}),e(j,{selectedTemplate:z,onOpen:()=>M(!0),onClear:()=>P(null),texts:u}),e(b,{open:S,onOpenChange:M,children:t(v,{className:"flex max-h-[92vh] min-h-[92vh] max-w-5xl flex-col overflow-hidden",children:[t(y,{children:[e(w,{children:u?.templateDialogTitle??"Chọn template đề thi"}),e(N,{children:u?.templateDialogDescription??"Chọn loại đề thi bạn muốn tạo."})]}),e(D,{api:n,onTemplateSelect:e=>{P(e),M(!1)},onCancel:()=>M(!1)})]})}),t("div",{className:"flex justify-end gap-3",children:[e(h,{variant:"outline",onClick:()=>Y(c),disabled:R,children:u?.cancelLabel??x("admin.exams.create.cancel")}),e(h,{type:"submit",form:"exam-form",className:"bg-blue-600 hover:bg-blue-700",disabled:R||!z,children:t(a,R?{children:[e(d,{className:"mr-2 h-4 w-4 animate-spin"}),u?.savingLabel??x("admin.exams.create.saving")]}:{children:[u?.saveAndContinueLabel??x("admin.exams.create.saveAndContinue"),e(m,{className:"ml-2 h-4 w-4"})]})})]})]})}export function CreateExamPageContainer(t){return e(n,{fallback:e(S,{}),children:e(I,{...t})})}function j({selectedTemplate:a,onOpen:n,onClear:i,texts:r}){return t(u,{className:"border border-gray-200",children:[e(f,{className:"border-b border-gray-100 bg-gray-50/50",children:t(g,{className:"flex items-center gap-2 text-base font-semibold",children:[e(c,{className:"h-4 w-4 text-blue-600"}),r?.templateSectionTitle??"Chọn Template"," ",e("span",{className:"text-red-500",children:"*"})]})}),e(x,{className:"pt-6",children:t("div",a?{className:"flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-green-100",children:e(c,{className:"h-4 w-4 text-green-600"})}),t("div",{children:[e("p",{className:"text-sm font-medium text-green-800",children:a.name}),t("p",{className:"text-xs text-green-600",children:[a.totalQuestions," câu hỏi • ",a.parts.length," phần"]})]})]}),t("div",{className:"flex items-center gap-2",children:[e(h,{type:"button",variant:"outline",size:"sm",onClick:n,className:"border-green-300 text-green-700 hover:bg-green-100",children:r?.changeTemplateLabel??"Đổi template"}),e(h,{type:"button",variant:"ghost",size:"icon",onClick:i,className:"h-7 w-7 text-green-600 hover:bg-green-100 hover:text-green-800",children:e(p,{className:"h-3.5 w-3.5"})})]})]}:{className:"flex flex-col items-center gap-3 rounded-lg border-2 border-dashed border-gray-300 py-8",children:[e(c,{className:"h-8 w-8 text-gray-400"}),e("p",{className:"text-sm text-gray-500",children:r?.noTemplateText??"Chưa chọn template đề thi"}),t(h,{type:"button",variant:"outline",onClick:n,children:[e(c,{className:"mr-2 h-4 w-4"}),r?.selectTemplateLabel??"Chọn template"]})]})})]})}function S(){return e("div",{className:"flex h-screen items-center justify-center",children:e(d,{className:"h-8 w-8 animate-spin text-blue-600"})})}function A(e){return"EASY"===e?"EASY":"MEDIUM"===e?"MEDIUM":"HARD"===e?"HARD":void 0}
@@ -1,5 +1,6 @@
1
+ import { type ExistingQuestionBank } from '../questions/components/QuestionBankOption';
1
2
  import type { ExamData, ExamQuestion, ExamTemplate } from '../../shared/types/entities/exam.types';
2
- import type { SaveQuestionInput } from '../../api/exam-questions.types';
3
+ import type { SaveQuestionInput, SearchQuestionBankInput, SearchQuestionBankResult } from '../../api/exam-questions.types';
3
4
  export interface ExamCreatorProps {
4
5
  template: ExamTemplate;
5
6
  examId?: string;
@@ -7,10 +8,15 @@ export interface ExamCreatorProps {
7
8
  examLevel?: string;
8
9
  subjectId?: string;
9
10
  initialQuestions?: Partial<ExamQuestion>[];
10
- onSaveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | void>;
11
+ onSaveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | {
12
+ question: Partial<ExamQuestion>;
13
+ questionBank?: ExistingQuestionBank | null;
14
+ } | void>;
11
15
  onSave?: (examData: ExamData) => void | Promise<void>;
12
16
  onCancel?: () => void;
13
17
  onPartAudioChange?: (partId: string, audioUrl: string) => void | Promise<void>;
14
18
  partAudioOverrides?: Record<string, string>;
19
+ searchQuestions?: (input: SearchQuestionBankInput) => Promise<SearchQuestionBankResult>;
20
+ saveExamRef?: React.MutableRefObject<(() => void) | null>;
15
21
  }
16
- export declare function ExamCreator({ template, examId, examDifficulty, examLevel, subjectId, initialQuestions, onSaveQuestion, onSave, onCancel, onPartAudioChange, partAudioOverrides }: ExamCreatorProps): import("react").JSX.Element;
22
+ export declare function ExamCreator(props: ExamCreatorProps): import("react").JSX.Element;
@@ -1 +1 @@
1
- "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useMemo as r,useRef as o,useState as i}from"react";import{ChevronLeft as a,ChevronRight as s,Save as n}from"lucide-react";import{Button as l}from"../../components/ui/button";import{QuestionCreator as d}from"../questions/creator/QuestionCreator";import{useToast as u}from"../../shared/lib/hooks/useToast";import{useT as m}from"../../shared/lib/i18n";import{ExamPartTabs as c}from"./ExamPartTabs";import{ExamQuestionGrid as p}from"./ExamQuestionGrid";import{PartAudioUploader as f}from"./PartAudioUploader";import{getEffectiveQuestionConfig as g,getPartByQuestionIndex as x,normalizeTemplateGroupConfig as h}from"./exam-template.utils";export function ExamCreator({template:b,examId:v,examDifficulty:w,examLevel:C,subjectId:y,initialQuestions:I=[],onSaveQuestion:N,onSave:S,onCancel:Q,onPartAudioChange:k,partAudioOverrides:q}){const A=m(),{success:E,warning:T,error:j}=u(),O=o(null),[D,M]=i(0),[P,L]=i(()=>Object.fromEntries(I.map(e=>[(e.questionNumber??1)-1,e]))),[_,U]=i(()=>new Set(I.map(e=>(e.questionNumber??1)-1))),[V,z]=i({}),[B,G]=i(!1),[R,$]=i(!1),H=o({}),F=x(b,D),J=F?h(F):void 0,K=F?g(F,D):void 0,W=!(!J?.groups||K?.groups),X=K&&"sectionName"in K?K.sectionName:void 0,Y=X&&!W?`${F?.id}_${X}`:F?.id,Z=P[D],ee=r(()=>({examId:v,templateId:b.id,questions:Object.values(P),currentQuestionIndex:D,lastSaved:(new Date).toISOString(),createdAt:(new Date).toISOString(),updatedAt:(new Date).toISOString(),isCompleted:_.size>=b.totalQuestions}),[_.size,D,v,P,b.id,b.totalQuestions]),te=e=>{R&&T({message:A("common.examCreator.saveBeforeExit")}),M(Math.max(0,Math.min(b.totalQuestions-1,e))),$(!1),H.current={}},re=async e=>{if(!F)return;if(!1===await(O.current?.triggerValidation?.()))return;const t={...Z,...H.current,...e,type:e?.type??H.current.type??Z?.type??K?.questionType,partId:F.id,questionNumber:D+1,difficulty:e?.difficulty??w??Z?.difficulty,skill:F.skill,isCompleted:!0};G(!0);try{const e=await(N?.({question:t,questionIndex:D,template:b,examId:v,examDifficulty:w,examLevel:C,subjectId:y}))||t;L(t=>({...t,[D]:e})),U(e=>new Set(e).add(D)),$(!1),E({message:A("common.examCreator.saveSuccess",{index:String(D+1)})})}catch(e){j({message:A("common.examCreator.saveError",{index:String(D+1)}),description:e instanceof Error?e.message:void 0})}finally{G(!1)}};return e("div",{className:"flex min-h-0 flex-1 flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50/30",children:t("div",{className:"flex min-h-0 flex-1 overflow-hidden",children:[e("div",{className:"w-80 overflow-y-auto border-r border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(c,{template:b,currentPartId:F?.id??"",completedQuestions:_,onPartChange:e=>te(b.parts.find(t=>t.id===e)?.startIndex??0)})}),t("div",{className:"flex flex-1 flex-col overflow-hidden",children:[e("div",{className:"min-h-0 flex-1 overflow-y-auto p-4 pb-4",children:t("div",{className:"mx-auto max-w-4xl",children:["LISTENING"===F?.skill&&e(f,{partId:F.id,partName:F.name,audioUrl:q?.[F.id]??F.audioUrl??"",examId:v,onAudioChange:e=>{k?.(F.id,e)},isReadOnly:!k}),e(d,{initialData:Z,prefilledType:K?.questionType,prefilledSkill:F?.skill,optionCount:K?.optionCount,optionMode:K?.optionMode,defaultValue:K?.defaultValue,defaultQuestion:K?.defaultQuestion,viewMode:K?.viewMode,groups:K?.groups||J?.groups,groupConfig:K?.groupConfig||J?.groupConfig,groupType:K?.groupType||J?.groupType,articleCount:K?.articleCount??F?.articleCount,questionConfig:K?.questionConfig||F?.questionConfig,questionIndexInPart:W?D-(F?.startIndex??0):D-(K?.startIndex??F?.startIndex??0),partId:Y,onChange:e=>{H.current=e,$(!0)},onSave:re,onCancel:Q,isEmbedded:!0,externalErrors:V[D],validationRef:O,suggestedAnswers:K?.suggestedAnswers||F?.suggestedAnswers,hasSuggestedAnswers:K?.hasSuggestedAnswers??F?.hasSuggestedAnswers},D)]})}),e("div",{className:"flex-shrink-0 border-t border-gray-200/80 bg-white/95 shadow-[0_-4px_20px_rgba(0,0,0,0.08)] backdrop-blur-sm",children:t("div",{className:"mx-auto flex max-w-4xl items-center justify-between px-6 py-4",children:[t(l,{variant:"outline",onClick:()=>te(D-1),disabled:0===D,children:[e(a,{className:"mr-2 h-4 w-4"}),"Câu trước"]}),t(l,{onClick:()=>{re()},disabled:B,className:"bg-green-600 hover:bg-green-700",children:[e(n,{className:"mr-2 h-4 w-4"}),B?"Đang lưu...":"Lưu câu hỏi"]}),t(l,{variant:"outline",onClick:()=>te(D+1),disabled:D>=b.totalQuestions-1,children:["Câu tiếp",e(s,{className:"ml-2 h-4 w-4"})]})]})})]}),t("div",{className:"w-80 overflow-y-auto border-l border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:[e(p,{template:b,currentQuestionIndex:D,completedQuestions:_,onQuestionChange:te}),t(l,{onClick:()=>{(async()=>{R?T({message:A("common.examCreator.saveBeforeExit")}):await(S?.(ee))})()},className:"mt-4 w-full bg-blue-600 hover:bg-blue-700",children:[e(n,{className:"mr-2 h-4 w-4"}),"Hoàn tất đề thi"]})]})]})})}
1
+ "use client";import{jsx as e,jsxs as t,Fragment as n}from"react/jsx-runtime";import{useCallback as o,useEffect as i,useMemo as r,useRef as a,useState as s}from"react";import{CheckCircle as d,ChevronLeft as l,ChevronRight as u,Loader2 as c,Save as m}from"lucide-react";import{Button as p}from"../../components/ui/button";import{QuestionBankOption as g}from"../questions/components/QuestionBankOption";import{QuestionSearchDropdown as f}from"../questions/components/QuestionSearchDropdown";import{QuestionCreator as h}from"../questions/creator/QuestionCreator";import{useToast as x}from"../../shared/lib/hooks/useToast";import{useT as b}from"../../shared/lib/i18n";import{useExamQuestionStore as v}from"../../shared/lib/stores/examQuestionStore";import{transformApiQuestionToFrontend as C}from"../../shared/lib/utils/question-reverse-transform";import{ExamPartTabs as w}from"./ExamPartTabs";import{ExamQuestionGrid as y}from"./ExamQuestionGrid";import{PartAudioUploader as N}from"./PartAudioUploader";import{CompletionDialog as I}from"./CompletionDialog";import{UnsavedChangesDialog as k}from"./UnsavedChangesDialog";import{clearQuestionStatuses as q,getQuestionStatus as Q,QuestionStatus as S,setQuestionStatus as E}from"./exam-question-status";import{getEffectiveQuestionConfig as D,getPartByQuestionIndex as T,normalizeTemplateGroupConfig as A}from"./exam-template.utils";export function ExamCreator(L){const{template:B,examId:V=`exam-${Date.now()}`,initialQuestions:j=[]}=L,U=b(),{success:P,warning:$,error:M}=x(),R=a(null),z=a({}),F=a(!1),G=a(!1),H=a(!1),K=v(e=>e.initialized),W=v(e=>e.examId),J=v(e=>e.initializeExam),X=v(e=>e.resetStore),Y=v(e=>e.currentQuestionIndex),Z=v(e=>e.setCurrentQuestionIndex),ee=v(e=>e.questions),te=v(e=>e.upsertQuestionData),ne=v(e=>e.clearQuestionData),oe=v(e=>e.markQuestionCompleted),ie=v(e=>e.getQuestionData),re=v(e=>e.setSharedHintLetters),[ae,se]=s({}),[de,le]=s(!1),[ue,ce]=s(!1),[me,pe]=s(!1),[ge,fe]=s(null),[he,xe]=s(!1),[be,ve]=s(!1),[Ce,we]=s({}),[ye,Ne]=s({}),[Ie,ke]=s({}),[qe,Qe]=s(),[Se,Ee]=s(),De=T(B,Y),Te=De?.id??"",Ae=De?D(De,Y):void 0,Oe=De?A(De):void 0,_e=Boolean(Oe?.groups&&!Ae?.groups),Le=_(Ae),Be=Le&&!_e?`${Te}_${Le}`:Te,Ve=ee[Y]?.data,je=Ce[Y]??{checked:!1,code:"",title:""},Ue=r(()=>{const e=new Set;return Object.entries(ee).forEach(([t,n])=>{n.isCompleted&&e.add(Number(t))}),e},[ee]),Pe=r(()=>({id:V,templateId:B.id,questions:Object.values(ee).map(e=>e.data),currentQuestionIndex:Y,lastSaved:(new Date).toISOString(),createdAt:(new Date).toISOString(),updatedAt:(new Date).toISOString(),isCompleted:Ue.size>=B.totalQuestions}),[Ue.size,Y,V,ee,B.id,B.totalQuestions]);i(()=>{const e=W&&W!==V;e&&X(),K&&!e||J({examId:V,templateId:B.id,totalQuestions:B.totalQuestions})},[V,J,K,X,W,B.id,B.totalQuestions]),i(()=>{if(!K||0===j.length)return;F.current=!0,j.forEach(e=>{const t=(e.questionNumber??1)-1,n={...e,isCompleted:!0};te(t,n),oe(t,!0),z.current[t]=n,function(e,t,n,o){if("LOOK_PICTURE_FILL_WORD_HINT"!==t.type||!t.partId)return;const i=t.answer,r=t.questionGroup?.groupNumber;if(!i?.hintLetters?.length||!r)return;const a=T(n,e),s=a?D(a,e):void 0,d=_(s),l=d?`${t.partId}_${d}`:t.partId;o(l,r,i.hintLetters)}(t,n,B,re);const o=e.questionBank;o?.id&&Ne(e=>({...e,[t]:o}))});const e=window.setTimeout(()=>{F.current=!1},1e3);return()=>window.clearTimeout(e)},[j,K,oe,re,B,te]),i(()=>{pe(Q(V,Y+1)===S.UNSAVED)},[Y,V]),i(()=>{ce(Object.values(ee).some(e=>Object.keys(e.data).length>0&&!e.isCompleted))},[ee]),i(()=>{const e=e=>{ue&&(e.preventDefault(),e.returnValue="")};return window.addEventListener("beforeunload",e),()=>window.removeEventListener("beforeunload",e)},[ue]);const $e=o(e=>{H.current=!0,pe(!1),Z(Math.max(0,Math.min(B.totalQuestions-1,e))),window.setTimeout(()=>{H.current=!1},500)},[Z,B.totalQuestions]),Me=e=>{if(me)return fe(e),void xe(!0);$e(e)},Re=o(e=>{F.current||G.current||H.current||(te(Y,{questionNumber:Y+1,partId:Te,...e}),E(V,Y+1,S.UNSAVED),pe(!0))},[Te,Y,V,te]),ze=async e=>{if(!De)return;if(!function(e,t,n,o){if(!e.checked||t?.id)return!0;const i=e.code.trim()?void 0:"Vui lòng nhập mã câu hỏi cho ngân hàng",r=e.title.trim()?void 0:"Vui lòng nhập tiêu đề câu hỏi cho ngân hàng";return n(i),o(r),!i&&!r}(je,ye[Y],Qe,Ee))return;if(!1===await(R.current?.triggerValidation?.()))return;const t=R.current?.getFormData?.(),n=function(e,t,n,o,i,r,a){return{...n,...e,...t?{answer:t}:{},type:e?.type??n?.type??o,id:n?.id??e?.id,partId:i.id,questionNumber:r+1,difficulty:e?.difficulty??a??n?.difficulty,skill:i.skill,isCompleted:!0}}(e,t,ie(Y),Ae?.questionType,De,Y,L.examDifficulty);le(!0);try{const e=await(L.onSaveQuestion?.({question:n,questionIndex:Y,template:B,examId:V,examDifficulty:L.examDifficulty,examLevel:L.examLevel,subjectId:L.subjectId,sectionId:(o=Ae,o&&"sectionId"in o?o.sectionId:void 0),addToQuestionBank:!ye[Y]?.id&&je.checked,questionBankCode:je.checked?je.code:void 0,questionBankTitle:je.checked?je.title:void 0,existingQuestionBankId:ye[Y]?.id})),t=function(e,t){return e&&"question"in e?{...t,...e.question,isCompleted:!0}:{...t,...e??{},isCompleted:!0}}(e,n);te(Y,t),oe(Y,!0),z.current[Y]=t,function(e,t,n){const o=e&&"questionBank"in e?e.questionBank:e?.questionBank;o?.id&&n(e=>({...e,[t]:o}))}(e,Y,Ne),G.current=!0,window.setTimeout(()=>{G.current=!1},500),E(V,Y+1,S.SAVED),pe(!1),P({message:U("common.examCreator.saveSuccess",{index:String(Y+1)})})}catch(e){M({message:U("common.examCreator.saveError",{index:String(Y+1)}),description:e instanceof Error?e.message:void 0})}finally{le(!1)}var o};return L.saveExamRef&&(L.saveExamRef.current=()=>{me?$({message:U("common.examCreator.saveBeforeExit")}):ve(!0)}),t("div",{className:"flex min-h-0 flex-1 flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50/30",children:[t("div",{className:"flex min-h-0 flex-1 overflow-hidden",children:[e("div",{className:"w-80 overflow-y-auto border-r border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(w,{template:B,currentPartId:Te,completedQuestions:Ue,onPartChange:e=>Me(B.parts.find(t=>t.id===e)?.startIndex??0)})}),t("div",{className:"flex flex-1 flex-col overflow-hidden",children:[e("div",{className:"min-h-0 flex-1 overflow-y-auto p-4 pb-4",children:t("div",{className:"mx-auto max-w-4xl",children:["LISTENING"===De?.skill&&e(N,{partId:De.id,partName:De.name,audioUrl:L.partAudioOverrides?.[De.id]??De.audioUrl??"",examId:V,onAudioChange:e=>{L.onPartAudioChange?.(De.id,e)},isReadOnly:!L.onPartAudioChange}),e(h,{initialData:Ve,prefilledType:Ae?.questionType,prefilledSkill:De?.skill,optionCount:Ae?.optionCount,optionMode:Ae?.optionMode,defaultValue:Ae?.defaultValue,defaultQuestion:Ae?.defaultQuestion,viewMode:Ae?.viewMode,groups:Ae?.groups||Oe?.groups,groupConfig:Ae?.groupConfig||Oe?.groupConfig,groupType:Ae?.groupType||Oe?.groupType,articleCount:Ae?.articleCount??De?.articleCount,questionConfig:Ae?.questionConfig||De?.questionConfig,questionIndexInPart:_e?Y-(De?.startIndex??0):Y-(Ae?.startIndex??De?.startIndex??0),partId:Be,onChange:Re,onUnsavedChangesChange:pe,onSave:ze,onCancel:L.onCancel,isEmbedded:!0,externalErrors:ae[Y],validationRef:R,suggestedAnswers:Ae?.suggestedAnswers||De?.suggestedAnswers,hasSuggestedAnswers:Ae?.hasSuggestedAnswers??De?.hasSuggestedAnswers,headerExtra:e(f,{questionType:Ae?.questionType||De?.questionType,onSelect:e=>{const t={...C({question_type:e.type,content:e.content,correct_answer:e.answer,points:e.points,difficult_level:e.difficulty,question_skill_type:e.skill,explanation:e.explanation}),id:void 0,partId:Te,questionNumber:Y+1,isCompleted:!1};te(Y,t);const n=e.id,o=e.code,i=e.title;(o||i)&&ke(e=>({...e,[Y]:{code:o,title:i}})),n&&(o||i)&&Ne(e=>({...e,[Y]:{id:n,code:o??"",title:i??""}})),E(V,Y+1,S.UNSAVED),pe(!0),oe(Y,!1),P({message:U("common.examCreator.bankSelected")})},selectedQuestionLabel:Ie[Y]?`${Ie[Y].code?`#${Ie[Y].code} - `:""}${Ie[Y].title||""}`:void 0,disabled:!(Ae?.questionType||De?.questionType),compact:!0,searchQuestions:L.searchQuestions})},Y),e("div",{className:"mt-8",children:e(g,{checked:je.checked,onCheckedChange:e=>O(Y,{checked:e},we,pe,V),code:je.code,onCodeChange:e=>O(Y,{code:e},we,pe,V),title:je.title,onTitleChange:e=>O(Y,{title:e},we,pe,V),codeError:qe,error:Se,existingBank:ye[Y]||null})})]})}),e("div",{className:"flex-shrink-0 border-t border-gray-200/80 bg-white/95 shadow-[0_-4px_20px_rgba(0,0,0,0.08)] backdrop-blur-sm",children:t("div",{className:"mx-auto flex max-w-4xl items-center justify-between px-6 py-4",children:[t(p,{variant:"outline",onClick:()=>Me(Y-1),disabled:0===Y,className:"gap-2 border-gray-300 hover:bg-gray-50",children:[e(l,{className:"h-4 w-4"}),"Câu trước"]}),t("div",{className:"flex items-center gap-4",children:[me?t("div",{className:"flex items-center gap-2 rounded-full bg-amber-50 px-3 py-1.5 ring-1 ring-amber-200",children:[e("span",{className:"h-2 w-2 animate-pulse rounded-full bg-amber-500"}),e("span",{className:"text-xs font-medium text-amber-700",children:"Chưa lưu"})]}):ee[Y]?.isCompleted?t("div",{className:"flex items-center gap-2 rounded-full bg-green-50 px-3 py-1.5 ring-1 ring-green-200",children:[e(d,{className:"h-3.5 w-3.5 text-green-500"}),e("span",{className:"text-xs font-medium text-green-700",children:"Đã lưu"})]}):null,t("div",{className:"flex items-center gap-2 rounded-full bg-gray-100 px-4 py-2",children:[t("span",{className:"text-sm font-bold text-gray-900",children:["Câu ",Y+1]}),e("span",{className:"text-sm text-gray-400",children:"/"}),e("span",{className:"text-sm text-gray-500",children:B.totalQuestions})]}),e(p,{onClick:()=>{ze(ie(Y))},disabled:de||!me,className:"gap-2 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 shadow-md hover:shadow-lg transition-all",children:t(n,de?{children:[e(c,{className:"h-4 w-4 animate-spin"}),"Đang lưu..."]}:{children:[e(m,{className:"h-4 w-4"}),"Lưu câu hỏi"]})})]}),t(p,{variant:"outline",onClick:()=>Me(Y+1),disabled:Y>=B.totalQuestions-1||!ee[Y]?.isCompleted,className:"gap-2 border-gray-300 hover:bg-gray-50",children:["Câu tiếp theo",e(u,{className:"h-4 w-4"})]})]})})]}),e("div",{className:"w-80 overflow-y-auto border-l border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(y,{template:B,currentQuestionIndex:Y,completedQuestions:Ue,onQuestionChange:Me})})]}),e(I,{open:be,onOpenChange:ve,completedCount:Ue.size,totalQuestions:B.totalQuestions,onConfirm:()=>{ve(!1),q(V),X(),L.onSave?.(Pe)}}),e(k,{open:he,onOpenChange:xe,onConfirm:()=>{const e=z.current[Y];e?(te(Y,e),E(V,Y+1,S.SAVED)):ne(Y),pe(!1),null!==ge&&$e(ge),fe(null),xe(!1)},onCancel:()=>xe(!1),questionNumber:Y+1})]})}function O(e,t,n,o,i){n(n=>{const o=n[e]??{checked:!1,code:"",title:""};return{...n,[e]:{...o,...t}}}),o(!0),E(i,e+1,S.UNSAVED)}function _(e){return e&&"sectionName"in e?e.sectionName:void 0}
@@ -5,5 +5,5 @@ interface ExamPartTabsProps {
5
5
  completedQuestions: Set<number>;
6
6
  onPartChange: (partId: string) => void;
7
7
  }
8
- export declare function ExamPartTabs({ template, currentPartId, completedQuestions, onPartChange }: ExamPartTabsProps): import("react").JSX.Element;
8
+ export declare function ExamPartTabs({ template, currentPartId, completedQuestions, onPartChange, }: ExamPartTabsProps): import("react").JSX.Element;
9
9
  export {};