@tinyweb_dev/oe-exam-sdk 0.1.1 → 0.1.2

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 (485) 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.js +1 -98
  4. package/dist/api/exam-questions.types.js +1 -1
  5. package/dist/api/exam-taking.js +1 -73
  6. package/dist/api/exam-taking.types.js +1 -1
  7. package/dist/api/index.js +1 -6
  8. package/dist/api/result-review.js +1 -50
  9. package/dist/api/result-review.types.js +1 -1
  10. package/dist/components/CueCardEditor.js +1 -58
  11. package/dist/components/common/ResolvedImage.js +1 -15
  12. package/dist/components/exams/CreateExamInfoForm.js +1 -35
  13. package/dist/components/exams/CreateExamPageContainer.js +1 -103
  14. package/dist/components/exams/ExamCreator.js +1 -89
  15. package/dist/components/exams/ExamPartTabs.js +1 -40
  16. package/dist/components/exams/ExamQuestionGrid.js +1 -18
  17. package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
  18. package/dist/components/exams/PartAudioUploader.js +1 -9
  19. package/dist/components/exams/TemplateSelector.js +1 -66
  20. package/dist/components/exams/api.js +1 -3
  21. package/dist/components/exams/create-exam.validation.js +1 -18
  22. package/dist/components/exams/exam-template.utils.js +1 -18
  23. package/dist/components/exams/index.js +1 -10
  24. package/dist/components/exams/take/ExamTakingPageContainer.js +1 -153
  25. package/dist/components/exams/take/GenericQuestionRenderer.js +1 -14
  26. package/dist/components/exams/take/exam-taking.utils.js +1 -88
  27. package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -24
  28. package/dist/components/exams/take/index.js +1 -2
  29. package/dist/components/exams/take/types.js +1 -1
  30. package/dist/components/index.js +1 -42
  31. package/dist/components/questions/_shared/config/question-types.config.js +1 -333
  32. package/dist/components/questions/_shared/constants/color-palette.js +1 -21
  33. package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +1 -105
  34. package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +1 -222
  35. package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +1 -140
  36. package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +1 -123
  37. package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +1 -50
  38. package/dist/components/questions/_shared/hooks/useQuestionFormState.js +1 -54
  39. package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +1 -123
  40. package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +1 -121
  41. package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +1 -70
  42. package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +1 -194
  43. package/dist/components/questions/_shared/index.js +1 -7
  44. package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
  45. package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
  46. package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +1 -3
  47. package/dist/components/questions/_shared/types/color-region.config.js +1 -1
  48. package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
  49. package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
  50. package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
  51. package/dist/components/questions/_shared/types/index.js +1 -12
  52. package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
  53. package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
  54. package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
  55. package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
  56. package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
  57. package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
  58. package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
  59. package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
  60. package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +1 -3
  61. package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
  62. package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +1 -3
  63. package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
  64. package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
  65. package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
  66. package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
  67. package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
  68. package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
  69. package/dist/components/questions/_shared/types/question-group.type.js +1 -11
  70. package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
  71. package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
  72. package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
  73. package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
  74. package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
  75. package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
  76. package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
  77. package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
  78. package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
  79. package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
  80. package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +1 -3
  81. package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
  82. package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
  83. package/dist/components/questions/_shared/utils/answer-builder.js +1 -79
  84. package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +1 -39
  85. package/dist/components/questions/_shared/utils/id-generator.js +1 -3
  86. package/dist/components/questions/_shared/utils/question-validation.js +1 -21
  87. package/dist/components/questions/components/ColorNode.js +1 -85
  88. package/dist/components/questions/components/ColorRegionClient.js +1 -545
  89. package/dist/components/questions/components/ColorRegionCreator.js +1 -420
  90. package/dist/components/questions/components/ColorRegionNode.js +1 -59
  91. package/dist/components/questions/components/CreatorGuide.js +1 -103
  92. package/dist/components/questions/components/NameNode.js +1 -26
  93. package/dist/components/questions/components/QuestionBankOption.js +1 -46
  94. package/dist/components/questions/components/QuestionBasicInfoForm.js +1 -9
  95. package/dist/components/questions/components/QuestionCommonFields.js +1 -33
  96. package/dist/components/questions/components/QuestionSearchDropdown.js +1 -258
  97. package/dist/components/questions/components/QuestionTypeSelector.js +1 -32
  98. package/dist/components/questions/components/QuestionTypeSpecificForms.js +1 -6
  99. package/dist/components/questions/components/RegionNode.js +1 -94
  100. package/dist/components/questions/components/index.js +1 -14
  101. package/dist/components/questions/creator/QuestionCreator.js +1 -90
  102. package/dist/components/questions/creator/QuestionGroupCreator.js +1 -119
  103. package/dist/components/questions/creator/dedicated-component-router.js +1 -79
  104. package/dist/components/questions/creator/index.js +1 -3
  105. package/dist/components/questions/creator/question-type-registry.js +1 -80
  106. package/dist/components/questions/groups/ArticlesGroupCard.js +1 -121
  107. package/dist/components/questions/groups/BasicQuestionGroupCard.js +1 -50
  108. package/dist/components/questions/groups/DocumentGroupCard.js +1 -114
  109. package/dist/components/questions/groups/MatchAnswerGroupCard.js +1 -25
  110. package/dist/components/questions/index.js +1 -10
  111. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +1 -37
  112. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +1 -351
  113. package/dist/components/questions/types/answer-the-question/register.js +1 -67
  114. package/dist/components/questions/types/answer-the-question/transform.js +1 -36
  115. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +1 -141
  116. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +1 -118
  117. package/dist/components/questions/types/arrange-letters-into-words/index.js +1 -2
  118. package/dist/components/questions/types/arrange-letters-into-words/register.js +1 -4
  119. package/dist/components/questions/types/arrange-letters-into-words/transform.js +1 -24
  120. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +1 -64
  121. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +1 -169
  122. package/dist/components/questions/types/choose-correct-adjective/index.js +1 -2
  123. package/dist/components/questions/types/choose-correct-adjective/register.js +1 -55
  124. package/dist/components/questions/types/choose-correct-adjective/transform.js +1 -16
  125. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -131
  126. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -581
  127. package/dist/components/questions/types/choose-the-correct-answer/index.js +1 -2
  128. package/dist/components/questions/types/choose-the-correct-answer/register.js +1 -101
  129. package/dist/components/questions/types/choose-the-correct-answer/transform.js +1 -43
  130. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -275
  131. package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +1 -448
  132. package/dist/components/questions/types/fill-in-blank/index.js +1 -2
  133. package/dist/components/questions/types/fill-in-blank/register.js +1 -138
  134. package/dist/components/questions/types/fill-in-blank/transform.js +1 -59
  135. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +1 -117
  136. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +1 -671
  137. package/dist/components/questions/types/fill-missing-words-in-grid/index.js +1 -2
  138. package/dist/components/questions/types/fill-missing-words-in-grid/register.js +1 -37
  139. package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +1 -15
  140. package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +1 -262
  141. package/dist/components/questions/types/gn-speaking-interview/register.js +1 -121
  142. package/dist/components/questions/types/gn-speaking-interview/transform.js +1 -48
  143. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +1 -16
  144. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +1 -27
  145. package/dist/components/questions/types/image-object-matching/index.js +1 -2
  146. package/dist/components/questions/types/image-object-matching/register.js +1 -3
  147. package/dist/components/questions/types/image-object-matching/transform.js +1 -2
  148. package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +1 -102
  149. package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +1 -344
  150. package/dist/components/questions/types/label-the-picture/index.js +1 -2
  151. package/dist/components/questions/types/label-the-picture/register.js +1 -54
  152. package/dist/components/questions/types/label-the-picture/transform.js +1 -28
  153. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +1 -78
  154. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +1 -232
  155. package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +1 -2
  156. package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +1 -82
  157. package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +1 -27
  158. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +1 -70
  159. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +1 -386
  160. package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +1 -2
  161. package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +1 -100
  162. package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +1 -33
  163. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +1 -106
  164. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +1 -457
  165. package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +1 -2
  166. package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +1 -110
  167. package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +1 -35
  168. package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +1 -8
  169. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +1 -22
  170. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +1 -291
  171. package/dist/components/questions/types/listen-and-speak-answer/index.js +1 -2
  172. package/dist/components/questions/types/listen-and-speak-answer/register.js +1 -84
  173. package/dist/components/questions/types/listen-and-speak-answer/transform.js +1 -27
  174. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +1 -32
  175. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +1 -295
  176. package/dist/components/questions/types/listen-and-speak-compare-images/register.js +1 -79
  177. package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +1 -27
  178. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +1 -34
  179. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +1 -285
  180. package/dist/components/questions/types/listen-and-speak-image-group/register.js +1 -81
  181. package/dist/components/questions/types/listen-and-speak-image-group/transform.js +1 -26
  182. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +1 -38
  183. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +1 -385
  184. package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +1 -87
  185. package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +1 -32
  186. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +1 -26
  187. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +1 -247
  188. package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +1 -59
  189. package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +1 -18
  190. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +1 -26
  191. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +1 -256
  192. package/dist/components/questions/types/listen-and-speak-question-list/register.js +1 -81
  193. package/dist/components/questions/types/listen-and-speak-question-list/transform.js +1 -26
  194. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +1 -26
  195. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +1 -230
  196. package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +1 -56
  197. package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +1 -16
  198. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +1 -72
  199. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +1 -230
  200. package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
  201. package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +1 -66
  202. package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +1 -42
  203. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +1 -96
  204. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +1 -238
  205. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +1 -2
  206. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +1 -76
  207. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +1 -19
  208. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +1 -134
  209. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +1 -617
  210. package/dist/components/questions/types/look-picture-fill-word-hint/index.js +1 -2
  211. package/dist/components/questions/types/look-picture-fill-word-hint/register.js +1 -82
  212. package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +1 -29
  213. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +1 -38
  214. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +1 -359
  215. package/dist/components/questions/types/match-by-writing-answer/index.js +1 -3
  216. package/dist/components/questions/types/match-by-writing-answer/register.js +1 -58
  217. package/dist/components/questions/types/match-by-writing-answer/transform.js +1 -61
  218. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +1 -16
  219. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +1 -23
  220. package/dist/components/questions/types/read-and-color-objects/index.js +1 -2
  221. package/dist/components/questions/types/read-and-color-objects/register.js +1 -3
  222. package/dist/components/questions/types/read-and-color-objects/transform.js +1 -36
  223. package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +1 -125
  224. package/dist/components/questions/types/read-displayed-content/register.js +1 -64
  225. package/dist/components/questions/types/read-displayed-content/transform.js +1 -12
  226. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +1 -64
  227. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +1 -511
  228. package/dist/components/questions/types/read-passage-and-answer-questions/index.js +1 -2
  229. package/dist/components/questions/types/read-passage-and-answer-questions/register.js +1 -58
  230. package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +1 -28
  231. package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +1 -373
  232. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -41
  233. package/dist/components/questions/types/speaking-conversation/register.js +1 -110
  234. package/dist/components/questions/types/speaking-conversation/transform.js +1 -34
  235. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -74
  236. package/dist/components/questions/types/speaking-cue-card/register.js +1 -52
  237. package/dist/components/questions/types/speaking-cue-card/transform.js +1 -12
  238. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +1 -10
  239. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +1 -161
  240. package/dist/components/questions/types/speaking-describe-image/index.js +1 -2
  241. package/dist/components/questions/types/speaking-describe-image/register.js +1 -69
  242. package/dist/components/questions/types/speaking-describe-image/transform.js +1 -16
  243. package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +1 -234
  244. package/dist/components/questions/types/spontaneous-qa/register.js +1 -93
  245. package/dist/components/questions/types/spontaneous-qa/transform.js +1 -34
  246. package/dist/components/questions/types/true-false/TrueFalseClient.js +1 -75
  247. package/dist/components/questions/types/true-false/TrueFalseCreator.js +1 -244
  248. package/dist/components/questions/types/true-false/index.js +1 -2
  249. package/dist/components/questions/types/true-false/register.js +1 -77
  250. package/dist/components/questions/types/true-false/transform.js +1 -32
  251. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +1 -233
  252. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +1 -631
  253. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +1 -28
  254. package/dist/components/questions/types/word-fill-paragraph/index.js +1 -2
  255. package/dist/components/questions/types/word-fill-paragraph/register.js +1 -136
  256. package/dist/components/questions/types/word-fill-paragraph/transform.js +1 -56
  257. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +1 -138
  258. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +1 -261
  259. package/dist/components/questions/types/word-fill-structured-form/index.js +1 -2
  260. package/dist/components/questions/types/word-fill-structured-form/register.js +1 -67
  261. package/dist/components/questions/types/word-fill-structured-form/transform.js +1 -40
  262. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +1 -84
  263. package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +1 -192
  264. package/dist/components/questions/types/word-ordering/index.js +1 -2
  265. package/dist/components/questions/types/word-ordering/register.js +1 -46
  266. package/dist/components/questions/types/word-ordering/transform.js +1 -24
  267. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +1 -98
  268. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +1 -320
  269. package/dist/components/questions/types/write-a-short-letter/index.js +1 -2
  270. package/dist/components/questions/types/write-a-short-letter/register.js +1 -68
  271. package/dist/components/questions/types/write-a-short-letter/transform.js +1 -25
  272. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +1 -61
  273. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +1 -196
  274. package/dist/components/questions/types/write-correct-verb-form/index.js +1 -2
  275. package/dist/components/questions/types/write-correct-verb-form/register.js +1 -62
  276. package/dist/components/questions/types/write-correct-verb-form/transform.js +1 -17
  277. package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +1 -22
  278. package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +1 -174
  279. package/dist/components/questions/types/write-sentences/index.js +1 -2
  280. package/dist/components/questions/types/write-sentences/register.js +1 -58
  281. package/dist/components/questions/types/write-sentences/transform.js +1 -28
  282. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +1 -96
  283. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +1 -161
  284. package/dist/components/questions/types/write-short-paragraph/index.js +1 -2
  285. package/dist/components/questions/types/write-short-paragraph/register.js +1 -4
  286. package/dist/components/questions/types/write-short-paragraph/transform.js +1 -21
  287. package/dist/components/questions/viewer/QuestionGroupViewer.js +1 -36
  288. package/dist/components/questions/viewer/QuestionViewer.js +1 -446
  289. package/dist/components/questions/viewer/index.js +1 -2
  290. package/dist/components/results/ResultReviewPageContainer.js +1 -98
  291. package/dist/components/results/ReviewQuestionRenderer.js +1 -72
  292. package/dist/components/results/index.js +1 -2
  293. package/dist/components/results/result-review.utils.js +1 -6
  294. package/dist/components/results/review-data.js +1 -150
  295. package/dist/components/results/review-renderer.utils.js +1 -97
  296. package/dist/components/results/review-types.js +1 -1
  297. package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +1 -138
  298. package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +1 -12
  299. package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +1 -18
  300. package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +1 -11
  301. package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +1 -11
  302. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +1 -9
  303. package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +1 -13
  304. package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +1 -8
  305. package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +1 -6
  306. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +1 -5
  307. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +1 -9
  308. package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +1 -41
  309. package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -25
  310. package/dist/components/themes/cambridge-yle/index.js +1 -13
  311. package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +1 -24
  312. package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +1 -88
  313. package/dist/components/themes/english-certification/EcWordFillParagraph.js +1 -55
  314. package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +1 -156
  315. package/dist/components/themes/english-certification/EcWritingTask.js +1 -34
  316. package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -114
  317. package/dist/components/themes/english-certification/EnglishCertificationBrand.js +1 -11
  318. package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +1 -21
  319. package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +1 -15
  320. package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +1 -19
  321. package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +1 -148
  322. package/dist/components/themes/english-certification/EnglishCertificationHeader.js +1 -31
  323. package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +1 -71
  324. package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +1 -33
  325. package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +1 -16
  326. package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +1 -22
  327. package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +1 -37
  328. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -72
  329. package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +1 -90
  330. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +1 -18
  331. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +1 -29
  332. package/dist/components/themes/english-certification/english-certification-theme.js +1 -58
  333. package/dist/components/themes/english-certification/grouped-question-utils.js +1 -218
  334. package/dist/components/themes/english-certification/index.js +1 -21
  335. package/dist/components/themes/index.js +1 -6
  336. package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +1 -170
  337. package/dist/components/themes/summer-sky/SummerSkyLayout.js +1 -221
  338. package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +1 -18
  339. package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +1 -194
  340. package/dist/components/themes/types.js +1 -1
  341. package/dist/components/ui/alert-dialog.js +1 -26
  342. package/dist/components/ui/alert.js +1 -22
  343. package/dist/components/ui/avatar.js +1 -12
  344. package/dist/components/ui/badge.js +1 -38
  345. package/dist/components/ui/button.js +1 -35
  346. package/dist/components/ui/calendar.js +1 -76
  347. package/dist/components/ui/card.js +1 -16
  348. package/dist/components/ui/carousel.js +1 -91
  349. package/dist/components/ui/checkbox.js +1 -9
  350. package/dist/components/ui/collapsible.js +1 -13
  351. package/dist/components/ui/command.js +1 -34
  352. package/dist/components/ui/confirm-dialog.js +1 -49
  353. package/dist/components/ui/dialog.js +1 -23
  354. package/dist/components/ui/drawer.js +1 -35
  355. package/dist/components/ui/dropdown-menu.js +1 -33
  356. package/dist/components/ui/file-upload.js +1 -155
  357. package/dist/components/ui/form.js +1 -62
  358. package/dist/components/ui/hover-card.js +1 -10
  359. package/dist/components/ui/image-preview.js +1 -29
  360. package/dist/components/ui/input.js +1 -8
  361. package/dist/components/ui/label.js +1 -9
  362. package/dist/components/ui/language-switcher.js +1 -15
  363. package/dist/components/ui/multi-select.js +1 -90
  364. package/dist/components/ui/points-input.js +1 -33
  365. package/dist/components/ui/popover.js +1 -26
  366. package/dist/components/ui/progress.js +1 -16
  367. package/dist/components/ui/radio-group.js +1 -12
  368. package/dist/components/ui/scroll-area.js +1 -11
  369. package/dist/components/ui/select.js +1 -38
  370. package/dist/components/ui/separator.js +1 -8
  371. package/dist/components/ui/sheet.js +1 -40
  372. package/dist/components/ui/single-select.js +1 -76
  373. package/dist/components/ui/skeleton.js +1 -6
  374. package/dist/components/ui/switch.js +1 -8
  375. package/dist/components/ui/table.js +1 -20
  376. package/dist/components/ui/tabs.js +1 -17
  377. package/dist/components/ui/textarea.js +1 -8
  378. package/dist/components/ui/tooltip.js +1 -11
  379. package/dist/index.js +1 -19
  380. package/dist/mcp/config.js +1 -32
  381. package/dist/mcp/http-client.js +1 -48
  382. package/dist/mcp/index.js +1 -29
  383. package/dist/mcp/tools/_helpers.js +1 -25
  384. package/dist/mcp/tools/exams.js +1 -439
  385. package/dist/mcp/tools/index.js +1 -6
  386. package/dist/mcp/tools/results.js +1 -19
  387. package/dist/shared/constants/ApiConstant.js +1 -276
  388. package/dist/shared/constants/QueryKeyConstant.js +1 -112
  389. package/dist/shared/constants/exam-level.enum.js +1 -41
  390. package/dist/shared/constants/landings/placement-test.constants.js +1 -431
  391. package/dist/shared/constants/question-skills.js +1 -267
  392. package/dist/shared/constants/routes.js +1 -81
  393. package/dist/shared/index.js +1 -7
  394. package/dist/shared/lib/hooks/index.js +1 -10
  395. package/dist/shared/lib/hooks/useAlertDialog.js +1 -57
  396. package/dist/shared/lib/hooks/useAudioPlayCounter.js +1 -30
  397. package/dist/shared/lib/hooks/useAudioPreview.js +1 -81
  398. package/dist/shared/lib/hooks/useAudioUpload.js +1 -24
  399. package/dist/shared/lib/hooks/useConfirmDialog.js +1 -89
  400. package/dist/shared/lib/hooks/useDebouncedCallback.js +1 -32
  401. package/dist/shared/lib/hooks/useDelayedAutoPlay.js +1 -12
  402. package/dist/shared/lib/hooks/useFileUpload.js +1 -24
  403. package/dist/shared/lib/hooks/useImageUrl.js +1 -5
  404. package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -10
  405. package/dist/shared/lib/hooks/useReactHookForm.js +1 -31
  406. package/dist/shared/lib/hooks/useToast.js +1 -62
  407. package/dist/shared/lib/i18n/index.js +1 -54
  408. package/dist/shared/lib/i18n/messages/en/admin.js +1 -2104
  409. package/dist/shared/lib/i18n/messages/en/auth.js +1 -86
  410. package/dist/shared/lib/i18n/messages/en/common.js +1 -140
  411. package/dist/shared/lib/i18n/messages/en/file-upload.js +1 -18
  412. package/dist/shared/lib/i18n/messages/en/student.js +1 -133
  413. package/dist/shared/lib/i18n/messages/en/terms.js +1 -37
  414. package/dist/shared/lib/i18n/messages/en.js +1 -15
  415. package/dist/shared/lib/i18n/messages/vi/admin.js +1 -2104
  416. package/dist/shared/lib/i18n/messages/vi/auth.js +1 -86
  417. package/dist/shared/lib/i18n/messages/vi/common.js +1 -140
  418. package/dist/shared/lib/i18n/messages/vi/file-upload.js +1 -18
  419. package/dist/shared/lib/i18n/messages/vi/student.js +1 -133
  420. package/dist/shared/lib/i18n/messages/vi/terms.js +1 -37
  421. package/dist/shared/lib/i18n/messages/vi.js +1 -15
  422. package/dist/shared/lib/i18n/types.js +1 -1
  423. package/dist/shared/lib/stores/examQuestionStore.js +1 -212
  424. package/dist/shared/lib/stores/localeStore.js +1 -10
  425. package/dist/shared/lib/utils/public-page.js +1 -44
  426. package/dist/shared/lib/utils/question-transform-types.js +1 -1
  427. package/dist/shared/lib/utils/question-transform.js +1 -174
  428. package/dist/shared/lib/utils.js +1 -168
  429. package/dist/shared/lib/validation/choose-correct-answer.validation.js +1 -111
  430. package/dist/shared/lib/validation/label-the-picture.validation.js +1 -19
  431. package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +1 -46
  432. package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +1 -21
  433. package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +1 -34
  434. package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +1 -40
  435. package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +1 -45
  436. package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +1 -19
  437. package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +1 -39
  438. package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +1 -32
  439. package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +1 -19
  440. package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +1 -28
  441. package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +1 -12
  442. package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +1 -47
  443. package/dist/shared/lib/validation/read-and-color-objects.validation.js +1 -29
  444. package/dist/shared/lib/validation/read-displayed-content.validation.js +1 -28
  445. package/dist/shared/lib/validation/speaking-conversation.validation.js +1 -75
  446. package/dist/shared/lib/validation/speaking-describe-image.validation.js +1 -31
  447. package/dist/shared/lib/validation/spontaneous-qa.validation.js +1 -48
  448. package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +1 -38
  449. package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +1 -39
  450. package/dist/shared/lib/validation/word-fill-paragraph.validation.js +1 -43
  451. package/dist/shared/lib/validation/word-ordering.validation.js +1 -33
  452. package/dist/shared/types/branch.types.js +1 -6
  453. package/dist/shared/types/common.types.js +1 -50
  454. package/dist/shared/types/entities/exam-schedule.types.js +1 -5
  455. package/dist/shared/types/entities/exam.types.js +1 -1
  456. package/dist/shared/types/entities/question.types.js +1 -4
  457. package/dist/shared/types/entities/result.types.js +1 -4
  458. package/dist/shared/types/entities/user.types.js +1 -4
  459. package/dist/shared/types/exam-taking.types.js +1 -2
  460. package/dist/shared/types/history.types.js +1 -34
  461. package/dist/shared/types/index.js +1 -1
  462. package/dist/shared/types/questions/choose-correct-adjective.js +1 -4
  463. package/dist/shared/types/questions/choose-the-correct-answer.js +1 -9
  464. package/dist/shared/types/questions/fill-missing-words-in-grid.js +1 -11
  465. package/dist/shared/types/questions/index.js +1 -40
  466. package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
  467. package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
  468. package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
  469. package/dist/shared/types/questions/listen-and-speak-info-exchange.js +1 -3
  470. package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +1 -3
  471. package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
  472. package/dist/shared/types/questions/listen-and-speak-with-story-images.js +1 -3
  473. package/dist/shared/types/questions/look-picture-choose-correct-answer.js +1 -5
  474. package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +1 -9
  475. package/dist/shared/types/questions/look-picture-fill-word-hint.js +1 -7
  476. package/dist/shared/types/questions/read-displayed-content.js +1 -1
  477. package/dist/shared/types/questions/speaking-conversation.js +1 -7
  478. package/dist/shared/types/questions/speaking-describe-image.js +1 -1
  479. package/dist/shared/types/questions/spontaneous-qa.js +1 -1
  480. package/dist/shared/types/questions/write-correct-verb-form.js +1 -12
  481. package/dist/shared/types/role.types.js +1 -1
  482. package/dist/shared/types/structure-management.js +1 -28
  483. package/dist/shared/types/student.types.js +1 -54
  484. package/dist/shared/types/teacher-dashboard-rooms.js +1 -2
  485. package/package.json +4 -2
@@ -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)})]})}