@tinyweb_dev/oe-exam-sdk 0.1.1 → 0.1.3

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 (514) hide show
  1. package/dist/api/exam-create.js +1 -179
  2. package/dist/api/exam-create.types.js +1 -1
  3. package/dist/api/exam-questions.d.ts +1 -1
  4. package/dist/api/exam-questions.js +1 -98
  5. package/dist/api/exam-questions.types.d.ts +45 -1
  6. package/dist/api/exam-questions.types.js +1 -1
  7. package/dist/api/exam-taking.js +1 -73
  8. package/dist/api/exam-taking.types.js +1 -1
  9. package/dist/api/index.js +1 -6
  10. package/dist/api/result-review.js +1 -50
  11. package/dist/api/result-review.types.js +1 -1
  12. package/dist/components/CueCardEditor.js +1 -58
  13. package/dist/components/common/ResolvedImage.js +1 -15
  14. package/dist/components/exams/CompletionDialog.d.ts +8 -0
  15. package/dist/components/exams/CompletionDialog.js +1 -0
  16. package/dist/components/exams/CreateExamInfoForm.d.ts +4 -1
  17. package/dist/components/exams/CreateExamInfoForm.js +1 -35
  18. package/dist/components/exams/CreateExamPageContainer.d.ts +3 -0
  19. package/dist/components/exams/CreateExamPageContainer.js +1 -103
  20. package/dist/components/exams/ExamCreator.d.ts +9 -3
  21. package/dist/components/exams/ExamCreator.js +1 -89
  22. package/dist/components/exams/ExamPartTabs.d.ts +1 -1
  23. package/dist/components/exams/ExamPartTabs.js +1 -40
  24. package/dist/components/exams/ExamPreviewDialog.d.ts +20 -0
  25. package/dist/components/exams/ExamPreviewDialog.js +1 -0
  26. package/dist/components/exams/ExamQuestionGrid.d.ts +1 -1
  27. package/dist/components/exams/ExamQuestionGrid.js +1 -18
  28. package/dist/components/exams/ExamQuestionsPageContainer.d.ts +3 -0
  29. package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
  30. package/dist/components/exams/ExamViewDialog.d.ts +24 -0
  31. package/dist/components/exams/ExamViewDialog.js +1 -0
  32. package/dist/components/exams/PartAudioUploader.js +1 -9
  33. package/dist/components/exams/TemplateSelector.js +1 -66
  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/api.js +1 -3
  38. package/dist/components/exams/create-exam.texts.d.ts +58 -0
  39. package/dist/components/exams/create-exam.texts.js +1 -0
  40. package/dist/components/exams/create-exam.validation.js +1 -18
  41. package/dist/components/exams/exam-question-status.d.ts +9 -0
  42. package/dist/components/exams/exam-question-status.js +1 -0
  43. package/dist/components/exams/exam-template.utils.js +1 -18
  44. package/dist/components/exams/index.d.ts +11 -1
  45. package/dist/components/exams/index.js +1 -10
  46. package/dist/components/exams/take/ExamTakingPageContainer.js +1 -153
  47. package/dist/components/exams/take/GenericQuestionRenderer.js +1 -14
  48. package/dist/components/exams/take/exam-taking.utils.js +1 -88
  49. package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -24
  50. package/dist/components/exams/take/index.js +1 -2
  51. package/dist/components/exams/take/types.js +1 -1
  52. package/dist/components/index.js +1 -42
  53. package/dist/components/questions/_shared/config/question-types.config.js +1 -333
  54. package/dist/components/questions/_shared/constants/color-palette.js +1 -21
  55. package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +1 -105
  56. package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +1 -222
  57. package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +1 -140
  58. package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +1 -123
  59. package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +1 -50
  60. package/dist/components/questions/_shared/hooks/useQuestionFormState.js +1 -54
  61. package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +1 -123
  62. package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +1 -121
  63. package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +1 -70
  64. package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +1 -194
  65. package/dist/components/questions/_shared/index.js +1 -7
  66. package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
  67. package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
  68. package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +1 -3
  69. package/dist/components/questions/_shared/types/color-region.config.js +1 -1
  70. package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
  71. package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
  72. package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
  73. package/dist/components/questions/_shared/types/index.js +1 -12
  74. package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
  75. package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
  76. package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
  77. package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
  78. package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
  79. package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
  80. package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
  81. package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
  82. package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +1 -3
  83. package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
  84. package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +1 -3
  85. package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
  86. package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
  87. package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
  88. package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
  89. package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
  90. package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
  91. package/dist/components/questions/_shared/types/question-group.type.js +1 -11
  92. package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
  93. package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
  94. package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
  95. package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
  96. package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
  97. package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
  98. package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
  99. package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
  100. package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
  101. package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
  102. package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +1 -3
  103. package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
  104. package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
  105. package/dist/components/questions/_shared/utils/answer-builder.js +1 -79
  106. package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +1 -39
  107. package/dist/components/questions/_shared/utils/id-generator.js +1 -3
  108. package/dist/components/questions/_shared/utils/question-validation.js +1 -21
  109. package/dist/components/questions/components/ColorNode.js +1 -85
  110. package/dist/components/questions/components/ColorRegionClient.js +1 -545
  111. package/dist/components/questions/components/ColorRegionCreator.js +1 -420
  112. package/dist/components/questions/components/ColorRegionNode.js +1 -59
  113. package/dist/components/questions/components/CreatorGuide.js +1 -103
  114. package/dist/components/questions/components/NameNode.js +1 -26
  115. package/dist/components/questions/components/QuestionBankOption.js +1 -46
  116. package/dist/components/questions/components/QuestionBasicInfoForm.js +1 -9
  117. package/dist/components/questions/components/QuestionCommonFields.js +1 -33
  118. package/dist/components/questions/components/QuestionSearchDropdown.js +1 -258
  119. package/dist/components/questions/components/QuestionTypeSelector.js +1 -32
  120. package/dist/components/questions/components/QuestionTypeSpecificForms.js +1 -6
  121. package/dist/components/questions/components/RegionNode.js +1 -94
  122. package/dist/components/questions/components/index.js +1 -14
  123. package/dist/components/questions/creator/QuestionCreator.js +1 -90
  124. package/dist/components/questions/creator/QuestionGroupCreator.js +1 -119
  125. package/dist/components/questions/creator/dedicated-component-router.js +1 -79
  126. package/dist/components/questions/creator/index.js +1 -3
  127. package/dist/components/questions/creator/question-type-registry.js +1 -80
  128. package/dist/components/questions/groups/ArticlesGroupCard.js +1 -121
  129. package/dist/components/questions/groups/BasicQuestionGroupCard.js +1 -50
  130. package/dist/components/questions/groups/DocumentGroupCard.js +1 -114
  131. package/dist/components/questions/groups/MatchAnswerGroupCard.js +1 -25
  132. package/dist/components/questions/index.js +1 -10
  133. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +1 -37
  134. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +1 -351
  135. package/dist/components/questions/types/answer-the-question/register.js +1 -67
  136. package/dist/components/questions/types/answer-the-question/transform.js +1 -36
  137. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +1 -141
  138. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +1 -118
  139. package/dist/components/questions/types/arrange-letters-into-words/index.js +1 -2
  140. package/dist/components/questions/types/arrange-letters-into-words/register.js +1 -4
  141. package/dist/components/questions/types/arrange-letters-into-words/transform.js +1 -24
  142. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +1 -64
  143. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +1 -169
  144. package/dist/components/questions/types/choose-correct-adjective/index.js +1 -2
  145. package/dist/components/questions/types/choose-correct-adjective/register.js +1 -55
  146. package/dist/components/questions/types/choose-correct-adjective/transform.js +1 -16
  147. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -131
  148. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -581
  149. package/dist/components/questions/types/choose-the-correct-answer/index.js +1 -2
  150. package/dist/components/questions/types/choose-the-correct-answer/register.js +1 -101
  151. package/dist/components/questions/types/choose-the-correct-answer/transform.js +1 -43
  152. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -275
  153. package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +1 -448
  154. package/dist/components/questions/types/fill-in-blank/index.js +1 -2
  155. package/dist/components/questions/types/fill-in-blank/register.js +1 -138
  156. package/dist/components/questions/types/fill-in-blank/transform.js +1 -59
  157. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +1 -117
  158. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +1 -671
  159. package/dist/components/questions/types/fill-missing-words-in-grid/index.js +1 -2
  160. package/dist/components/questions/types/fill-missing-words-in-grid/register.js +1 -37
  161. package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +1 -15
  162. package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +1 -262
  163. package/dist/components/questions/types/gn-speaking-interview/register.js +1 -121
  164. package/dist/components/questions/types/gn-speaking-interview/transform.js +1 -48
  165. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +1 -16
  166. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +1 -27
  167. package/dist/components/questions/types/image-object-matching/index.js +1 -2
  168. package/dist/components/questions/types/image-object-matching/register.js +1 -3
  169. package/dist/components/questions/types/image-object-matching/transform.js +1 -2
  170. package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +1 -102
  171. package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +1 -344
  172. package/dist/components/questions/types/label-the-picture/index.js +1 -2
  173. package/dist/components/questions/types/label-the-picture/register.js +1 -54
  174. package/dist/components/questions/types/label-the-picture/transform.js +1 -28
  175. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +1 -78
  176. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +1 -232
  177. package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +1 -2
  178. package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +1 -82
  179. package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +1 -27
  180. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +1 -70
  181. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +1 -386
  182. package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +1 -2
  183. package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +1 -100
  184. package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +1 -33
  185. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +1 -106
  186. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +1 -457
  187. package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +1 -2
  188. package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +1 -110
  189. package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +1 -35
  190. package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +1 -8
  191. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +1 -22
  192. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +1 -291
  193. package/dist/components/questions/types/listen-and-speak-answer/index.js +1 -2
  194. package/dist/components/questions/types/listen-and-speak-answer/register.js +1 -84
  195. package/dist/components/questions/types/listen-and-speak-answer/transform.js +1 -27
  196. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +1 -32
  197. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +1 -295
  198. package/dist/components/questions/types/listen-and-speak-compare-images/register.js +1 -79
  199. package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +1 -27
  200. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +1 -34
  201. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +1 -285
  202. package/dist/components/questions/types/listen-and-speak-image-group/register.js +1 -81
  203. package/dist/components/questions/types/listen-and-speak-image-group/transform.js +1 -26
  204. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +1 -38
  205. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +1 -385
  206. package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +1 -87
  207. package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +1 -32
  208. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +1 -26
  209. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +1 -247
  210. package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +1 -59
  211. package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +1 -18
  212. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +1 -26
  213. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +1 -256
  214. package/dist/components/questions/types/listen-and-speak-question-list/register.js +1 -81
  215. package/dist/components/questions/types/listen-and-speak-question-list/transform.js +1 -26
  216. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +1 -26
  217. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +1 -230
  218. package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +1 -56
  219. package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +1 -16
  220. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +1 -72
  221. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +1 -230
  222. package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
  223. package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +1 -66
  224. package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +1 -42
  225. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +1 -96
  226. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +1 -238
  227. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +1 -2
  228. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +1 -76
  229. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +1 -19
  230. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +1 -134
  231. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +1 -617
  232. package/dist/components/questions/types/look-picture-fill-word-hint/index.js +1 -2
  233. package/dist/components/questions/types/look-picture-fill-word-hint/register.js +1 -82
  234. package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +1 -29
  235. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +1 -38
  236. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +1 -359
  237. package/dist/components/questions/types/match-by-writing-answer/index.js +1 -3
  238. package/dist/components/questions/types/match-by-writing-answer/register.js +1 -58
  239. package/dist/components/questions/types/match-by-writing-answer/transform.js +1 -61
  240. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +1 -16
  241. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +1 -23
  242. package/dist/components/questions/types/read-and-color-objects/index.js +1 -2
  243. package/dist/components/questions/types/read-and-color-objects/register.js +1 -3
  244. package/dist/components/questions/types/read-and-color-objects/transform.js +1 -36
  245. package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +1 -125
  246. package/dist/components/questions/types/read-displayed-content/register.js +1 -64
  247. package/dist/components/questions/types/read-displayed-content/transform.js +1 -12
  248. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +1 -64
  249. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +1 -511
  250. package/dist/components/questions/types/read-passage-and-answer-questions/index.js +1 -2
  251. package/dist/components/questions/types/read-passage-and-answer-questions/register.js +1 -58
  252. package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +1 -28
  253. package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +1 -373
  254. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -41
  255. package/dist/components/questions/types/speaking-conversation/register.js +1 -110
  256. package/dist/components/questions/types/speaking-conversation/transform.js +1 -34
  257. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -74
  258. package/dist/components/questions/types/speaking-cue-card/register.js +1 -52
  259. package/dist/components/questions/types/speaking-cue-card/transform.js +1 -12
  260. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +1 -10
  261. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +1 -161
  262. package/dist/components/questions/types/speaking-describe-image/index.js +1 -2
  263. package/dist/components/questions/types/speaking-describe-image/register.js +1 -69
  264. package/dist/components/questions/types/speaking-describe-image/transform.js +1 -16
  265. package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +1 -234
  266. package/dist/components/questions/types/spontaneous-qa/register.js +1 -93
  267. package/dist/components/questions/types/spontaneous-qa/transform.js +1 -34
  268. package/dist/components/questions/types/true-false/TrueFalseClient.js +1 -75
  269. package/dist/components/questions/types/true-false/TrueFalseCreator.js +1 -244
  270. package/dist/components/questions/types/true-false/index.js +1 -2
  271. package/dist/components/questions/types/true-false/register.js +1 -77
  272. package/dist/components/questions/types/true-false/transform.js +1 -32
  273. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +1 -233
  274. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +1 -631
  275. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +1 -28
  276. package/dist/components/questions/types/word-fill-paragraph/index.js +1 -2
  277. package/dist/components/questions/types/word-fill-paragraph/register.js +1 -136
  278. package/dist/components/questions/types/word-fill-paragraph/transform.js +1 -56
  279. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +1 -138
  280. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +1 -261
  281. package/dist/components/questions/types/word-fill-structured-form/index.js +1 -2
  282. package/dist/components/questions/types/word-fill-structured-form/register.js +1 -67
  283. package/dist/components/questions/types/word-fill-structured-form/transform.js +1 -40
  284. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +1 -84
  285. package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +1 -192
  286. package/dist/components/questions/types/word-ordering/index.js +1 -2
  287. package/dist/components/questions/types/word-ordering/register.js +1 -46
  288. package/dist/components/questions/types/word-ordering/transform.js +1 -24
  289. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +1 -98
  290. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +1 -320
  291. package/dist/components/questions/types/write-a-short-letter/index.js +1 -2
  292. package/dist/components/questions/types/write-a-short-letter/register.js +1 -68
  293. package/dist/components/questions/types/write-a-short-letter/transform.js +1 -25
  294. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +1 -61
  295. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +1 -196
  296. package/dist/components/questions/types/write-correct-verb-form/index.js +1 -2
  297. package/dist/components/questions/types/write-correct-verb-form/register.js +1 -62
  298. package/dist/components/questions/types/write-correct-verb-form/transform.js +1 -17
  299. package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +1 -22
  300. package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +1 -174
  301. package/dist/components/questions/types/write-sentences/index.js +1 -2
  302. package/dist/components/questions/types/write-sentences/register.js +1 -58
  303. package/dist/components/questions/types/write-sentences/transform.js +1 -28
  304. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +1 -96
  305. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +1 -161
  306. package/dist/components/questions/types/write-short-paragraph/index.js +1 -2
  307. package/dist/components/questions/types/write-short-paragraph/register.js +1 -4
  308. package/dist/components/questions/types/write-short-paragraph/transform.js +1 -21
  309. package/dist/components/questions/viewer/QuestionGroupViewer.js +1 -36
  310. package/dist/components/questions/viewer/QuestionViewer.js +1 -446
  311. package/dist/components/questions/viewer/index.js +1 -2
  312. package/dist/components/results/ResultReviewPageContainer.js +1 -98
  313. package/dist/components/results/ReviewQuestionRenderer.js +1 -72
  314. package/dist/components/results/index.js +1 -2
  315. package/dist/components/results/result-review.utils.js +1 -6
  316. package/dist/components/results/review-data.js +1 -150
  317. package/dist/components/results/review-renderer.utils.js +1 -97
  318. package/dist/components/results/review-types.js +1 -1
  319. package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +1 -138
  320. package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +1 -12
  321. package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +1 -18
  322. package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +1 -11
  323. package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +1 -11
  324. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +1 -9
  325. package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +1 -13
  326. package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +1 -8
  327. package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +1 -6
  328. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +1 -5
  329. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +1 -9
  330. package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +1 -41
  331. package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -25
  332. package/dist/components/themes/cambridge-yle/index.js +1 -13
  333. package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +1 -24
  334. package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +1 -88
  335. package/dist/components/themes/english-certification/EcWordFillParagraph.js +1 -55
  336. package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +1 -156
  337. package/dist/components/themes/english-certification/EcWritingTask.js +1 -34
  338. package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -114
  339. package/dist/components/themes/english-certification/EnglishCertificationBrand.js +1 -11
  340. package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +1 -21
  341. package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +1 -15
  342. package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +1 -19
  343. package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +1 -148
  344. package/dist/components/themes/english-certification/EnglishCertificationHeader.js +1 -31
  345. package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +1 -71
  346. package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +1 -33
  347. package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +1 -16
  348. package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +1 -22
  349. package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +1 -37
  350. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -72
  351. package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +1 -90
  352. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +1 -18
  353. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +1 -29
  354. package/dist/components/themes/english-certification/english-certification-theme.js +1 -58
  355. package/dist/components/themes/english-certification/grouped-question-utils.js +1 -218
  356. package/dist/components/themes/english-certification/index.js +1 -21
  357. package/dist/components/themes/index.js +1 -6
  358. package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +1 -170
  359. package/dist/components/themes/summer-sky/SummerSkyLayout.js +1 -221
  360. package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +1 -18
  361. package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +1 -194
  362. package/dist/components/themes/types.js +1 -1
  363. package/dist/components/ui/alert-dialog.js +1 -26
  364. package/dist/components/ui/alert.js +1 -22
  365. package/dist/components/ui/avatar.js +1 -12
  366. package/dist/components/ui/badge.js +1 -38
  367. package/dist/components/ui/button.js +1 -35
  368. package/dist/components/ui/calendar.js +1 -76
  369. package/dist/components/ui/card.js +1 -16
  370. package/dist/components/ui/carousel.js +1 -91
  371. package/dist/components/ui/checkbox.js +1 -9
  372. package/dist/components/ui/collapsible.js +1 -13
  373. package/dist/components/ui/command.js +1 -34
  374. package/dist/components/ui/confirm-dialog.js +1 -49
  375. package/dist/components/ui/dialog.js +1 -23
  376. package/dist/components/ui/drawer.js +1 -35
  377. package/dist/components/ui/dropdown-menu.js +1 -33
  378. package/dist/components/ui/file-upload.js +1 -155
  379. package/dist/components/ui/form.js +1 -62
  380. package/dist/components/ui/hover-card.js +1 -10
  381. package/dist/components/ui/image-preview.js +1 -29
  382. package/dist/components/ui/input.js +1 -8
  383. package/dist/components/ui/label.js +1 -9
  384. package/dist/components/ui/language-switcher.js +1 -15
  385. package/dist/components/ui/multi-select.js +1 -90
  386. package/dist/components/ui/points-input.js +1 -33
  387. package/dist/components/ui/popover.js +1 -26
  388. package/dist/components/ui/progress.js +1 -16
  389. package/dist/components/ui/radio-group.js +1 -12
  390. package/dist/components/ui/scroll-area.js +1 -11
  391. package/dist/components/ui/select.js +1 -38
  392. package/dist/components/ui/separator.js +1 -8
  393. package/dist/components/ui/sheet.js +1 -40
  394. package/dist/components/ui/single-select.js +1 -76
  395. package/dist/components/ui/skeleton.js +1 -6
  396. package/dist/components/ui/switch.js +1 -8
  397. package/dist/components/ui/table.js +1 -20
  398. package/dist/components/ui/tabs.js +1 -17
  399. package/dist/components/ui/textarea.js +1 -8
  400. package/dist/components/ui/tooltip.js +1 -11
  401. package/dist/index.d.ts +3 -3
  402. package/dist/index.js +1 -19
  403. package/dist/mcp/config.js +1 -32
  404. package/dist/mcp/http-client.js +1 -48
  405. package/dist/mcp/index.js +1 -29
  406. package/dist/mcp/tools/_helpers.js +1 -25
  407. package/dist/mcp/tools/exams.js +1 -439
  408. package/dist/mcp/tools/index.js +1 -6
  409. package/dist/mcp/tools/results.js +1 -19
  410. package/dist/shared/constants/ApiConstant.js +1 -276
  411. package/dist/shared/constants/QueryKeyConstant.js +1 -112
  412. package/dist/shared/constants/exam-level.enum.js +1 -41
  413. package/dist/shared/constants/landings/placement-test.constants.js +1 -431
  414. package/dist/shared/constants/question-skills.js +1 -267
  415. package/dist/shared/constants/routes.js +1 -81
  416. package/dist/shared/index.d.ts +3 -0
  417. package/dist/shared/index.js +1 -7
  418. package/dist/shared/lib/hooks/index.js +1 -10
  419. package/dist/shared/lib/hooks/useAlertDialog.js +1 -57
  420. package/dist/shared/lib/hooks/useAudioPlayCounter.js +1 -30
  421. package/dist/shared/lib/hooks/useAudioPreview.js +1 -81
  422. package/dist/shared/lib/hooks/useAudioUpload.js +1 -24
  423. package/dist/shared/lib/hooks/useConfirmDialog.js +1 -89
  424. package/dist/shared/lib/hooks/useDebouncedCallback.js +1 -32
  425. package/dist/shared/lib/hooks/useDelayedAutoPlay.js +1 -12
  426. package/dist/shared/lib/hooks/useFileUpload.js +1 -24
  427. package/dist/shared/lib/hooks/useImageUrl.js +1 -5
  428. package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -10
  429. package/dist/shared/lib/hooks/useReactHookForm.js +1 -31
  430. package/dist/shared/lib/hooks/useToast.js +1 -62
  431. package/dist/shared/lib/i18n/index.js +1 -54
  432. package/dist/shared/lib/i18n/messages/en/admin.js +1 -2104
  433. package/dist/shared/lib/i18n/messages/en/auth.js +1 -86
  434. package/dist/shared/lib/i18n/messages/en/common.js +1 -140
  435. package/dist/shared/lib/i18n/messages/en/file-upload.js +1 -18
  436. package/dist/shared/lib/i18n/messages/en/student.js +1 -133
  437. package/dist/shared/lib/i18n/messages/en/terms.js +1 -37
  438. package/dist/shared/lib/i18n/messages/en.js +1 -15
  439. package/dist/shared/lib/i18n/messages/vi/admin.js +1 -2104
  440. package/dist/shared/lib/i18n/messages/vi/auth.js +1 -86
  441. package/dist/shared/lib/i18n/messages/vi/common.js +1 -140
  442. package/dist/shared/lib/i18n/messages/vi/file-upload.js +1 -18
  443. package/dist/shared/lib/i18n/messages/vi/student.js +1 -133
  444. package/dist/shared/lib/i18n/messages/vi/terms.js +1 -37
  445. package/dist/shared/lib/i18n/messages/vi.js +1 -15
  446. package/dist/shared/lib/i18n/types.js +1 -1
  447. package/dist/shared/lib/stores/examQuestionStore.js +1 -212
  448. package/dist/shared/lib/stores/localeStore.js +1 -10
  449. package/dist/shared/lib/utils/public-page.js +1 -44
  450. package/dist/shared/lib/utils/question-reverse-transform.d.ts +6 -0
  451. package/dist/shared/lib/utils/question-reverse-transform.js +1 -0
  452. package/dist/shared/lib/utils/question-transform-types.js +1 -1
  453. package/dist/shared/lib/utils/question-transform-validation.d.ts +6 -0
  454. package/dist/shared/lib/utils/question-transform-validation.js +1 -0
  455. package/dist/shared/lib/utils/question-transform.d.ts +6 -2
  456. package/dist/shared/lib/utils/question-transform.js +1 -174
  457. package/dist/shared/lib/utils.js +1 -168
  458. package/dist/shared/lib/validation/choose-correct-answer.validation.js +1 -111
  459. package/dist/shared/lib/validation/label-the-picture.validation.js +1 -19
  460. package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +1 -46
  461. package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +1 -21
  462. package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +1 -34
  463. package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +1 -40
  464. package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +1 -45
  465. package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +1 -19
  466. package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +1 -39
  467. package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +1 -32
  468. package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +1 -19
  469. package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +1 -28
  470. package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +1 -12
  471. package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +1 -47
  472. package/dist/shared/lib/validation/read-and-color-objects.validation.js +1 -29
  473. package/dist/shared/lib/validation/read-displayed-content.validation.js +1 -28
  474. package/dist/shared/lib/validation/speaking-conversation.validation.js +1 -75
  475. package/dist/shared/lib/validation/speaking-describe-image.validation.js +1 -31
  476. package/dist/shared/lib/validation/spontaneous-qa.validation.js +1 -48
  477. package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +1 -38
  478. package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +1 -39
  479. package/dist/shared/lib/validation/word-fill-paragraph.validation.js +1 -43
  480. package/dist/shared/lib/validation/word-ordering.validation.js +1 -33
  481. package/dist/shared/types/branch.types.js +1 -6
  482. package/dist/shared/types/common.types.js +1 -50
  483. package/dist/shared/types/entities/exam-schedule.types.js +1 -5
  484. package/dist/shared/types/entities/exam.types.js +1 -1
  485. package/dist/shared/types/entities/question.types.js +1 -4
  486. package/dist/shared/types/entities/result.types.js +1 -4
  487. package/dist/shared/types/entities/user.types.js +1 -4
  488. package/dist/shared/types/exam-taking.types.js +1 -2
  489. package/dist/shared/types/history.types.js +1 -34
  490. package/dist/shared/types/index.js +1 -1
  491. package/dist/shared/types/questions/choose-correct-adjective.js +1 -4
  492. package/dist/shared/types/questions/choose-the-correct-answer.js +1 -9
  493. package/dist/shared/types/questions/fill-missing-words-in-grid.js +1 -11
  494. package/dist/shared/types/questions/index.js +1 -40
  495. package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
  496. package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
  497. package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
  498. package/dist/shared/types/questions/listen-and-speak-info-exchange.js +1 -3
  499. package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +1 -3
  500. package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
  501. package/dist/shared/types/questions/listen-and-speak-with-story-images.js +1 -3
  502. package/dist/shared/types/questions/look-picture-choose-correct-answer.js +1 -5
  503. package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +1 -9
  504. package/dist/shared/types/questions/look-picture-fill-word-hint.js +1 -7
  505. package/dist/shared/types/questions/read-displayed-content.js +1 -1
  506. package/dist/shared/types/questions/speaking-conversation.js +1 -7
  507. package/dist/shared/types/questions/speaking-describe-image.js +1 -1
  508. package/dist/shared/types/questions/spontaneous-qa.js +1 -1
  509. package/dist/shared/types/questions/write-correct-verb-form.js +1 -12
  510. package/dist/shared/types/role.types.js +1 -1
  511. package/dist/shared/types/structure-management.js +1 -28
  512. package/dist/shared/types/student.types.js +1 -54
  513. package/dist/shared/types/teacher-dashboard-rooms.js +1 -2
  514. package/package.json +12 -3
@@ -1,156 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useRef, useEffect, useMemo, useCallback, memo } from 'react';
4
- import { ResolvedImage } from '../../../components/common/ResolvedImage';
5
- import { cn } from '../../../shared/lib/utils';
6
- import { transformWordFillStructuredForm } from '../../../components/exams/take/exam-taking.utils';
7
- /**
8
- * Replace `{key}` placeholders in raw HTML with inline `<input>` elements so the
9
- * full HTML structure (tables, forms, …) is preserved and inputs render inside
10
- * the correct cells — never breaking onto new lines or squeezing the layout.
11
- */
12
- /** Convert a 0-based blank key into the 1-based number shown to the student. */
13
- function toDisplayNumber(key) {
14
- const numeric = Number(key);
15
- return Number.isFinite(numeric) ? String(numeric + 1) : key;
16
- }
17
- function buildHtmlWithInputPlaceholders(html) {
18
- return html.replace(/\{([\w\d]+)\}/g, (_match, key) => {
19
- return `
20
- <span class="ec-blank-wrapper" data-blank-wrapper="${key}">
21
- <span class="ec-blank-number" data-blank-number="${key}">${toDisplayNumber(key)}</span>
22
- <input type="text" data-blank-key="${key}" class="ec-blank-input" />
23
- </span>
24
- `;
25
- });
26
- }
27
- function normalizeStructuredAnswer(value, caseSensitive) {
28
- const normalized = value.trim();
29
- return caseSensitive ? normalized : normalized.toLowerCase();
30
- }
31
- /**
32
- * Build the card label from the blank keys (which are the IELTS question numbers).
33
- * Returns "Questions x–y" for a numeric range, a single "Question n", or falls
34
- * back to `Question {fallback}` when keys are non-numeric/empty.
35
- */
36
- function formatBlankRange(answers, fallback) {
37
- const numericKeys = Object.keys(answers)
38
- .map((key) => Number(key))
39
- .filter((n) => Number.isFinite(n))
40
- .sort((a, b) => a - b);
41
- if (numericKeys.length === 0) {
42
- return `Question ${fallback}`;
43
- }
44
- // Keys are 0-based; display numbers are 1-based.
45
- const min = numericKeys[0] + 1;
46
- const max = numericKeys[numericKeys.length - 1] + 1;
47
- return min === max ? `Question ${min}` : `Questions ${min}–${max}`;
48
- }
49
- /** Stable empty object so React.memo children are not broken by new `{}`. */
50
- const EMPTY_OBJECT = {};
51
- /**
52
- * Renders one structured-form question's HTML with inline branded inputs.
53
- *
54
- * CRITICAL: We set `innerHTML` imperatively via a ref exactly ONCE on mount and
55
- * attach native input listeners, bypassing React reconciliation. Using
56
- * `dangerouslySetInnerHTML` would re-apply HTML on every render, destroying the
57
- * DOM inputs and losing focus while the student types.
58
- */
59
- const StructuredFormBody = memo(function StructuredFormBody({ question, initialAnswers, onBlankChange, isReviewMode, }) {
60
- const containerRef = useRef(null);
61
- const onBlankChangeRef = useRef(onBlankChange);
62
- onBlankChangeRef.current = onBlankChange;
63
- const mountedRef = useRef(false);
64
- const initialAnswersRef = useRef(initialAnswers);
65
- const processedHtml = useMemo(() => buildHtmlWithInputPlaceholders(question.content), [question.content]);
66
- useEffect(() => {
67
- const container = containerRef.current;
68
- if (!container)
69
- return;
70
- if (!mountedRef.current) {
71
- container.innerHTML = processedHtml;
72
- mountedRef.current = true;
73
- }
74
- const inputs = container.querySelectorAll('input.ec-blank-input');
75
- const handlers = [];
76
- inputs.forEach((input) => {
77
- const key = input.getAttribute('data-blank-key') || '';
78
- const wrapper = container.querySelector(`[data-blank-wrapper="${key}"]`);
79
- const userAnswer = initialAnswersRef.current[key] || '';
80
- const correctAnswer = question.answers[key] || '';
81
- const hasUserAnswer = userAnswer.trim().length > 0;
82
- const isCorrect = hasUserAnswer &&
83
- normalizeStructuredAnswer(userAnswer, question.caseSensitive) ===
84
- normalizeStructuredAnswer(correctAnswer, question.caseSensitive);
85
- const shouldShowReviewText = isReviewMode && !isCorrect && correctAnswer.trim().length > 0;
86
- input.value = shouldShowReviewText
87
- ? `${hasUserAnswer ? userAnswer : '(empty)'} → ${correctAnswer}`
88
- : userAnswer;
89
- input.disabled = isReviewMode;
90
- input.className = cn('ec-blank-input inline-block w-[110px] border-0 border-b-2 border-dashed bg-transparent px-1 py-0 text-base font-semibold leading-tight align-middle outline-none transition-colors', isReviewMode
91
- ? isCorrect
92
- ? 'border-[#22c55e] text-[#15803d]'
93
- : hasUserAnswer
94
- ? 'border-[#ef4444] text-red-700'
95
- : 'border-[#f97316] text-[#c2410c]'
96
- : 'border-[#1167e8] text-[#0f1730] hover:border-[#0b57d0] focus:border-[#0b57d0]');
97
- const numberBadge = container.querySelector(`[data-blank-number="${key}"]`);
98
- if (numberBadge) {
99
- // Always set 1-based display number here so it is correct even if
100
- // the cached innerHTML was built with a different formatter.
101
- numberBadge.textContent = toDisplayNumber(key);
102
- numberBadge.className = cn('ec-blank-number mr-1 inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full px-1 text-[11px] font-bold leading-none text-white align-middle', isReviewMode
103
- ? isCorrect
104
- ? 'bg-[#22c55e]'
105
- : hasUserAnswer
106
- ? 'bg-[#ef4444]'
107
- : 'bg-[#f97316]'
108
- : 'bg-[#0b57d0]');
109
- }
110
- if (wrapper) {
111
- wrapper.className = cn('ec-blank-wrapper inline-flex items-center whitespace-nowrap align-middle', isReviewMode ? 'mx-1' : 'mx-0.5');
112
- }
113
- const handler = (e) => {
114
- const target = e.target;
115
- onBlankChangeRef.current(key, target.value);
116
- };
117
- input.addEventListener('input', handler);
118
- handlers.push({ el: input, handler });
119
- });
120
- return () => {
121
- handlers.forEach(({ el, handler }) => {
122
- el.removeEventListener('input', handler);
123
- });
124
- };
125
- // eslint-disable-next-line react-hooks/exhaustive-deps
126
- }, [processedHtml, isReviewMode]);
127
- return (_jsxs("div", { className: "space-y-3", children: [question.title && (_jsx("h3", { className: "text-base font-semibold text-[#0f1730]", children: question.title })), question.imageUrl && (_jsx("div", { className: "flex justify-center", children: _jsx("div", { className: "overflow-hidden rounded-lg border border-[#d9e4f2]", children: _jsx(ResolvedImage, { src: question.imageUrl, alt: question.title || 'Question image', className: "max-h-56 w-auto object-contain" }) }) })), _jsx("div", { ref: containerRef, className: "ec-structured-content w-full overflow-x-auto rounded-xl border border-[#d9e4f2] bg-[#f4f9ff] p-5 text-base leading-relaxed text-[#0f1730] [&_table]:w-full [&_table]:border-collapse [&_td]:border [&_td]:border-[#d9e4f2] [&_td]:px-4 [&_td]:py-3 [&_td]:align-top [&_th]:border [&_th]:border-[#d9e4f2] [&_th]:bg-[#e9f3ff] [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-semibold" }), isReviewMode && Object.keys(question.answers).length > 0 && (_jsxs("div", { className: "rounded-xl border border-[#d9e4f2] bg-white/70 p-3", children: [_jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wider text-[#64748b]", children: "Correct answers" }), _jsx("div", { className: "mt-1 flex flex-wrap gap-1.5", children: Object.entries(question.answers).map(([key, val]) => (_jsxs("span", { className: "rounded border border-[#b9edc8] bg-[#ddf8e7] px-2 py-0.5 text-xs font-semibold text-[#15803d]", children: [key, ": ", val] }, key))) })] }))] }));
128
- });
129
- /**
130
- * Branded English Certification renderer for `WORD_FILL_STRUCTURED_FORM`.
131
- *
132
- * Replaces the shared Movers two-column grid (which squeezed IELTS tables into
133
- * narrow columns) with a full-width branded layout. Reuses
134
- * {@link transformWordFillStructuredForm} so the answer shape
135
- * (`{ [questionId]: { [blankKey]: value } }`) stays compatible with the autosave
136
- * and grading store.
137
- */
138
- export function EcWordFillStructuredForm({ part, answers, onAnswerChange, isReviewMode = false, }) {
139
- const { questions, instruction, groupTitle, groupContent, groupImageUrl } = useMemo(() => transformWordFillStructuredForm(part.questions), [part.questions]);
140
- const answersRef = useRef(answers);
141
- answersRef.current = answers;
142
- const onAnswerChangeRef = useRef(onAnswerChange);
143
- onAnswerChangeRef.current = onAnswerChange;
144
- const handleBlankChange = useCallback((questionId, key, value) => {
145
- const currentAnswer = answersRef.current[questionId] || {};
146
- onAnswerChangeRef.current(questionId, { ...currentAnswer, [key]: value });
147
- }, []);
148
- const blankChangeCallbacks = useMemo(() => {
149
- const map = {};
150
- questions.forEach((q) => {
151
- map[q.id] = (key, value) => handleBlankChange(q.id, key, value);
152
- });
153
- return map;
154
- }, [questions, handleBlankChange]);
155
- return (_jsxs("div", { className: "flex flex-col gap-4", children: [instruction && (_jsx("div", { className: "rounded-xl border border-[#d9e4f2] bg-[#e9f3ff] px-4 py-2.5 text-sm font-semibold uppercase tracking-wide text-[#0b57d0]", children: instruction })), groupTitle && (_jsx("h2", { className: "text-center text-lg font-bold uppercase text-[#0b57d0]", children: groupTitle })), groupImageUrl && (_jsx("div", { className: "flex items-center justify-center", children: _jsx(ResolvedImage, { src: groupImageUrl, alt: groupTitle || 'Group image', className: "max-h-[300px] rounded-lg object-contain shadow-md" }) })), _jsx("div", { className: "flex flex-col gap-4", children: questions.map((q) => (_jsxs("div", { id: `question-${q.id}`, className: "rounded-2xl border border-[#d9e4f2] bg-white/90 p-4 shadow-[0_14px_35px_rgba(13,71,161,0.10)] backdrop-blur-md", children: [_jsx("div", { className: "mb-2 flex items-center gap-2", children: _jsx("span", { className: "inline-flex items-center rounded-full bg-gradient-to-b from-[#1f7cff] to-[#0b57d0] px-3 py-1 text-xs font-bold text-white shadow-sm", children: formatBlankRange(q.answers, q.questionNumber) }) }), _jsx(StructuredFormBody, { question: q, initialAnswers: answers[q.id] || EMPTY_OBJECT, onBlankChange: blankChangeCallbacks[q.id], isReviewMode: isReviewMode })] }, q.id))) })] }));
156
- }
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useRef as r,useEffect as n,useMemo as s,useCallback as a,memo as i}from"react";import{ResolvedImage as o}from"../../../components/common/ResolvedImage";import{cn as l}from"../../../shared/lib/utils";import{transformWordFillStructuredForm as d}from"../../../components/exams/take/exam-taking.utils";function c(e){const t=Number(e);return Number.isFinite(t)?String(t+1):e}function b(e,t){const r=e.trim();return t?r:r.toLowerCase()}function u(e,t){const r=Object.keys(e).map(e=>Number(e)).filter(e=>Number.isFinite(e)).sort((e,t)=>e-t);if(0===r.length)return`Question ${t}`;const n=r[0]+1,s=r[r.length-1]+1;return n===s?`Question ${n}`:`Questions ${n}–${s}`}const m={},p=i(function({question:a,initialAnswers:i,onBlankChange:d,isReviewMode:u}){const m=r(null),p=r(d);p.current=d;const f=r(!1),x=r(i),h=s(()=>a.content.replace(/\{([\w\d]+)\}/g,(e,t)=>`\n <span class="ec-blank-wrapper" data-blank-wrapper="${t}">\n <span class="ec-blank-number" data-blank-number="${t}">${c(t)}</span>\n <input type="text" data-blank-key="${t}" class="ec-blank-input" />\n </span>\n `),[a.content]);return n(()=>{const e=m.current;if(!e)return;f.current||(e.innerHTML=h,f.current=!0);const t=e.querySelectorAll("input.ec-blank-input"),r=[];return t.forEach(t=>{const n=t.getAttribute("data-blank-key")||"",s=e.querySelector(`[data-blank-wrapper="${n}"]`),i=x.current[n]||"",o=a.answers[n]||"",d=i.trim().length>0,m=d&&b(i,a.caseSensitive)===b(o,a.caseSensitive),f=u&&!m&&o.trim().length>0;t.value=f?`${d?i:"(empty)"} → ${o}`:i,t.disabled=u,t.className=l("ec-blank-input inline-block w-[110px] border-0 border-b-2 border-dashed bg-transparent px-1 py-0 text-base font-semibold leading-tight align-middle outline-none transition-colors",u?m?"border-[#22c55e] text-[#15803d]":d?"border-[#ef4444] text-red-700":"border-[#f97316] text-[#c2410c]":"border-[#1167e8] text-[#0f1730] hover:border-[#0b57d0] focus:border-[#0b57d0]");const h=e.querySelector(`[data-blank-number="${n}"]`);h&&(h.textContent=c(n),h.className=l("ec-blank-number mr-1 inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full px-1 text-[11px] font-bold leading-none text-white align-middle",u?m?"bg-[#22c55e]":d?"bg-[#ef4444]":"bg-[#f97316]":"bg-[#0b57d0]")),s&&(s.className=l("ec-blank-wrapper inline-flex items-center whitespace-nowrap align-middle",u?"mx-1":"mx-0.5"));const g=e=>{const t=e.target;p.current(n,t.value)};t.addEventListener("input",g),r.push({el:t,handler:g})}),()=>{r.forEach(({el:e,handler:t})=>{e.removeEventListener("input",t)})}},[h,u]),t("div",{className:"space-y-3",children:[a.title&&e("h3",{className:"text-base font-semibold text-[#0f1730]",children:a.title}),a.imageUrl&&e("div",{className:"flex justify-center",children:e("div",{className:"overflow-hidden rounded-lg border border-[#d9e4f2]",children:e(o,{src:a.imageUrl,alt:a.title||"Question image",className:"max-h-56 w-auto object-contain"})})}),e("div",{ref:m,className:"ec-structured-content w-full overflow-x-auto rounded-xl border border-[#d9e4f2] bg-[#f4f9ff] p-5 text-base leading-relaxed text-[#0f1730] [&_table]:w-full [&_table]:border-collapse [&_td]:border [&_td]:border-[#d9e4f2] [&_td]:px-4 [&_td]:py-3 [&_td]:align-top [&_th]:border [&_th]:border-[#d9e4f2] [&_th]:bg-[#e9f3ff] [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-semibold"}),u&&Object.keys(a.answers).length>0&&t("div",{className:"rounded-xl border border-[#d9e4f2] bg-white/70 p-3",children:[e("span",{className:"text-[10px] font-semibold uppercase tracking-wider text-[#64748b]",children:"Correct answers"}),e("div",{className:"mt-1 flex flex-wrap gap-1.5",children:Object.entries(a.answers).map(([e,r])=>t("span",{className:"rounded border border-[#b9edc8] bg-[#ddf8e7] px-2 py-0.5 text-xs font-semibold text-[#15803d]",children:[e,": ",r]},e))})]})]})});export function EcWordFillStructuredForm({part:n,answers:i,onAnswerChange:l,isReviewMode:c=!1}){const{questions:b,instruction:f,groupTitle:x,groupContent:h,groupImageUrl:g}=s(()=>d(n.questions),[n.questions]),w=r(i);w.current=i;const N=r(l);N.current=l;const v=a((e,t,r)=>{const n=w.current[e]||{};N.current(e,{...n,[t]:r})},[]),k=s(()=>{const e={};return b.forEach(t=>{e[t.id]=(e,r)=>v(t.id,e,r)}),e},[b,v]);return t("div",{className:"flex flex-col gap-4",children:[f&&e("div",{className:"rounded-xl border border-[#d9e4f2] bg-[#e9f3ff] px-4 py-2.5 text-sm font-semibold uppercase tracking-wide text-[#0b57d0]",children:f}),x&&e("h2",{className:"text-center text-lg font-bold uppercase text-[#0b57d0]",children:x}),g&&e("div",{className:"flex items-center justify-center",children:e(o,{src:g,alt:x||"Group image",className:"max-h-[300px] rounded-lg object-contain shadow-md"})}),e("div",{className:"flex flex-col gap-4",children:b.map(r=>t("div",{id:`question-${r.id}`,className:"rounded-2xl border border-[#d9e4f2] bg-white/90 p-4 shadow-[0_14px_35px_rgba(13,71,161,0.10)] backdrop-blur-md",children:[e("div",{className:"mb-2 flex items-center gap-2",children:e("span",{className:"inline-flex items-center rounded-full bg-gradient-to-b from-[#1f7cff] to-[#0b57d0] px-3 py-1 text-xs font-bold text-white shadow-sm",children:u(r.answers,r.questionNumber)})}),e(p,{question:r,initialAnswers:i[r.id]||m,onBlankChange:k[r.id],isReviewMode:c})]},r.id))})]})}
@@ -1,34 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useMemo } from 'react';
4
- import { ResolvedImage } from '../../../components/common/ResolvedImage';
5
- import { cn } from '../../../shared/lib/utils';
6
- import { transformWriteShortLetter } from '../../../components/exams/take/exam-taking.utils';
7
- /** Count words in a free-text answer (collapses whitespace). */
8
- function countWords(text) {
9
- const trimmed = text.trim();
10
- if (!trimmed)
11
- return 0;
12
- return trimmed.split(/\s+/).length;
13
- }
14
- /**
15
- * Branded English Certification renderer for `WRITE_A_SHORT_LETTER`.
16
- *
17
- * Shows the prompt, optional requirements/guide images and a large branded
18
- * textarea with a live word counter. The stored answer is a plain string at
19
- * `answers[questionId]`, matching the shared writing renderer so the grading
20
- * store stays compatible.
21
- */
22
- export function EcWritingTask({ part, answers, onAnswerChange, isReviewMode = false, }) {
23
- const firstQuestion = part.questions[0];
24
- const data = useMemo(() => transformWriteShortLetter(firstQuestion), [firstQuestion]);
25
- if (!firstQuestion) {
26
- return null;
27
- }
28
- const { prompt, requirements, minWords, maxWords, guideImages } = data;
29
- const answer = answers[firstQuestion.id] || '';
30
- const wordCount = countWords(answer);
31
- const meetsMin = minWords ? wordCount >= minWords : true;
32
- const withinMax = maxWords ? wordCount <= maxWords : true;
33
- return (_jsxs("div", { className: "flex flex-col gap-4", children: [part.instructions && (_jsx("div", { className: "rounded-xl border border-[#d9e4f2] bg-[#e9f3ff] px-4 py-2.5 text-sm font-semibold uppercase tracking-wide text-[#0b57d0]", children: part.instructions })), prompt && (_jsx("div", { className: "whitespace-pre-line rounded-2xl border border-[#d9e4f2] bg-white/90 p-5 text-base leading-relaxed text-[#0f1730] shadow-[0_14px_35px_rgba(13,71,161,0.10)]", children: prompt })), requirements && requirements.length > 0 && (_jsxs("div", { className: "rounded-2xl border border-[#d9e4f2] bg-white/70 p-4", children: [_jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wider text-[#64748b]", children: "Y\u00EAu c\u1EA7u" }), _jsx("ul", { className: "mt-2 list-inside list-disc space-y-1 text-sm text-[#0f1730]", children: requirements.map((req, idx) => (_jsx("li", { children: req }, idx))) })] })), guideImages && guideImages.length > 0 && (_jsx("div", { className: "flex flex-col items-center justify-center gap-4 py-4", children: guideImages.map((img, idx) => (_jsx(ResolvedImage, { src: img, alt: `Guide ${idx + 1}`, className: "w-full max-w-3xl rounded-xl bg-white object-contain shadow-sm" }, idx))) })), _jsxs("div", { className: "rounded-2xl border border-[#d9e4f2] bg-white/90 p-4 shadow-[0_14px_35px_rgba(13,71,161,0.10)]", children: [_jsx("textarea", { value: answer, disabled: isReviewMode, placeholder: "Vi\u1EBFt b\u00E0i c\u1EE7a b\u1EA1n \u1EDF \u0111\u00E2y\u2026", onChange: (e) => onAnswerChange(firstQuestion.id, e.target.value), rows: 12, className: cn('w-full resize-y rounded-xl border-2 border-[#9cc4f5] bg-[#f4f9ff] px-4 py-3 text-sm leading-relaxed text-[#0f1730] outline-none transition-all focus:border-[#1167e8]', isReviewMode && 'cursor-not-allowed opacity-80') }), _jsxs("div", { className: "mt-2 flex items-center justify-between text-xs", children: [_jsxs("span", { className: cn('font-semibold', !meetsMin || !withinMax ? 'text-[#ef4444]' : 'text-[#15803d]'), children: [wordCount, " t\u1EEB"] }), (minWords || maxWords) && (_jsxs("span", { className: "text-[#64748b]", children: [minWords ? `Tối thiểu ${minWords}` : '', minWords && maxWords ? ' · ' : '', maxWords ? `Tối đa ${maxWords}` : ''] }))] })] })] }));
34
- }
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useMemo as r}from"react";import{ResolvedImage as s}from"../../../components/common/ResolvedImage";import{cn as i}from"../../../shared/lib/utils";import{transformWriteShortLetter as a}from"../../../components/exams/take/exam-taking.utils";export function EcWritingTask({part:n,answers:l,onAnswerChange:d,isReviewMode:o=!1}){const c=n.questions[0],m=r(()=>a(c),[c]);if(!c)return null;const{prompt:x,requirements:p,minWords:f,maxWords:u,guideImages:b}=m,h=l[c.id]||"",g=function(e){const t=e.trim();return t?t.split(/\s+/).length:0}(h),w=!f||g>=f,N=!u||g<=u;return t("div",{className:"flex flex-col gap-4",children:[n.instructions&&e("div",{className:"rounded-xl border border-[#d9e4f2] bg-[#e9f3ff] px-4 py-2.5 text-sm font-semibold uppercase tracking-wide text-[#0b57d0]",children:n.instructions}),x&&e("div",{className:"whitespace-pre-line rounded-2xl border border-[#d9e4f2] bg-white/90 p-5 text-base leading-relaxed text-[#0f1730] shadow-[0_14px_35px_rgba(13,71,161,0.10)]",children:x}),p&&p.length>0&&t("div",{className:"rounded-2xl border border-[#d9e4f2] bg-white/70 p-4",children:[e("span",{className:"text-[10px] font-semibold uppercase tracking-wider text-[#64748b]",children:"Yêu cầu"}),e("ul",{className:"mt-2 list-inside list-disc space-y-1 text-sm text-[#0f1730]",children:p.map((t,r)=>e("li",{children:t},r))})]}),b&&b.length>0&&e("div",{className:"flex flex-col items-center justify-center gap-4 py-4",children:b.map((t,r)=>e(s,{src:t,alt:`Guide ${r+1}`,className:"w-full max-w-3xl rounded-xl bg-white object-contain shadow-sm"},r))}),t("div",{className:"rounded-2xl border border-[#d9e4f2] bg-white/90 p-4 shadow-[0_14px_35px_rgba(13,71,161,0.10)]",children:[e("textarea",{value:h,disabled:o,placeholder:"Viết bài của bạn ở đây…",onChange:e=>d(c.id,e.target.value),rows:12,className:i("w-full resize-y rounded-xl border-2 border-[#9cc4f5] bg-[#f4f9ff] px-4 py-3 text-sm leading-relaxed text-[#0f1730] outline-none transition-all focus:border-[#1167e8]",o&&"cursor-not-allowed opacity-80")}),t("div",{className:"mt-2 flex items-center justify-between text-xs",children:[t("span",{className:i("font-semibold",w&&N?"text-[#15803d]":"text-[#ef4444]"),children:[g," từ"]}),(f||u)&&t("span",{className:"text-[#64748b]",children:[f?`Tối thiểu ${f}`:"",f&&u?" · ":"",u?`Tối đa ${u}`:""]})]})]})]})}
@@ -1,114 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useRef, useState, useEffect } from 'react';
4
- import { Play, Pause, RotateCcw, RefreshCw } from 'lucide-react';
5
- import { usePresignedFileUrl } from '../../../shared/lib/hooks/usePresignedFileUrl';
6
- import { cn } from '../../../shared/lib/utils';
7
- /**
8
- * Branded Ocean Edu audio bar for English Certification Listening sections.
9
- *
10
- * Handles play/pause/reset, progress and presigned-URL resolution while
11
- * adopting the compact Ocean Edu audio-bar look.
12
- */
13
- export function EnglishCertificationAudioPlayer({ audioUrl, partNumber = 0, isLoading = false, }) {
14
- const audioRef = useRef(null);
15
- // Resolve S3 storage keys to presigned URLs (HTTP URLs pass through unchanged)
16
- const { previewUrl: resolvedAudioUrl } = usePresignedFileUrl(audioUrl || undefined);
17
- const [isPlaying, setIsPlaying] = useState(false);
18
- const [currentTime, setCurrentTime] = useState(0);
19
- const [duration, setDuration] = useState(0);
20
- const [error, setError] = useState(null);
21
- const togglePlayPause = () => {
22
- if (!audioRef.current)
23
- return;
24
- if (isPlaying) {
25
- audioRef.current.pause();
26
- setIsPlaying(false);
27
- }
28
- else {
29
- audioRef.current.play().catch((err) => {
30
- setError(`Failed to play audio: ${err.message}`);
31
- });
32
- setIsPlaying(true);
33
- }
34
- };
35
- const handleReset = () => {
36
- if (!audioRef.current)
37
- return;
38
- audioRef.current.currentTime = 0;
39
- audioRef.current.pause();
40
- setIsPlaying(false);
41
- setCurrentTime(0);
42
- };
43
- const handleRetry = () => {
44
- const audio = audioRef.current;
45
- if (!audio || !resolvedAudioUrl)
46
- return;
47
- setError(null);
48
- setCurrentTime(0);
49
- setIsPlaying(false);
50
- audio.src = resolvedAudioUrl;
51
- audio.load();
52
- };
53
- const handleProgressChange = (e) => {
54
- const newTime = parseFloat(e.target.value);
55
- if (!audioRef.current)
56
- return;
57
- audioRef.current.currentTime = newTime;
58
- setCurrentTime(newTime);
59
- };
60
- const formatTime = (seconds) => {
61
- if (!seconds || isNaN(seconds))
62
- return '00:00';
63
- const mins = Math.floor(seconds / 60);
64
- const secs = Math.floor(seconds % 60);
65
- return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
66
- };
67
- useEffect(() => {
68
- const audio = audioRef.current;
69
- if (!audio)
70
- return;
71
- const handleTimeUpdate = () => setCurrentTime(audio.currentTime);
72
- const handleLoadedMetadata = () => {
73
- setDuration(audio.duration);
74
- setError(null);
75
- };
76
- const handleEnded = () => setIsPlaying(false);
77
- const handleError = () => {
78
- setError('Failed to load audio file');
79
- setIsPlaying(false);
80
- };
81
- audio.addEventListener('timeupdate', handleTimeUpdate);
82
- audio.addEventListener('loadedmetadata', handleLoadedMetadata);
83
- audio.addEventListener('ended', handleEnded);
84
- audio.addEventListener('error', handleError);
85
- return () => {
86
- audio.removeEventListener('timeupdate', handleTimeUpdate);
87
- audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
88
- audio.removeEventListener('ended', handleEnded);
89
- audio.removeEventListener('error', handleError);
90
- };
91
- }, []);
92
- useEffect(() => {
93
- if (audioRef.current && resolvedAudioUrl) {
94
- audioRef.current.src = resolvedAudioUrl;
95
- setCurrentTime(0);
96
- setIsPlaying(false);
97
- setError(null);
98
- }
99
- }, [resolvedAudioUrl]);
100
- const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
101
- const statusLabel = error
102
- ? 'Audio unavailable'
103
- : isLoading
104
- ? 'Loading...'
105
- : isPlaying
106
- ? 'Playing...'
107
- : currentTime > 0
108
- ? 'Paused'
109
- : 'Ready to play';
110
- return (_jsxs("div", { className: "grid w-full grid-cols-[64px_1fr] items-center gap-4 rounded-[10px] border border-[#d9e4f2] bg-white/90 p-4 shadow-[0_5px_16px_rgba(13,71,161,0.08)] sm:gap-[18px] sm:px-5", children: [_jsx("audio", { ref: audioRef }), _jsx("button", { type: "button", onClick: togglePlayPause, disabled: isLoading || !audioUrl || !!error, className: "grid h-14 w-14 place-items-center rounded-full text-2xl text-white shadow-[0_12px_22px_rgba(11,87,208,0.22)] transition-transform hover:scale-[1.03] disabled:cursor-not-allowed disabled:opacity-50", style: { background: 'linear-gradient(180deg, #1f7cff, #0b57d0)' }, "aria-label": isPlaying ? 'Pause audio' : 'Play audio', children: isPlaying ? _jsx(Pause, { size: 24 }) : _jsx(Play, { size: 24, className: "ml-0.5" }) }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("span", { className: "text-[18px] font-black text-[#0f1730]", children: ["Listening Audio", partNumber > 0 && ` · Part ${partNumber}`] }), _jsx("button", { type: "button", onClick: handleReset, disabled: isLoading || !audioUrl || !!error, className: "flex h-8 w-8 items-center justify-center rounded-full border border-[#d9e4f2] text-[#0b57d0] transition-colors hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-50", "aria-label": "Replay from start", children: _jsx(RotateCcw, { size: 16 }) })] }), _jsxs("div", { className: "mt-1 flex items-center gap-2.5", children: [_jsx("span", { className: cn('text-sm font-bold', error ? 'text-red-500' : 'text-[#64748b]'), children: error ?? statusLabel }), error && (_jsxs("button", { type: "button", onClick: handleRetry, disabled: !audioUrl, className: "inline-flex items-center gap-1.5 rounded-full border border-red-200 bg-red-50 px-3 py-1 text-xs font-bold text-red-600 transition-colors hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-50", "aria-label": "Retry loading audio", children: [_jsx(RefreshCw, { size: 13 }), "Retry"] }))] }), _jsxs("div", { className: "mt-2 grid grid-cols-[46px_1fr_46px] items-center gap-2.5 text-sm font-bold tabular-nums text-[#0f1730]", children: [_jsx("span", { children: formatTime(currentTime) }), _jsxs("div", { className: "relative", children: [_jsx("div", { className: "pointer-events-none absolute inset-y-0 left-0 rounded-full", style: {
111
- width: `${progressPercent}%`,
112
- background: 'linear-gradient(90deg, #1f7cff, #0b57d0)',
113
- } }), _jsx("input", { type: "range", min: "0", max: duration || 0, step: "0.1", value: currentTime, onChange: handleProgressChange, disabled: isLoading || !audioUrl || !!error, className: "relative z-10 h-2 w-full cursor-pointer appearance-none rounded-full bg-[#dfe9f6] accent-[#0b57d0] disabled:cursor-not-allowed disabled:opacity-50", "aria-label": "Audio progress" })] }), _jsx("span", { children: duration > 0 ? formatTime(duration) : '00:00' })] })] })] }));
114
- }
1
+ "use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useRef as t,useState as a,useEffect as n}from"react";import{Play as i,Pause as d,RotateCcw as l,RefreshCw as s}from"lucide-react";import{usePresignedFileUrl as o}from"../../../shared/lib/hooks/usePresignedFileUrl";import{cn as c}from"../../../shared/lib/utils";export function EnglishCertificationAudioPlayer({audioUrl:u,partNumber:p=0,isLoading:m=!1}){const b=t(null),{previewUrl:f}=o(u||void 0),[g,x]=a(!1),[h,v]=a(0),[y,N]=a(0),[w,L]=a(null),E=e=>{if(!e||isNaN(e))return"00:00";const r=Math.floor(e/60),t=Math.floor(e%60);return`${r.toString().padStart(2,"0")}:${t.toString().padStart(2,"0")}`};n(()=>{const e=b.current;if(!e)return;const r=()=>v(e.currentTime),t=()=>{N(e.duration),L(null)},a=()=>x(!1),n=()=>{L("Failed to load audio file"),x(!1)};return e.addEventListener("timeupdate",r),e.addEventListener("loadedmetadata",t),e.addEventListener("ended",a),e.addEventListener("error",n),()=>{e.removeEventListener("timeupdate",r),e.removeEventListener("loadedmetadata",t),e.removeEventListener("ended",a),e.removeEventListener("error",n)}},[]),n(()=>{b.current&&f&&(b.current.src=f,v(0),x(!1),L(null))},[f]);const _=y>0?h/y*100:0,k=w?"Audio unavailable":m?"Loading...":g?"Playing...":h>0?"Paused":"Ready to play";return r("div",{className:"grid w-full grid-cols-[64px_1fr] items-center gap-4 rounded-[10px] border border-[#d9e4f2] bg-white/90 p-4 shadow-[0_5px_16px_rgba(13,71,161,0.08)] sm:gap-[18px] sm:px-5",children:[e("audio",{ref:b}),e("button",{type:"button",onClick:()=>{b.current&&(g?(b.current.pause(),x(!1)):(b.current.play().catch(e=>{L(`Failed to play audio: ${e.message}`)}),x(!0)))},disabled:m||!u||!!w,className:"grid h-14 w-14 place-items-center rounded-full text-2xl text-white shadow-[0_12px_22px_rgba(11,87,208,0.22)] transition-transform hover:scale-[1.03] disabled:cursor-not-allowed disabled:opacity-50",style:{background:"linear-gradient(180deg, #1f7cff, #0b57d0)"},"aria-label":g?"Pause audio":"Play audio",children:g?e(d,{size:24}):e(i,{size:24,className:"ml-0.5"})}),r("div",{className:"min-w-0",children:[r("div",{className:"flex items-center justify-between gap-3",children:[r("span",{className:"text-[18px] font-black text-[#0f1730]",children:["Listening Audio",p>0&&` · Part ${p}`]}),e("button",{type:"button",onClick:()=>{b.current&&(b.current.currentTime=0,b.current.pause(),x(!1),v(0))},disabled:m||!u||!!w,className:"flex h-8 w-8 items-center justify-center rounded-full border border-[#d9e4f2] text-[#0b57d0] transition-colors hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-50","aria-label":"Replay from start",children:e(l,{size:16})})]}),r("div",{className:"mt-1 flex items-center gap-2.5",children:[e("span",{className:c("text-sm font-bold",w?"text-red-500":"text-[#64748b]"),children:w??k}),w&&r("button",{type:"button",onClick:()=>{const e=b.current;e&&f&&(L(null),v(0),x(!1),e.src=f,e.load())},disabled:!u,className:"inline-flex items-center gap-1.5 rounded-full border border-red-200 bg-red-50 px-3 py-1 text-xs font-bold text-red-600 transition-colors hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-50","aria-label":"Retry loading audio",children:[e(s,{size:13}),"Retry"]})]}),r("div",{className:"mt-2 grid grid-cols-[46px_1fr_46px] items-center gap-2.5 text-sm font-bold tabular-nums text-[#0f1730]",children:[e("span",{children:E(h)}),r("div",{className:"relative",children:[e("div",{className:"pointer-events-none absolute inset-y-0 left-0 rounded-full",style:{width:`${_}%`,background:"linear-gradient(90deg, #1f7cff, #0b57d0)"}}),e("input",{type:"range",min:"0",max:y||0,step:"0.1",value:h,onChange:e=>{const r=parseFloat(e.target.value);b.current&&(b.current.currentTime=r,v(r))},disabled:m||!u||!!w,className:"relative z-10 h-2 w-full cursor-pointer appearance-none rounded-full bg-[#dfe9f6] accent-[#0b57d0] disabled:cursor-not-allowed disabled:opacity-50","aria-label":"Audio progress"})]}),e("span",{children:y>0?E(y):"00:00"})]})]})]})}
@@ -1,11 +1 @@
1
- 'use client';
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import Image from 'next/image';
4
- import { cn } from '../../../shared/lib/utils';
5
- /**
6
- * Ocean Edu brand lockup: horizontal logo image.
7
- * Used in the English Certification header.
8
- */
9
- export function EnglishCertificationBrand({ className }) {
10
- return (_jsx("div", { className: cn('flex items-center', className), "aria-label": "Ocean Edu", children: _jsx(Image, { src: "/images/logo-ngang.png", alt: "Ocean Edu", width: 180, height: 40, priority: true, className: "h-10 w-auto object-contain" }) }));
11
- }
1
+ "use client";import{jsx as e}from"react/jsx-runtime";import i from"next/image";import{cn as a}from"../../../shared/lib/utils";export function EnglishCertificationBrand({className:t}){return e("div",{className:a("flex items-center",t),"aria-label":"Ocean Edu",children:e(i,{src:"/images/logo-ngang.png",alt:"Ocean Edu",width:180,height:40,priority:!0,className:"h-10 w-auto object-contain"})})}
@@ -1,21 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState } from 'react';
4
- import { List } from 'lucide-react';
5
- import { Sheet, SheetContent, SheetHeader, SheetTitle, } from '../../../components/ui/sheet';
6
- import { EnglishCertificationHeader, } from './EnglishCertificationHeader';
7
- import { EnglishCertificationNavigationPanel } from './EnglishCertificationNavigationPanel';
8
- import { EnglishCertificationFooterBar, } from './EnglishCertificationFooterBar';
9
- import { ENGLISH_CERT_APP_BACKGROUND, ENGLISH_CERT_PRIMARY_GRADIENT, } from './english-certification-theme';
10
- /**
11
- * Ocean Edu branded exam-taking shell for English Certification exams.
12
- *
13
- * Layout: branded sticky header · main content + collapsible "Answer Sheet"
14
- * sidebar · branded wave footer with navigation actions. On mobile the sidebar
15
- * is replaced by a bottom-sheet opened via a floating button.
16
- */
17
- export function EnglishCertificationExamLayout({ children, headerProps, navigationProps, footerProps, }) {
18
- const [isSidebarOpen, setIsSidebarOpen] = useState(true);
19
- const [isMobileSheetOpen, setIsMobileSheetOpen] = useState(false);
20
- return (_jsxs("div", { className: "flex h-screen flex-col overflow-hidden text-[#0f1730]", style: { background: ENGLISH_CERT_APP_BACKGROUND }, children: [_jsx(EnglishCertificationHeader, { ...headerProps }), _jsxs("div", { className: "flex flex-1 gap-4 overflow-hidden px-4 py-4 lg:px-6", children: [_jsx("main", { id: "exam-main-scroll-area", className: "min-w-0 flex-1 overflow-y-auto", children: children }), isSidebarOpen && (_jsxs("aside", { className: "hidden w-[360px] shrink-0 flex-col overflow-hidden rounded-xl border border-[#d9e4f2] bg-white/90 shadow-[0_14px_35px_rgba(13,71,161,0.12)] backdrop-blur-md lg:flex", "aria-label": "Answer sheet navigation", children: [_jsxs("div", { className: "flex h-14 items-center justify-between px-5 text-white", style: { background: ENGLISH_CERT_PRIMARY_GRADIENT }, children: [_jsx("h3", { className: "text-[18px] font-black tracking-[0.1em]", children: "ANSWER SHEET" }), _jsx("button", { type: "button", onClick: () => setIsSidebarOpen(false), className: "text-sm font-bold text-white/90 hover:text-white", children: "Hide \u226A" })] }), _jsx("div", { className: "flex-1 overflow-hidden", children: _jsx(EnglishCertificationNavigationPanel, { ...navigationProps }) })] })), !isSidebarOpen && (_jsx("button", { type: "button", onClick: () => setIsSidebarOpen(true), className: "fixed right-0 top-24 z-30 hidden rounded-l-lg border border-r-0 border-[#d9e4f2] bg-white px-2 py-3 text-sm font-bold text-[#0b57d0] shadow-sm hover:bg-blue-50 lg:block", "aria-label": "Show answer sheet", children: "\u226B" }))] }), footerProps && _jsx(EnglishCertificationFooterBar, { ...footerProps }), _jsx("button", { type: "button", onClick: () => setIsMobileSheetOpen(true), className: "fixed bottom-24 right-4 z-30 flex h-12 w-12 items-center justify-center rounded-full text-white shadow-lg lg:hidden", style: { background: ENGLISH_CERT_PRIMARY_GRADIENT }, "aria-label": "Open answer sheet", children: _jsx(List, { className: "h-5 w-5" }) }), _jsx(Sheet, { open: isMobileSheetOpen, onOpenChange: setIsMobileSheetOpen, children: _jsxs(SheetContent, { side: "bottom", className: "h-[78vh] p-0", children: [_jsx(SheetHeader, { className: "flex h-14 flex-row items-center px-5 text-white", style: { background: ENGLISH_CERT_PRIMARY_GRADIENT }, children: _jsx(SheetTitle, { className: "text-[18px] font-black tracking-[0.1em] text-white", children: "ANSWER SHEET" }) }), _jsx("div", { className: "h-[calc(78vh-3.5rem)] overflow-hidden", children: _jsx(EnglishCertificationNavigationPanel, { ...navigationProps }) })] }) })] }));
21
- }
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useState as i}from"react";import{List as r}from"lucide-react";import{Sheet as o,SheetContent as l,SheetHeader as n,SheetTitle as a}from"../../../components/ui/sheet";import{EnglishCertificationHeader as s}from"./EnglishCertificationHeader";import{EnglishCertificationNavigationPanel as d}from"./EnglishCertificationNavigationPanel";import{EnglishCertificationFooterBar as c}from"./EnglishCertificationFooterBar";import{ENGLISH_CERT_APP_BACKGROUND as h,ENGLISH_CERT_PRIMARY_GRADIENT as m}from"./english-certification-theme";export function EnglishCertificationExamLayout({children:f,headerProps:x,navigationProps:p,footerProps:b}){const[g,w]=i(!0),[u,v]=i(!1);return t("div",{className:"flex h-screen flex-col overflow-hidden text-[#0f1730]",style:{background:h},children:[e(s,{...x}),t("div",{className:"flex flex-1 gap-4 overflow-hidden px-4 py-4 lg:px-6",children:[e("main",{id:"exam-main-scroll-area",className:"min-w-0 flex-1 overflow-y-auto",children:f}),g&&t("aside",{className:"hidden w-[360px] shrink-0 flex-col overflow-hidden rounded-xl border border-[#d9e4f2] bg-white/90 shadow-[0_14px_35px_rgba(13,71,161,0.12)] backdrop-blur-md lg:flex","aria-label":"Answer sheet navigation",children:[t("div",{className:"flex h-14 items-center justify-between px-5 text-white",style:{background:m},children:[e("h3",{className:"text-[18px] font-black tracking-[0.1em]",children:"ANSWER SHEET"}),e("button",{type:"button",onClick:()=>w(!1),className:"text-sm font-bold text-white/90 hover:text-white",children:"Hide ≪"})]}),e("div",{className:"flex-1 overflow-hidden",children:e(d,{...p})})]}),!g&&e("button",{type:"button",onClick:()=>w(!0),className:"fixed right-0 top-24 z-30 hidden rounded-l-lg border border-r-0 border-[#d9e4f2] bg-white px-2 py-3 text-sm font-bold text-[#0b57d0] shadow-sm hover:bg-blue-50 lg:block","aria-label":"Show answer sheet",children:"≫"})]}),b&&e(c,{...b}),e("button",{type:"button",onClick:()=>v(!0),className:"fixed bottom-24 right-4 z-30 flex h-12 w-12 items-center justify-center rounded-full text-white shadow-lg lg:hidden",style:{background:m},"aria-label":"Open answer sheet",children:e(r,{className:"h-5 w-5"})}),e(o,{open:u,onOpenChange:v,children:t(l,{side:"bottom",className:"h-[78vh] p-0",children:[e(n,{className:"flex h-14 flex-row items-center px-5 text-white",style:{background:m},children:e(a,{className:"text-[18px] font-black tracking-[0.1em] text-white",children:"ANSWER SHEET"})}),e("div",{className:"h-[calc(78vh-3.5rem)] overflow-hidden",children:e(d,{...p})})]})})]})}
@@ -1,15 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { cn } from '../../../shared/lib/utils';
4
- /**
5
- * Shared Ocean Edu "Mark for Review" control.
6
- *
7
- * Single source of truth for the mark/flag affordance so the bookmark flag
8
- * looks identical on single-question and grouped-reading screens.
9
- */
10
- export function EnglishCertificationFlagButton({ isFlagged, onToggle, size = 'default', className, }) {
11
- const isCompact = size === 'sm';
12
- return (_jsxs("button", { type: "button", onClick: onToggle, className: cn('inline-flex items-center gap-1.5 rounded-md border font-bold transition-colors', isCompact ? 'px-2 py-1 text-xs' : 'px-3 py-1.5 text-sm', isFlagged
13
- ? 'border-[#ff8a00] bg-[#fff1db] text-[#ff8a00]'
14
- : 'border-[#d9e4f2] bg-white text-[#64748b] hover:text-[#ff8a00]', className), "aria-label": isFlagged ? 'Unmark for review' : 'Mark for review', "aria-pressed": isFlagged, children: [_jsx("span", { className: "h-[14px] w-2 shrink-0 bg-current", style: { clipPath: 'polygon(0 0, 100% 0, 100% 100%, 50% 70%, 0 100%)' }, "aria-hidden": "true" }), !isCompact && _jsx("span", { children: isFlagged ? 'Marked' : 'Mark' })] }));
15
- }
1
+ "use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{cn as t}from"../../../shared/lib/utils";export function EnglishCertificationFlagButton({isFlagged:a,onToggle:s,size:i="default",className:n}){const o="sm"===i;return r("button",{type:"button",onClick:s,className:t("inline-flex items-center gap-1.5 rounded-md border font-bold transition-colors",o?"px-2 py-1 text-xs":"px-3 py-1.5 text-sm",a?"border-[#ff8a00] bg-[#fff1db] text-[#ff8a00]":"border-[#d9e4f2] bg-white text-[#64748b] hover:text-[#ff8a00]",n),"aria-label":a?"Unmark for review":"Mark for review","aria-pressed":a,children:[e("span",{className:"h-[14px] w-2 shrink-0 bg-current",style:{clipPath:"polygon(0 0, 100% 0, 100% 100%, 50% 70%, 0 100%)"},"aria-hidden":"true"}),!o&&e("span",{children:a?"Marked":"Mark"})]})}
@@ -1,19 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { cn } from '../../../shared/lib/utils';
4
- import { ENGLISH_CERT_FOOTER_NOTE, ENGLISH_CERT_FOOTER_WAVE, ENGLISH_CERT_PRIMARY_GRADIENT, } from './english-certification-theme';
5
- /**
6
- * Branded Ocean Edu footer action bar for English Certification exam taking.
7
- *
8
- * Provides Previous / Clear Answer / Mark for Review / Next actions plus the
9
- * decorative wave, brand lockup and encouragement note.
10
- */
11
- export function EnglishCertificationFooterBar({ onPrevious, onClear, onToggleMark, onNext, isMarked = false, isPreviousDisabled = false, isNextDisabled = false, showSecondaryActions = true, }) {
12
- return (_jsxs("footer", { className: "sticky bottom-0 z-40 flex flex-col gap-2 px-4 pt-3 lg:px-7", style: {
13
- background: `linear-gradient(180deg, rgba(255,255,255,0.94) 0 calc(100% - 64px), transparent calc(100% - 64px)), ${ENGLISH_CERT_FOOTER_WAVE} bottom/100% 64px no-repeat`,
14
- }, children: [_jsxs("div", { className: cn('grid grid-cols-2 items-center gap-3 lg:gap-12', showSecondaryActions
15
- ? 'lg:grid-cols-[240px_190px_1fr_200px]'
16
- : 'mx-auto w-full max-w-[520px] lg:grid-cols-2'), children: [_jsx("button", { type: "button", onClick: onPrevious, disabled: isPreviousDisabled, className: cn('h-[42px] rounded-md border border-[#d9e4f2] bg-white text-[15px] font-black text-[#0a3d9a] shadow-[0_5px_16px_rgba(13,71,161,0.08)] disabled:cursor-not-allowed disabled:opacity-50', !showSecondaryActions && 'lg:w-[200px] lg:justify-self-start'), children: "\u2190 \u00A0 Previous" }), showSecondaryActions && onClear && (_jsx("button", { type: "button", onClick: onClear, className: "h-[42px] rounded-md border border-[#d9e4f2] bg-white text-[15px] font-black text-[#0a3d9a] shadow-[0_5px_16px_rgba(13,71,161,0.08)]", children: "\u25C6 \u00A0 Clear Answer" })), showSecondaryActions && onToggleMark && (_jsxs("button", { type: "button", onClick: onToggleMark, className: cn('h-[42px] rounded-md border bg-white text-[15px] font-black shadow-[0_5px_16px_rgba(13,71,161,0.08)] lg:w-[220px] lg:justify-self-center', isMarked
17
- ? 'border-[#ff8a00] text-[#ff8a00]'
18
- : 'border-[#d9e4f2] text-[#0a3d9a]'), children: ["\u25AE \u00A0 ", isMarked ? 'Unmark Review' : 'Mark for Review'] })), _jsx("button", { type: "button", onClick: onNext, disabled: isNextDisabled, className: cn('h-[42px] rounded-md border border-[#0b57d0] text-[15px] font-black text-white shadow-[0_5px_16px_rgba(13,71,161,0.08)] disabled:cursor-not-allowed disabled:opacity-50', !showSecondaryActions && 'lg:w-[200px] lg:justify-self-end'), style: { background: ENGLISH_CERT_PRIMARY_GRADIENT }, children: "Next \u00A0 \u2192" })] }), _jsxs("div", { className: "pointer-events-none hidden h-9 items-center gap-4 text-white lg:flex", children: [_jsxs("div", { className: "flex shrink-0 items-center gap-2 font-black tracking-wide", children: [_jsx("span", { className: "grid h-[25px] w-[25px] place-items-center rounded-full border-2 border-white text-xs", children: "\u2726" }), "OCEAN EDU"] }), _jsxs("span", { className: "min-w-0 flex-1 text-center text-[13px] font-bold", children: ["\u3030 \u00A0 ", ENGLISH_CERT_FOOTER_NOTE] })] })] }));
19
- }
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{cn as r}from"../../../shared/lib/utils";import{ENGLISH_CERT_FOOTER_NOTE as a,ENGLISH_CERT_FOOTER_WAVE as o,ENGLISH_CERT_PRIMARY_GRADIENT as l}from"./english-certification-theme";export function EnglishCertificationFooterBar({onPrevious:d,onClear:i,onToggleMark:s,onNext:n,isMarked:c=!1,isPreviousDisabled:x=!1,isNextDisabled:p=!1,showSecondaryActions:b=!0}){return t("footer",{className:"sticky bottom-0 z-40 flex flex-col gap-2 px-4 pt-3 lg:px-7",style:{background:`linear-gradient(180deg, rgba(255,255,255,0.94) 0 calc(100% - 64px), transparent calc(100% - 64px)), ${o} bottom/100% 64px no-repeat`},children:[t("div",{className:r("grid grid-cols-2 items-center gap-3 lg:gap-12",b?"lg:grid-cols-[240px_190px_1fr_200px]":"mx-auto w-full max-w-[520px] lg:grid-cols-2"),children:[e("button",{type:"button",onClick:d,disabled:x,className:r("h-[42px] rounded-md border border-[#d9e4f2] bg-white text-[15px] font-black text-[#0a3d9a] shadow-[0_5px_16px_rgba(13,71,161,0.08)] disabled:cursor-not-allowed disabled:opacity-50",!b&&"lg:w-[200px] lg:justify-self-start"),children:"←   Previous"}),b&&i&&e("button",{type:"button",onClick:i,className:"h-[42px] rounded-md border border-[#d9e4f2] bg-white text-[15px] font-black text-[#0a3d9a] shadow-[0_5px_16px_rgba(13,71,161,0.08)]",children:"◆   Clear Answer"}),b&&s&&t("button",{type:"button",onClick:s,className:r("h-[42px] rounded-md border bg-white text-[15px] font-black shadow-[0_5px_16px_rgba(13,71,161,0.08)] lg:w-[220px] lg:justify-self-center",c?"border-[#ff8a00] text-[#ff8a00]":"border-[#d9e4f2] text-[#0a3d9a]"),children:["▮   ",c?"Unmark Review":"Mark for Review"]}),e("button",{type:"button",onClick:n,disabled:p,className:r("h-[42px] rounded-md border border-[#0b57d0] text-[15px] font-black text-white shadow-[0_5px_16px_rgba(13,71,161,0.08)] disabled:cursor-not-allowed disabled:opacity-50",!b&&"lg:w-[200px] lg:justify-self-end"),style:{background:l},children:"Next   →"})]}),t("div",{className:"pointer-events-none hidden h-9 items-center gap-4 text-white lg:flex",children:[t("div",{className:"flex shrink-0 items-center gap-2 font-black tracking-wide",children:[e("span",{className:"grid h-[25px] w-[25px] place-items-center rounded-full border-2 border-white text-xs",children:"✦"}),"OCEAN EDU"]}),t("span",{className:"min-w-0 flex-1 text-center text-[13px] font-bold",children:["〰   ",a]})]})]})}
@@ -1,148 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { FileText, Headphones, Info } from 'lucide-react';
4
- import { EnglishCertificationQuestionCard } from './EnglishCertificationQuestionCard';
5
- import { EnglishCertificationPassagePanel } from './EnglishCertificationPassagePanel';
6
- import { EnglishCertificationReadingSection } from './EnglishCertificationReadingSection';
7
- import { EnglishCertificationAudioPlayer } from './EnglishCertificationAudioPlayer';
8
- import { ENGLISH_CERT_CONTENT_WIDTH_CLASS } from './english-certification-theme';
9
- import { isHtmlContent, stripInstruction } from './grouped-question-utils';
10
- /** Fixed split layout: equal-width passage / questions columns. */
11
- const SPLIT_VIEW_GRID = 'minmax(0, 1fr) minmax(0, 1fr)';
12
- /**
13
- * Splits a grouped reading part into its individual document groups while
14
- * preserving the original question order. Each document keeps its own passage
15
- * payload so the layout can stack them vertically on a single screen.
16
- */
17
- function buildDocumentGroups(part, sharedInstruction) {
18
- const order = [];
19
- const byGroup = new Map();
20
- part.questions.forEach((question) => {
21
- const groupId = question.group_id ?? question.id;
22
- if (!byGroup.has(groupId)) {
23
- byGroup.set(groupId, []);
24
- order.push(groupId);
25
- }
26
- byGroup.get(groupId).push(question);
27
- });
28
- return order.map((groupId) => {
29
- const questions = byGroup.get(groupId);
30
- const payload = questions[0]?.questionGroup?.payload ?? {};
31
- const rawPassage = payload.content ?? payload.passage ?? '';
32
- const passage = stripInstruction(rawPassage, sharedInstruction);
33
- const passageImageUrl = payload.imageUrl ?? payload.passageImageUrl ?? '';
34
- return {
35
- groupId,
36
- questions,
37
- passage,
38
- // API exposes the group heading as `title`; older Reading payloads use
39
- // `passageTitle`. Fall back to `title` so grouped MCQ headings show.
40
- passageTitle: payload.passageTitle ?? payload.title ?? '',
41
- passageSubtitle: payload.subtitle ?? '',
42
- passageImageUrl,
43
- hasPassageContent: Boolean(passage.trim()) || Boolean(passageImageUrl),
44
- };
45
- });
46
- }
47
- /**
48
- * Resolves the shared part instruction.
49
- *
50
- * Priority:
51
- * 1. `questionGroup.payload.instructions` — used by some grouped question types.
52
- * 2. `partInstructions` — the template-level instruction from the QuestionPart
53
- * (e.g. Part 6 TOEIC where the instruction lives on the template part).
54
- * 3. `content.question` of the first question — legacy fallback.
55
- */
56
- function resolveSharedInstruction(part, partInstructions) {
57
- const first = part.questions[0];
58
- return (first?.questionGroup?.payload?.instructions ??
59
- (partInstructions?.trim() || undefined) ??
60
- first?.content?.question ??
61
- '').trim();
62
- }
63
- /**
64
- * Ocean Edu branded split-view renderer for grouped reading parts.
65
- *
66
- * Renders every document of the part stacked vertically. Each document shows a
67
- * reading passage (title, optional image, body, source bar) on the left and its
68
- * MCQ questions on the right in a balanced 50/50 split.
69
- */
70
- export function EnglishCertificationGroupedQuestion({ part, partConfig, answers, flaggedQuestions, onAnswerChange, onToggleFlag, isReviewMode = false, isListeningPart = false, partAudioUrl, }) {
71
- const startIndex = partConfig.startIndex ?? 0;
72
- // Part-level instruction (e.g. "Read the texts that follow…") is shared by
73
- // every document. Render it once at the top and strip it from each passage
74
- // (the API prepends it to every document's content) to avoid duplication.
75
- // Prefer part.instructions (set by groupQuestionsByPart from the template)
76
- // which covers TOEIC Part 6 where the instruction lives on the template part
77
- // rather than inside the question group payload.
78
- const sharedInstruction = resolveSharedInstruction(part, part.instructions ?? partConfig.instructions);
79
- // When the part carries template sections, render every question in a single
80
- // document so the section loop in renderQuestions runs once and stacks each
81
- // sub-section. Splitting by group_id here would duplicate the section headings
82
- // and break per-section dispatch.
83
- //
84
- // The document still carries the shared passage when present: IELTS Reading
85
- // parts ship a DOCUMENT group whose passage lives on each question's
86
- // `questionGroup.payload` (content/title/subtitle). Surfacing it flips
87
- // `hasPassageContent` to true so the split branch shows passage-left /
88
- // questions-right (TOEIC Part 7 style). Mixed Listening parts (SECTION 2:
89
- // CHOOSE + MATCH + WORD_FILL) carry no passage, so this stays false and the
90
- // full-width no-passage branch is preserved.
91
- const buildSectionedDocument = () => {
92
- const payload = (part.questions[0]?.questionGroup?.payload ?? {});
93
- const rawPassage = (payload.content ?? payload.passage ?? '');
94
- const passage = stripInstruction(rawPassage, sharedInstruction);
95
- const passageImageUrl = (payload.imageUrl ?? payload.passageImageUrl ?? '');
96
- return {
97
- groupId: part.partId,
98
- questions: part.questions,
99
- passage,
100
- passageTitle: (payload.passageTitle ?? payload.title ?? ''),
101
- passageSubtitle: (payload.subtitle ?? ''),
102
- passageImageUrl,
103
- hasPassageContent: Boolean(passage.trim()) || Boolean(passageImageUrl),
104
- };
105
- };
106
- const documents = part.sections?.length
107
- ? [buildSectionedDocument()]
108
- : buildDocumentGroups(part, sharedInstruction);
109
- if (!documents.length) {
110
- return (_jsx("div", { className: "flex h-full items-center justify-center rounded-xl border border-red-200 bg-red-50 p-8 text-red-600", children: "No questions found for this part" }));
111
- }
112
- const showAudio = isListeningPart && Boolean(partAudioUrl);
113
- /** Renders a document's question list (used by both split and full-width layouts). */
114
- const renderQuestions = (document) => {
115
- if (part.sections?.length) {
116
- return (_jsx("div", { className: "flex-1 space-y-6 px-4 py-4", children: part.sections.map((section, sectionIdx) => {
117
- const sectionQuestions = document.questions.filter((question) => {
118
- const pos = part.questions.indexOf(question);
119
- return (pos >= section.startIndex - startIndex &&
120
- pos <= section.endIndex - startIndex);
121
- });
122
- if (!sectionQuestions.length)
123
- return null;
124
- return (_jsx(EnglishCertificationReadingSection, { part: part, section: section, questions: sectionQuestions, startIndex: startIndex, answers: answers, flaggedQuestions: flaggedQuestions, onAnswerChange: onAnswerChange, onToggleFlag: onToggleFlag, isReviewMode: isReviewMode }, `${document.groupId}-section-${sectionIdx}`));
125
- }) }));
126
- }
127
- return (_jsx("div", { className: "flex-1 space-y-3 px-4 py-4", children: document.questions.map((question) => {
128
- const questionNumber = part.questions.indexOf(question) + 1;
129
- const globalQuestionNumber = startIndex + questionNumber;
130
- return (_jsx(EnglishCertificationQuestionCard, { question: question, globalQuestionNumber: globalQuestionNumber, answer: answers[question.id], isFlagged: flaggedQuestions.has(question.id), sharedInstruction: sharedInstruction, onAnswerChange: onAnswerChange, onToggleFlag: onToggleFlag, isReviewMode: isReviewMode }, question.id));
131
- }) }));
132
- };
133
- return (_jsxs("div", { className: "flex h-full flex-col gap-6 overflow-y-auto pb-4", children: [showAudio && (_jsx("div", { className: `${ENGLISH_CERT_CONTENT_WIDTH_CLASS} w-full self-center`, children: _jsx(EnglishCertificationAudioPlayer, { audioUrl: partAudioUrl, partNumber: part.partNo }) })), sharedInstruction && (_jsx("div", { className: `${ENGLISH_CERT_CONTENT_WIDTH_CLASS} w-full self-center`, children: _jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-[#bfd8f8] bg-gradient-to-r from-[#eef5ff] to-[#f7fbff] px-5 py-4 shadow-[0_5px_16px_rgba(13,71,161,0.08)] backdrop-blur-md", children: [_jsx("span", { className: "mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-full bg-[#1167e8] text-white", children: isListeningPart ? (_jsx(Headphones, { className: "h-3.5 w-3.5" })) : (_jsx(Info, { className: "h-3.5 w-3.5" })) }), _jsxs("div", { className: "min-w-0", children: [part.name && (_jsx("div", { className: "mb-1 text-xs font-black uppercase tracking-wide text-[#0b57d0]", children: part.name })), _jsx("p", { className: "text-base font-semibold leading-relaxed text-[#0f1730]", children: sharedInstruction })] })] }) })), documents.map((document) => {
134
- const firstNumber = part.questions.indexOf(document.questions[0]) + startIndex + 1;
135
- const lastNumber = part.questions.indexOf(document.questions[document.questions.length - 1]) +
136
- startIndex +
137
- 1;
138
- const passageIsHtml = isHtmlContent(document.passage);
139
- // No-passage group (e.g. Listening grouped MCQ that only carries a
140
- // heading like "Choose the correct letter, A, B or C"): hide the empty
141
- // passage panel and render the group title as a banner above the
142
- // full-width questions.
143
- if (!document.hasPassageContent) {
144
- return (_jsxs("div", { className: `${ENGLISH_CERT_CONTENT_WIDTH_CLASS} flex w-full flex-col gap-3 self-center`, children: [_jsxs("div", { className: "flex items-center gap-4 rounded-xl border border-[#d9e4f2] bg-white/90 px-5 py-4 shadow-[0_5px_16px_rgba(13,71,161,0.08)] backdrop-blur-md", children: [_jsx("span", { className: "grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-[#eaf2ff] text-[#1167e8]", children: _jsx(FileText, { className: "h-6 w-6" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-md border border-[#bfd8f8] bg-[#eaf2ff] px-2.5 py-1 text-xs font-bold text-[#0b57d0]", children: ["Questions ", firstNumber, "\u2013", lastNumber] }), document.passageTitle && (_jsx("p", { className: "mt-2 text-lg font-bold leading-tight text-[#0f1730]", children: document.passageTitle })), document.passageSubtitle && (_jsx("p", { className: "mt-1 text-[15px] font-medium text-[#475569]", children: document.passageSubtitle }))] }), document.passageImageUrl && (_jsx("img", { src: document.passageImageUrl, alt: document.passageTitle || 'Illustration', className: "h-20 w-32 shrink-0 rounded-lg object-contain" }))] }), _jsx("section", { className: "flex flex-col overflow-hidden rounded-xl border border-[#d9e4f2] bg-white/90 shadow-[0_14px_35px_rgba(13,71,161,0.12)] backdrop-blur-md", children: renderQuestions(document) })] }, document.groupId));
145
- }
146
- return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { className: "flex flex-wrap items-center gap-3 rounded-xl border border-[#d9e4f2] bg-white/85 px-4 py-3 shadow-[0_5px_16px_rgba(13,71,161,0.08)] backdrop-blur-md", children: _jsxs("div", { className: "flex items-center gap-2.5 rounded-md border border-[#bfd8f8] bg-white/70 px-3 py-1.5 text-base font-bold text-[#0b57d0]", children: [_jsx("span", { className: "grid h-[18px] w-[18px] place-items-center rounded-full bg-[#1167e8] text-white", children: _jsx(Info, { className: "h-3 w-3" }) }), "Questions ", firstNumber, "\u2013", lastNumber, " refer to the following passage."] }) }), _jsxs("div", { className: "grid gap-4", style: { gridTemplateColumns: SPLIT_VIEW_GRID }, children: [_jsx("div", { className: "max-h-[calc(100vh-220px)] min-h-0 overflow-y-auto", children: _jsx(EnglishCertificationPassagePanel, { passage: document.passage, passageTitle: document.passageTitle, passageSubtitle: document.passageSubtitle, passageImageUrl: document.passageImageUrl, passageIsHtml: passageIsHtml, partName: part.name ?? '' }) }), _jsx("section", { className: "flex max-h-[calc(100vh-220px)] min-h-0 flex-col overflow-y-auto rounded-xl border border-[#d9e4f2] bg-white/90 shadow-[0_14px_35px_rgba(13,71,161,0.12)] backdrop-blur-md", children: renderQuestions(document) })] })] }, document.groupId));
147
- })] }));
148
- }
1
+ "use client";import{jsx as e,jsxs as s}from"react/jsx-runtime";import{FileText as a,Headphones as t,Info as r}from"lucide-react";import{EnglishCertificationQuestionCard as i}from"./EnglishCertificationQuestionCard";import{EnglishCertificationPassagePanel as n}from"./EnglishCertificationPassagePanel";import{EnglishCertificationReadingSection as l}from"./EnglishCertificationReadingSection";import{EnglishCertificationAudioPlayer as o}from"./EnglishCertificationAudioPlayer";import{ENGLISH_CERT_CONTENT_WIDTH_CLASS as d}from"./english-certification-theme";import{isHtmlContent as c,stripInstruction as p}from"./grouped-question-utils";export function EnglishCertificationGroupedQuestion({part:g,partConfig:u,answers:m,flaggedQuestions:f,onAnswerChange:h,onToggleFlag:x,isReviewMode:b=!1,isListeningPart:w=!1,partAudioUrl:N}){const v=u.startIndex??0,q=function(e,s){const a=e.questions[0];return(a?.questionGroup?.payload?.instructions??(s?.trim()||void 0)??a?.content?.question??"").trim()}(g,g.instructions??u.instructions),I=g.sections?.length?[(()=>{const e=g.questions[0]?.questionGroup?.payload??{},s=e.content??e.passage??"",a=p(s,q),t=e.imageUrl??e.passageImageUrl??"";return{groupId:g.partId,questions:g.questions,passage:a,passageTitle:e.passageTitle??e.title??"",passageSubtitle:e.subtitle??"",passageImageUrl:t,hasPassageContent:Boolean(a.trim())||Boolean(t)}})()]:function(e,s){const a=[],t=new Map;return e.questions.forEach(e=>{const s=e.group_id??e.id;t.has(s)||(t.set(s,[]),a.push(s)),t.get(s).push(e)}),a.map(e=>{const a=t.get(e),r=a[0]?.questionGroup?.payload??{},i=r.content??r.passage??"",n=p(i,s),l=r.imageUrl??r.passageImageUrl??"";return{groupId:e,questions:a,passage:n,passageTitle:r.passageTitle??r.title??"",passageSubtitle:r.subtitle??"",passageImageUrl:l,hasPassageContent:Boolean(n.trim())||Boolean(l)}})}(g,q);if(!I.length)return e("div",{className:"flex h-full items-center justify-center rounded-xl border border-red-200 bg-red-50 p-8 text-red-600",children:"No questions found for this part"});const y=w&&Boolean(N),_=s=>g.sections?.length?e("div",{className:"flex-1 space-y-6 px-4 py-4",children:g.sections.map((a,t)=>{const r=s.questions.filter(e=>{const s=g.questions.indexOf(e);return s>=a.startIndex-v&&s<=a.endIndex-v});return r.length?e(l,{part:g,section:a,questions:r,startIndex:v,answers:m,flaggedQuestions:f,onAnswerChange:h,onToggleFlag:x,isReviewMode:b},`${s.groupId}-section-${t}`):null})}):e("div",{className:"flex-1 space-y-3 px-4 py-4",children:s.questions.map(s=>{const a=g.questions.indexOf(s)+1;return e(i,{question:s,globalQuestionNumber:v+a,answer:m[s.id],isFlagged:f.has(s.id),sharedInstruction:q,onAnswerChange:h,onToggleFlag:x,isReviewMode:b},s.id)})});return s("div",{className:"flex h-full flex-col gap-6 overflow-y-auto pb-4",children:[y&&e("div",{className:`${d} w-full self-center`,children:e(o,{audioUrl:N,partNumber:g.partNo})}),q&&e("div",{className:`${d} w-full self-center`,children:s("div",{className:"flex items-start gap-3 rounded-xl border border-[#bfd8f8] bg-gradient-to-r from-[#eef5ff] to-[#f7fbff] px-5 py-4 shadow-[0_5px_16px_rgba(13,71,161,0.08)] backdrop-blur-md",children:[e("span",{className:"mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-full bg-[#1167e8] text-white",children:e(w?t:r,{className:"h-3.5 w-3.5"})}),s("div",{className:"min-w-0",children:[g.name&&e("div",{className:"mb-1 text-xs font-black uppercase tracking-wide text-[#0b57d0]",children:g.name}),e("p",{className:"text-base font-semibold leading-relaxed text-[#0f1730]",children:q})]})]})}),I.map(t=>{const i=g.questions.indexOf(t.questions[0])+v+1,l=g.questions.indexOf(t.questions[t.questions.length-1])+v+1,o=c(t.passage);return t.hasPassageContent?s("div",{className:"flex flex-col gap-3",children:[e("div",{className:"flex flex-wrap items-center gap-3 rounded-xl border border-[#d9e4f2] bg-white/85 px-4 py-3 shadow-[0_5px_16px_rgba(13,71,161,0.08)] backdrop-blur-md",children:s("div",{className:"flex items-center gap-2.5 rounded-md border border-[#bfd8f8] bg-white/70 px-3 py-1.5 text-base font-bold text-[#0b57d0]",children:[e("span",{className:"grid h-[18px] w-[18px] place-items-center rounded-full bg-[#1167e8] text-white",children:e(r,{className:"h-3 w-3"})}),"Questions ",i,"–",l," refer to the following passage."]})}),s("div",{className:"grid gap-4",style:{gridTemplateColumns:"minmax(0, 1fr) minmax(0, 1fr)"},children:[e("div",{className:"max-h-[calc(100vh-220px)] min-h-0 overflow-y-auto",children:e(n,{passage:t.passage,passageTitle:t.passageTitle,passageSubtitle:t.passageSubtitle,passageImageUrl:t.passageImageUrl,passageIsHtml:o,partName:g.name??""})}),e("section",{className:"flex max-h-[calc(100vh-220px)] min-h-0 flex-col overflow-y-auto rounded-xl border border-[#d9e4f2] bg-white/90 shadow-[0_14px_35px_rgba(13,71,161,0.12)] backdrop-blur-md",children:_(t)})]})]},t.groupId):s("div",{className:`${d} flex w-full flex-col gap-3 self-center`,children:[s("div",{className:"flex items-center gap-4 rounded-xl border border-[#d9e4f2] bg-white/90 px-5 py-4 shadow-[0_5px_16px_rgba(13,71,161,0.08)] backdrop-blur-md",children:[e("span",{className:"grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-[#eaf2ff] text-[#1167e8]",children:e(a,{className:"h-6 w-6"})}),s("div",{className:"min-w-0 flex-1",children:[s("span",{className:"inline-flex items-center gap-1.5 rounded-md border border-[#bfd8f8] bg-[#eaf2ff] px-2.5 py-1 text-xs font-bold text-[#0b57d0]",children:["Questions ",i,"–",l]}),t.passageTitle&&e("p",{className:"mt-2 text-lg font-bold leading-tight text-[#0f1730]",children:t.passageTitle}),t.passageSubtitle&&e("p",{className:"mt-1 text-[15px] font-medium text-[#475569]",children:t.passageSubtitle})]}),t.passageImageUrl&&e("img",{src:t.passageImageUrl,alt:t.passageTitle||"Illustration",className:"h-20 w-32 shrink-0 rounded-lg object-contain"})]}),e("section",{className:"flex flex-col overflow-hidden rounded-xl border border-[#d9e4f2] bg-white/90 shadow-[0_14px_35px_rgba(13,71,161,0.12)] backdrop-blur-md",children:_(t)})]},t.groupId)})]})}