@tinyweb_dev/oe-exam-sdk 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (514) hide show
  1. package/dist/api/exam-create.js +1 -179
  2. package/dist/api/exam-create.types.js +1 -1
  3. package/dist/api/exam-questions.d.ts +1 -1
  4. package/dist/api/exam-questions.js +1 -98
  5. package/dist/api/exam-questions.types.d.ts +45 -1
  6. package/dist/api/exam-questions.types.js +1 -1
  7. package/dist/api/exam-taking.js +1 -73
  8. package/dist/api/exam-taking.types.js +1 -1
  9. package/dist/api/index.js +1 -6
  10. package/dist/api/result-review.js +1 -50
  11. package/dist/api/result-review.types.js +1 -1
  12. package/dist/components/CueCardEditor.js +1 -58
  13. package/dist/components/common/ResolvedImage.js +1 -15
  14. package/dist/components/exams/CompletionDialog.d.ts +8 -0
  15. package/dist/components/exams/CompletionDialog.js +1 -0
  16. package/dist/components/exams/CreateExamInfoForm.d.ts +4 -1
  17. package/dist/components/exams/CreateExamInfoForm.js +1 -35
  18. package/dist/components/exams/CreateExamPageContainer.d.ts +3 -0
  19. package/dist/components/exams/CreateExamPageContainer.js +1 -103
  20. package/dist/components/exams/ExamCreator.d.ts +9 -3
  21. package/dist/components/exams/ExamCreator.js +1 -89
  22. package/dist/components/exams/ExamPartTabs.d.ts +1 -1
  23. package/dist/components/exams/ExamPartTabs.js +1 -40
  24. package/dist/components/exams/ExamPreviewDialog.d.ts +20 -0
  25. package/dist/components/exams/ExamPreviewDialog.js +1 -0
  26. package/dist/components/exams/ExamQuestionGrid.d.ts +1 -1
  27. package/dist/components/exams/ExamQuestionGrid.js +1 -18
  28. package/dist/components/exams/ExamQuestionsPageContainer.d.ts +3 -0
  29. package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
  30. package/dist/components/exams/ExamViewDialog.d.ts +24 -0
  31. package/dist/components/exams/ExamViewDialog.js +1 -0
  32. package/dist/components/exams/PartAudioUploader.js +1 -9
  33. package/dist/components/exams/TemplateSelector.js +1 -66
  34. package/dist/components/exams/UnsavedChangesDialog.d.ts +8 -0
  35. package/dist/components/exams/UnsavedChangesDialog.js +1 -0
  36. package/dist/components/exams/api.d.ts +1 -1
  37. package/dist/components/exams/api.js +1 -3
  38. package/dist/components/exams/create-exam.texts.d.ts +58 -0
  39. package/dist/components/exams/create-exam.texts.js +1 -0
  40. package/dist/components/exams/create-exam.validation.js +1 -18
  41. package/dist/components/exams/exam-question-status.d.ts +9 -0
  42. package/dist/components/exams/exam-question-status.js +1 -0
  43. package/dist/components/exams/exam-template.utils.js +1 -18
  44. package/dist/components/exams/index.d.ts +11 -1
  45. package/dist/components/exams/index.js +1 -10
  46. package/dist/components/exams/take/ExamTakingPageContainer.js +1 -153
  47. package/dist/components/exams/take/GenericQuestionRenderer.js +1 -14
  48. package/dist/components/exams/take/exam-taking.utils.js +1 -88
  49. package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -24
  50. package/dist/components/exams/take/index.js +1 -2
  51. package/dist/components/exams/take/types.js +1 -1
  52. package/dist/components/index.js +1 -42
  53. package/dist/components/questions/_shared/config/question-types.config.js +1 -333
  54. package/dist/components/questions/_shared/constants/color-palette.js +1 -21
  55. package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +1 -105
  56. package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +1 -222
  57. package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +1 -140
  58. package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +1 -123
  59. package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +1 -50
  60. package/dist/components/questions/_shared/hooks/useQuestionFormState.js +1 -54
  61. package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +1 -123
  62. package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +1 -121
  63. package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +1 -70
  64. package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +1 -194
  65. package/dist/components/questions/_shared/index.js +1 -7
  66. package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
  67. package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
  68. package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +1 -3
  69. package/dist/components/questions/_shared/types/color-region.config.js +1 -1
  70. package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
  71. package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
  72. package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
  73. package/dist/components/questions/_shared/types/index.js +1 -12
  74. package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
  75. package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
  76. package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
  77. package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
  78. package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
  79. package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
  80. package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
  81. package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
  82. package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +1 -3
  83. package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
  84. package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +1 -3
  85. package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
  86. package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
  87. package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
  88. package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
  89. package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
  90. package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
  91. package/dist/components/questions/_shared/types/question-group.type.js +1 -11
  92. package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
  93. package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
  94. package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
  95. package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
  96. package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
  97. package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
  98. package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
  99. package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
  100. package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
  101. package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
  102. package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +1 -3
  103. package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
  104. package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
  105. package/dist/components/questions/_shared/utils/answer-builder.js +1 -79
  106. package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +1 -39
  107. package/dist/components/questions/_shared/utils/id-generator.js +1 -3
  108. package/dist/components/questions/_shared/utils/question-validation.js +1 -21
  109. package/dist/components/questions/components/ColorNode.js +1 -85
  110. package/dist/components/questions/components/ColorRegionClient.js +1 -545
  111. package/dist/components/questions/components/ColorRegionCreator.js +1 -420
  112. package/dist/components/questions/components/ColorRegionNode.js +1 -59
  113. package/dist/components/questions/components/CreatorGuide.js +1 -103
  114. package/dist/components/questions/components/NameNode.js +1 -26
  115. package/dist/components/questions/components/QuestionBankOption.js +1 -46
  116. package/dist/components/questions/components/QuestionBasicInfoForm.js +1 -9
  117. package/dist/components/questions/components/QuestionCommonFields.js +1 -33
  118. package/dist/components/questions/components/QuestionSearchDropdown.js +1 -258
  119. package/dist/components/questions/components/QuestionTypeSelector.js +1 -32
  120. package/dist/components/questions/components/QuestionTypeSpecificForms.js +1 -6
  121. package/dist/components/questions/components/RegionNode.js +1 -94
  122. package/dist/components/questions/components/index.js +1 -14
  123. package/dist/components/questions/creator/QuestionCreator.js +1 -90
  124. package/dist/components/questions/creator/QuestionGroupCreator.js +1 -119
  125. package/dist/components/questions/creator/dedicated-component-router.js +1 -79
  126. package/dist/components/questions/creator/index.js +1 -3
  127. package/dist/components/questions/creator/question-type-registry.js +1 -80
  128. package/dist/components/questions/groups/ArticlesGroupCard.js +1 -121
  129. package/dist/components/questions/groups/BasicQuestionGroupCard.js +1 -50
  130. package/dist/components/questions/groups/DocumentGroupCard.js +1 -114
  131. package/dist/components/questions/groups/MatchAnswerGroupCard.js +1 -25
  132. package/dist/components/questions/index.js +1 -10
  133. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +1 -37
  134. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +1 -351
  135. package/dist/components/questions/types/answer-the-question/register.js +1 -67
  136. package/dist/components/questions/types/answer-the-question/transform.js +1 -36
  137. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +1 -141
  138. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +1 -118
  139. package/dist/components/questions/types/arrange-letters-into-words/index.js +1 -2
  140. package/dist/components/questions/types/arrange-letters-into-words/register.js +1 -4
  141. package/dist/components/questions/types/arrange-letters-into-words/transform.js +1 -24
  142. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +1 -64
  143. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +1 -169
  144. package/dist/components/questions/types/choose-correct-adjective/index.js +1 -2
  145. package/dist/components/questions/types/choose-correct-adjective/register.js +1 -55
  146. package/dist/components/questions/types/choose-correct-adjective/transform.js +1 -16
  147. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -131
  148. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -581
  149. package/dist/components/questions/types/choose-the-correct-answer/index.js +1 -2
  150. package/dist/components/questions/types/choose-the-correct-answer/register.js +1 -101
  151. package/dist/components/questions/types/choose-the-correct-answer/transform.js +1 -43
  152. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -275
  153. package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +1 -448
  154. package/dist/components/questions/types/fill-in-blank/index.js +1 -2
  155. package/dist/components/questions/types/fill-in-blank/register.js +1 -138
  156. package/dist/components/questions/types/fill-in-blank/transform.js +1 -59
  157. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +1 -117
  158. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +1 -671
  159. package/dist/components/questions/types/fill-missing-words-in-grid/index.js +1 -2
  160. package/dist/components/questions/types/fill-missing-words-in-grid/register.js +1 -37
  161. package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +1 -15
  162. package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +1 -262
  163. package/dist/components/questions/types/gn-speaking-interview/register.js +1 -121
  164. package/dist/components/questions/types/gn-speaking-interview/transform.js +1 -48
  165. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +1 -16
  166. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +1 -27
  167. package/dist/components/questions/types/image-object-matching/index.js +1 -2
  168. package/dist/components/questions/types/image-object-matching/register.js +1 -3
  169. package/dist/components/questions/types/image-object-matching/transform.js +1 -2
  170. package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +1 -102
  171. package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +1 -344
  172. package/dist/components/questions/types/label-the-picture/index.js +1 -2
  173. package/dist/components/questions/types/label-the-picture/register.js +1 -54
  174. package/dist/components/questions/types/label-the-picture/transform.js +1 -28
  175. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +1 -78
  176. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +1 -232
  177. package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +1 -2
  178. package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +1 -82
  179. package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +1 -27
  180. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +1 -70
  181. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +1 -386
  182. package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +1 -2
  183. package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +1 -100
  184. package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +1 -33
  185. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +1 -106
  186. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +1 -457
  187. package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +1 -2
  188. package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +1 -110
  189. package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +1 -35
  190. package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +1 -8
  191. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +1 -22
  192. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +1 -291
  193. package/dist/components/questions/types/listen-and-speak-answer/index.js +1 -2
  194. package/dist/components/questions/types/listen-and-speak-answer/register.js +1 -84
  195. package/dist/components/questions/types/listen-and-speak-answer/transform.js +1 -27
  196. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +1 -32
  197. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +1 -295
  198. package/dist/components/questions/types/listen-and-speak-compare-images/register.js +1 -79
  199. package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +1 -27
  200. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +1 -34
  201. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +1 -285
  202. package/dist/components/questions/types/listen-and-speak-image-group/register.js +1 -81
  203. package/dist/components/questions/types/listen-and-speak-image-group/transform.js +1 -26
  204. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +1 -38
  205. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +1 -385
  206. package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +1 -87
  207. package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +1 -32
  208. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +1 -26
  209. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +1 -247
  210. package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +1 -59
  211. package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +1 -18
  212. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +1 -26
  213. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +1 -256
  214. package/dist/components/questions/types/listen-and-speak-question-list/register.js +1 -81
  215. package/dist/components/questions/types/listen-and-speak-question-list/transform.js +1 -26
  216. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +1 -26
  217. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +1 -230
  218. package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +1 -56
  219. package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +1 -16
  220. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +1 -72
  221. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +1 -230
  222. package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
  223. package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +1 -66
  224. package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +1 -42
  225. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +1 -96
  226. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +1 -238
  227. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +1 -2
  228. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +1 -76
  229. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +1 -19
  230. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +1 -134
  231. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +1 -617
  232. package/dist/components/questions/types/look-picture-fill-word-hint/index.js +1 -2
  233. package/dist/components/questions/types/look-picture-fill-word-hint/register.js +1 -82
  234. package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +1 -29
  235. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +1 -38
  236. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +1 -359
  237. package/dist/components/questions/types/match-by-writing-answer/index.js +1 -3
  238. package/dist/components/questions/types/match-by-writing-answer/register.js +1 -58
  239. package/dist/components/questions/types/match-by-writing-answer/transform.js +1 -61
  240. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +1 -16
  241. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +1 -23
  242. package/dist/components/questions/types/read-and-color-objects/index.js +1 -2
  243. package/dist/components/questions/types/read-and-color-objects/register.js +1 -3
  244. package/dist/components/questions/types/read-and-color-objects/transform.js +1 -36
  245. package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +1 -125
  246. package/dist/components/questions/types/read-displayed-content/register.js +1 -64
  247. package/dist/components/questions/types/read-displayed-content/transform.js +1 -12
  248. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +1 -64
  249. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +1 -511
  250. package/dist/components/questions/types/read-passage-and-answer-questions/index.js +1 -2
  251. package/dist/components/questions/types/read-passage-and-answer-questions/register.js +1 -58
  252. package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +1 -28
  253. package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +1 -373
  254. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -41
  255. package/dist/components/questions/types/speaking-conversation/register.js +1 -110
  256. package/dist/components/questions/types/speaking-conversation/transform.js +1 -34
  257. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -74
  258. package/dist/components/questions/types/speaking-cue-card/register.js +1 -52
  259. package/dist/components/questions/types/speaking-cue-card/transform.js +1 -12
  260. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +1 -10
  261. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +1 -161
  262. package/dist/components/questions/types/speaking-describe-image/index.js +1 -2
  263. package/dist/components/questions/types/speaking-describe-image/register.js +1 -69
  264. package/dist/components/questions/types/speaking-describe-image/transform.js +1 -16
  265. package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +1 -234
  266. package/dist/components/questions/types/spontaneous-qa/register.js +1 -93
  267. package/dist/components/questions/types/spontaneous-qa/transform.js +1 -34
  268. package/dist/components/questions/types/true-false/TrueFalseClient.js +1 -75
  269. package/dist/components/questions/types/true-false/TrueFalseCreator.js +1 -244
  270. package/dist/components/questions/types/true-false/index.js +1 -2
  271. package/dist/components/questions/types/true-false/register.js +1 -77
  272. package/dist/components/questions/types/true-false/transform.js +1 -32
  273. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +1 -233
  274. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +1 -631
  275. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +1 -28
  276. package/dist/components/questions/types/word-fill-paragraph/index.js +1 -2
  277. package/dist/components/questions/types/word-fill-paragraph/register.js +1 -136
  278. package/dist/components/questions/types/word-fill-paragraph/transform.js +1 -56
  279. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +1 -138
  280. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +1 -261
  281. package/dist/components/questions/types/word-fill-structured-form/index.js +1 -2
  282. package/dist/components/questions/types/word-fill-structured-form/register.js +1 -67
  283. package/dist/components/questions/types/word-fill-structured-form/transform.js +1 -40
  284. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +1 -84
  285. package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +1 -192
  286. package/dist/components/questions/types/word-ordering/index.js +1 -2
  287. package/dist/components/questions/types/word-ordering/register.js +1 -46
  288. package/dist/components/questions/types/word-ordering/transform.js +1 -24
  289. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +1 -98
  290. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +1 -320
  291. package/dist/components/questions/types/write-a-short-letter/index.js +1 -2
  292. package/dist/components/questions/types/write-a-short-letter/register.js +1 -68
  293. package/dist/components/questions/types/write-a-short-letter/transform.js +1 -25
  294. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +1 -61
  295. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +1 -196
  296. package/dist/components/questions/types/write-correct-verb-form/index.js +1 -2
  297. package/dist/components/questions/types/write-correct-verb-form/register.js +1 -62
  298. package/dist/components/questions/types/write-correct-verb-form/transform.js +1 -17
  299. package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +1 -22
  300. package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +1 -174
  301. package/dist/components/questions/types/write-sentences/index.js +1 -2
  302. package/dist/components/questions/types/write-sentences/register.js +1 -58
  303. package/dist/components/questions/types/write-sentences/transform.js +1 -28
  304. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +1 -96
  305. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +1 -161
  306. package/dist/components/questions/types/write-short-paragraph/index.js +1 -2
  307. package/dist/components/questions/types/write-short-paragraph/register.js +1 -4
  308. package/dist/components/questions/types/write-short-paragraph/transform.js +1 -21
  309. package/dist/components/questions/viewer/QuestionGroupViewer.js +1 -36
  310. package/dist/components/questions/viewer/QuestionViewer.js +1 -446
  311. package/dist/components/questions/viewer/index.js +1 -2
  312. package/dist/components/results/ResultReviewPageContainer.js +1 -98
  313. package/dist/components/results/ReviewQuestionRenderer.js +1 -72
  314. package/dist/components/results/index.js +1 -2
  315. package/dist/components/results/result-review.utils.js +1 -6
  316. package/dist/components/results/review-data.js +1 -150
  317. package/dist/components/results/review-renderer.utils.js +1 -97
  318. package/dist/components/results/review-types.js +1 -1
  319. package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +1 -138
  320. package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +1 -12
  321. package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +1 -18
  322. package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +1 -11
  323. package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +1 -11
  324. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +1 -9
  325. package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +1 -13
  326. package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +1 -8
  327. package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +1 -6
  328. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +1 -5
  329. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +1 -9
  330. package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +1 -41
  331. package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -25
  332. package/dist/components/themes/cambridge-yle/index.js +1 -13
  333. package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +1 -24
  334. package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +1 -88
  335. package/dist/components/themes/english-certification/EcWordFillParagraph.js +1 -55
  336. package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +1 -156
  337. package/dist/components/themes/english-certification/EcWritingTask.js +1 -34
  338. package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -114
  339. package/dist/components/themes/english-certification/EnglishCertificationBrand.js +1 -11
  340. package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +1 -21
  341. package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +1 -15
  342. package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +1 -19
  343. package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +1 -148
  344. package/dist/components/themes/english-certification/EnglishCertificationHeader.js +1 -31
  345. package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +1 -71
  346. package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +1 -33
  347. package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +1 -16
  348. package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +1 -22
  349. package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +1 -37
  350. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -72
  351. package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +1 -90
  352. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +1 -18
  353. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +1 -29
  354. package/dist/components/themes/english-certification/english-certification-theme.js +1 -58
  355. package/dist/components/themes/english-certification/grouped-question-utils.js +1 -218
  356. package/dist/components/themes/english-certification/index.js +1 -21
  357. package/dist/components/themes/index.js +1 -6
  358. package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +1 -170
  359. package/dist/components/themes/summer-sky/SummerSkyLayout.js +1 -221
  360. package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +1 -18
  361. package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +1 -194
  362. package/dist/components/themes/types.js +1 -1
  363. package/dist/components/ui/alert-dialog.js +1 -26
  364. package/dist/components/ui/alert.js +1 -22
  365. package/dist/components/ui/avatar.js +1 -12
  366. package/dist/components/ui/badge.js +1 -38
  367. package/dist/components/ui/button.js +1 -35
  368. package/dist/components/ui/calendar.js +1 -76
  369. package/dist/components/ui/card.js +1 -16
  370. package/dist/components/ui/carousel.js +1 -91
  371. package/dist/components/ui/checkbox.js +1 -9
  372. package/dist/components/ui/collapsible.js +1 -13
  373. package/dist/components/ui/command.js +1 -34
  374. package/dist/components/ui/confirm-dialog.js +1 -49
  375. package/dist/components/ui/dialog.js +1 -23
  376. package/dist/components/ui/drawer.js +1 -35
  377. package/dist/components/ui/dropdown-menu.js +1 -33
  378. package/dist/components/ui/file-upload.js +1 -155
  379. package/dist/components/ui/form.js +1 -62
  380. package/dist/components/ui/hover-card.js +1 -10
  381. package/dist/components/ui/image-preview.js +1 -29
  382. package/dist/components/ui/input.js +1 -8
  383. package/dist/components/ui/label.js +1 -9
  384. package/dist/components/ui/language-switcher.js +1 -15
  385. package/dist/components/ui/multi-select.js +1 -90
  386. package/dist/components/ui/points-input.js +1 -33
  387. package/dist/components/ui/popover.js +1 -26
  388. package/dist/components/ui/progress.js +1 -16
  389. package/dist/components/ui/radio-group.js +1 -12
  390. package/dist/components/ui/scroll-area.js +1 -11
  391. package/dist/components/ui/select.js +1 -38
  392. package/dist/components/ui/separator.js +1 -8
  393. package/dist/components/ui/sheet.js +1 -40
  394. package/dist/components/ui/single-select.js +1 -76
  395. package/dist/components/ui/skeleton.js +1 -6
  396. package/dist/components/ui/switch.js +1 -8
  397. package/dist/components/ui/table.js +1 -20
  398. package/dist/components/ui/tabs.js +1 -17
  399. package/dist/components/ui/textarea.js +1 -8
  400. package/dist/components/ui/tooltip.js +1 -11
  401. package/dist/index.d.ts +3 -3
  402. package/dist/index.js +1 -19
  403. package/dist/mcp/config.js +1 -32
  404. package/dist/mcp/http-client.js +1 -48
  405. package/dist/mcp/index.js +1 -29
  406. package/dist/mcp/tools/_helpers.js +1 -25
  407. package/dist/mcp/tools/exams.js +1 -439
  408. package/dist/mcp/tools/index.js +1 -6
  409. package/dist/mcp/tools/results.js +1 -19
  410. package/dist/shared/constants/ApiConstant.js +1 -276
  411. package/dist/shared/constants/QueryKeyConstant.js +1 -112
  412. package/dist/shared/constants/exam-level.enum.js +1 -41
  413. package/dist/shared/constants/landings/placement-test.constants.js +1 -431
  414. package/dist/shared/constants/question-skills.js +1 -267
  415. package/dist/shared/constants/routes.js +1 -81
  416. package/dist/shared/index.d.ts +3 -0
  417. package/dist/shared/index.js +1 -7
  418. package/dist/shared/lib/hooks/index.js +1 -10
  419. package/dist/shared/lib/hooks/useAlertDialog.js +1 -57
  420. package/dist/shared/lib/hooks/useAudioPlayCounter.js +1 -30
  421. package/dist/shared/lib/hooks/useAudioPreview.js +1 -81
  422. package/dist/shared/lib/hooks/useAudioUpload.js +1 -24
  423. package/dist/shared/lib/hooks/useConfirmDialog.js +1 -89
  424. package/dist/shared/lib/hooks/useDebouncedCallback.js +1 -32
  425. package/dist/shared/lib/hooks/useDelayedAutoPlay.js +1 -12
  426. package/dist/shared/lib/hooks/useFileUpload.js +1 -24
  427. package/dist/shared/lib/hooks/useImageUrl.js +1 -5
  428. package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -10
  429. package/dist/shared/lib/hooks/useReactHookForm.js +1 -31
  430. package/dist/shared/lib/hooks/useToast.js +1 -62
  431. package/dist/shared/lib/i18n/index.js +1 -54
  432. package/dist/shared/lib/i18n/messages/en/admin.js +1 -2104
  433. package/dist/shared/lib/i18n/messages/en/auth.js +1 -86
  434. package/dist/shared/lib/i18n/messages/en/common.js +1 -140
  435. package/dist/shared/lib/i18n/messages/en/file-upload.js +1 -18
  436. package/dist/shared/lib/i18n/messages/en/student.js +1 -133
  437. package/dist/shared/lib/i18n/messages/en/terms.js +1 -37
  438. package/dist/shared/lib/i18n/messages/en.js +1 -15
  439. package/dist/shared/lib/i18n/messages/vi/admin.js +1 -2104
  440. package/dist/shared/lib/i18n/messages/vi/auth.js +1 -86
  441. package/dist/shared/lib/i18n/messages/vi/common.js +1 -140
  442. package/dist/shared/lib/i18n/messages/vi/file-upload.js +1 -18
  443. package/dist/shared/lib/i18n/messages/vi/student.js +1 -133
  444. package/dist/shared/lib/i18n/messages/vi/terms.js +1 -37
  445. package/dist/shared/lib/i18n/messages/vi.js +1 -15
  446. package/dist/shared/lib/i18n/types.js +1 -1
  447. package/dist/shared/lib/stores/examQuestionStore.js +1 -212
  448. package/dist/shared/lib/stores/localeStore.js +1 -10
  449. package/dist/shared/lib/utils/public-page.js +1 -44
  450. package/dist/shared/lib/utils/question-reverse-transform.d.ts +6 -0
  451. package/dist/shared/lib/utils/question-reverse-transform.js +1 -0
  452. package/dist/shared/lib/utils/question-transform-types.js +1 -1
  453. package/dist/shared/lib/utils/question-transform-validation.d.ts +6 -0
  454. package/dist/shared/lib/utils/question-transform-validation.js +1 -0
  455. package/dist/shared/lib/utils/question-transform.d.ts +6 -2
  456. package/dist/shared/lib/utils/question-transform.js +1 -174
  457. package/dist/shared/lib/utils.js +1 -168
  458. package/dist/shared/lib/validation/choose-correct-answer.validation.js +1 -111
  459. package/dist/shared/lib/validation/label-the-picture.validation.js +1 -19
  460. package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +1 -46
  461. package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +1 -21
  462. package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +1 -34
  463. package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +1 -40
  464. package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +1 -45
  465. package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +1 -19
  466. package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +1 -39
  467. package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +1 -32
  468. package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +1 -19
  469. package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +1 -28
  470. package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +1 -12
  471. package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +1 -47
  472. package/dist/shared/lib/validation/read-and-color-objects.validation.js +1 -29
  473. package/dist/shared/lib/validation/read-displayed-content.validation.js +1 -28
  474. package/dist/shared/lib/validation/speaking-conversation.validation.js +1 -75
  475. package/dist/shared/lib/validation/speaking-describe-image.validation.js +1 -31
  476. package/dist/shared/lib/validation/spontaneous-qa.validation.js +1 -48
  477. package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +1 -38
  478. package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +1 -39
  479. package/dist/shared/lib/validation/word-fill-paragraph.validation.js +1 -43
  480. package/dist/shared/lib/validation/word-ordering.validation.js +1 -33
  481. package/dist/shared/types/branch.types.js +1 -6
  482. package/dist/shared/types/common.types.js +1 -50
  483. package/dist/shared/types/entities/exam-schedule.types.js +1 -5
  484. package/dist/shared/types/entities/exam.types.js +1 -1
  485. package/dist/shared/types/entities/question.types.js +1 -4
  486. package/dist/shared/types/entities/result.types.js +1 -4
  487. package/dist/shared/types/entities/user.types.js +1 -4
  488. package/dist/shared/types/exam-taking.types.js +1 -2
  489. package/dist/shared/types/history.types.js +1 -34
  490. package/dist/shared/types/index.js +1 -1
  491. package/dist/shared/types/questions/choose-correct-adjective.js +1 -4
  492. package/dist/shared/types/questions/choose-the-correct-answer.js +1 -9
  493. package/dist/shared/types/questions/fill-missing-words-in-grid.js +1 -11
  494. package/dist/shared/types/questions/index.js +1 -40
  495. package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
  496. package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
  497. package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
  498. package/dist/shared/types/questions/listen-and-speak-info-exchange.js +1 -3
  499. package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +1 -3
  500. package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
  501. package/dist/shared/types/questions/listen-and-speak-with-story-images.js +1 -3
  502. package/dist/shared/types/questions/look-picture-choose-correct-answer.js +1 -5
  503. package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +1 -9
  504. package/dist/shared/types/questions/look-picture-fill-word-hint.js +1 -7
  505. package/dist/shared/types/questions/read-displayed-content.js +1 -1
  506. package/dist/shared/types/questions/speaking-conversation.js +1 -7
  507. package/dist/shared/types/questions/speaking-describe-image.js +1 -1
  508. package/dist/shared/types/questions/spontaneous-qa.js +1 -1
  509. package/dist/shared/types/questions/write-correct-verb-form.js +1 -12
  510. package/dist/shared/types/role.types.js +1 -1
  511. package/dist/shared/types/structure-management.js +1 -28
  512. package/dist/shared/types/student.types.js +1 -54
  513. package/dist/shared/types/teacher-dashboard-rooms.js +1 -2
  514. package/package.json +12 -3
@@ -1,70 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useEffect } from 'react';
4
- import { Check, X, CircleHelp, Volume2 } from 'lucide-react';
5
- import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
- import { cn } from '../../../../shared/lib/utils';
7
- /**
8
- * Scene Object Button - A clickable image button overlaid on the background scene
9
- */
10
- function SceneObjectButton({ node, isSelected, isCorrect, isWrong, isReviewMode, onClick, }) {
11
- const { previewUrl: objectImageUrl } = usePresignedFileUrl(node.imageUrl || '');
12
- return (_jsxs("button", { type: "button", onClick: onClick, disabled: isReviewMode, className: cn('absolute cursor-pointer rounded-lg transition-all duration-200 overflow-hidden', 'hover:scale-105', 'focus:outline-none focus:ring-2 focus:ring-offset-1',
13
- // Default state
14
- !isSelected && !isCorrect && !isWrong && 'border-2 border-transparent hover:border-blue-300 focus:ring-blue-400',
15
- // Selected (not review mode)
16
- isSelected && !isReviewMode && 'border-3 border-blue-500 ring-2 ring-blue-200 shadow-lg shadow-blue-200/50 scale-105',
17
- // Review mode - correct
18
- isCorrect && 'border-3 border-green-500 ring-2 ring-green-200 shadow-lg shadow-green-200/50 scale-105',
19
- // Review mode - wrong (selected but not correct)
20
- isWrong && 'border-3 border-red-500 ring-2 ring-red-200 shadow-lg shadow-red-200/50 scale-105',
21
- // Disabled in review
22
- isReviewMode && 'cursor-default hover:scale-100'), style: {
23
- left: `${node.x}%`,
24
- top: `${node.y}%`,
25
- width: `${node.width}%`,
26
- height: `${node.height}%`,
27
- }, title: isReviewMode ? node.label : `Chọn ${node.label}`, children: [objectImageUrl ? (_jsx("img", { src: objectImageUrl, alt: node.label, className: "h-full w-full object-fill pointer-events-none", draggable: false })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center bg-gray-100/80 text-xs text-gray-500", children: node.label })), isReviewMode && (isCorrect || isWrong) && (_jsx("div", { className: cn('absolute -top-2 -right-2 flex h-5 w-5 items-center justify-center rounded-full shadow-md', isCorrect ? 'bg-green-500' : 'bg-red-500'), style: { zIndex: 1 }, children: isCorrect ? (_jsx(Check, { className: "h-3 w-3 text-white" })) : (_jsx(X, { className: "h-3 w-3 text-white" })) }))] }));
28
- }
29
- /**
30
- * ListenAndChooseObjectsInSceneClient
31
- *
32
- * Student/preview view for the "Listen and Choose Objects in Scene" question type.
33
- * Displays a background scene image with clickable object images overlaid.
34
- * Student listens to audio and clicks the correct object.
35
- */
36
- export function ListenAndChooseObjectsInSceneClient({ questionData, onSubmit, isReviewMode = false, userAnswer, correctAnswer, explanation, }) {
37
- // State
38
- const [selectedObjectId, setSelectedObjectId] = useState(userAnswer?.objectId || '');
39
- // Presigned URLs
40
- const { previewUrl: backgroundUrl, isLoading: bgLoading } = usePresignedFileUrl(questionData.backgroundImageUrl || '');
41
- const { previewUrl: audioUrl } = usePresignedFileUrl(questionData.audioUrl || '');
42
- // Sync userAnswer
43
- useEffect(() => {
44
- if (userAnswer?.objectId) {
45
- setSelectedObjectId(userAnswer.objectId);
46
- }
47
- }, [userAnswer]);
48
- // Handle object click
49
- const handleObjectClick = (objectId) => {
50
- if (isReviewMode)
51
- return;
52
- setSelectedObjectId(objectId);
53
- onSubmit?.({ objectId });
54
- };
55
- const hasAnswered = selectedObjectId !== '';
56
- const selectedNode = questionData.objectNodes.find((n) => n.id === selectedObjectId);
57
- const isAnswerCorrect = isReviewMode && correctAnswer && selectedObjectId === correctAnswer.objectId;
58
- return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm", children: _jsx(CircleHelp, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "Nghe v\u00E0 ch\u1ECDn \u0111\u1ED1i t\u01B0\u1EE3ng trong c\u1EA3nh" })] }), isReviewMode && hasAnswered && (_jsxs("div", { className: cn('flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold', isAnswerCorrect
59
- ? 'bg-green-100 text-green-700'
60
- : 'bg-red-100 text-red-700'), children: [isAnswerCorrect ? (_jsx(Check, { className: "h-3.5 w-3.5" })) : (_jsx(X, { className: "h-3.5 w-3.5" })), isAnswerCorrect ? 'Đúng' : 'Sai'] }))] }), _jsxs("div", { className: "space-y-5 p-5", children: [audioUrl && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-white", children: _jsx(Volume2, { className: "h-4 w-4" }) }), _jsx("audio", { controls: true, className: "h-8 w-full", src: audioUrl, children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })] })), _jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2 w-2 rounded-full bg-red-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-green-400" }), _jsx("span", { className: "ml-2 text-xs font-medium text-gray-500", children: "Scene" })] }) }), _jsx("div", { className: "relative p-2", children: bgLoading ? (_jsx("div", { className: "flex h-64 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })) : backgroundUrl ? (_jsxs("div", { className: "relative w-full", children: [_jsx("img", { src: backgroundUrl, alt: "Scene background", className: "w-full rounded-md object-contain", draggable: false }), questionData.objectNodes.map((node) => {
61
- const isSelected = selectedObjectId === node.id;
62
- const isCorrectNode = isReviewMode && correctAnswer?.objectId === node.id;
63
- const isWrongNode = isReviewMode && isSelected && correctAnswer?.objectId !== node.id;
64
- return (_jsx(SceneObjectButton, { node: node, isSelected: isSelected, isCorrect: isCorrectNode, isWrong: isWrongNode, isReviewMode: isReviewMode, onClick: () => handleObjectClick(node.id) }, node.id));
65
- })] })) : (_jsx("div", { className: "flex h-64 items-center justify-center text-sm text-gray-400", children: "Ch\u01B0a c\u00F3 \u1EA3nh n\u1EC1n" })) })] }), hasAnswered && selectedNode && (_jsxs("div", { className: cn('flex items-center gap-2 rounded-lg border p-3', isReviewMode
66
- ? isAnswerCorrect
67
- ? 'border-green-200 bg-green-50'
68
- : 'border-red-200 bg-red-50'
69
- : 'border-blue-200 bg-blue-50'), children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["\u0110\u00E3 ch\u1ECDn: ", _jsx("strong", { children: selectedNode.label })] }), isReviewMode && (_jsx("span", { className: cn('text-xs font-semibold', isAnswerCorrect ? 'text-green-600' : 'text-red-600'), children: isAnswerCorrect ? '✓ Chính xác' : '✗ Không chính xác' }))] })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-xs font-bold", children: "!" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "Nghe audio v\u00E0 nh\u1EA5p v\u00E0o \u0111\u1ED1i t\u01B0\u1EE3ng \u0111\u00FAng trong h\u00ECnh" })] })), isReviewMode && explanation && (_jsxs("div", { className: "rounded-lg border border-indigo-200 bg-indigo-50 p-4", children: [_jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch:" }), _jsx("p", { className: "mt-1 text-sm text-indigo-700", children: explanation })] }))] })] })] }));
70
- }
1
+ "use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useState as t,useEffect as d}from"react";import{Check as l,X as s,CircleHelp as n,Volume2 as i}from"lucide-react";import{usePresignedFileUrl as a}from"../../../../shared/lib/hooks";import{cn as o}from"../../../../shared/lib/utils";function c({node:t,isSelected:d,isCorrect:n,isWrong:i,isReviewMode:c,onClick:b}){const{previewUrl:m}=a(t.imageUrl||"");return r("button",{type:"button",onClick:b,disabled:c,className:o("absolute cursor-pointer rounded-lg transition-all duration-200 overflow-hidden","hover:scale-105","focus:outline-none focus:ring-2 focus:ring-offset-1",!d&&!n&&!i&&"border-2 border-transparent hover:border-blue-300 focus:ring-blue-400",d&&!c&&"border-3 border-blue-500 ring-2 ring-blue-200 shadow-lg shadow-blue-200/50 scale-105",n&&"border-3 border-green-500 ring-2 ring-green-200 shadow-lg shadow-green-200/50 scale-105",i&&"border-3 border-red-500 ring-2 ring-red-200 shadow-lg shadow-red-200/50 scale-105",c&&"cursor-default hover:scale-100"),style:{left:`${t.x}%`,top:`${t.y}%`,width:`${t.width}%`,height:`${t.height}%`},title:c?t.label:`Chọn ${t.label}`,children:[m?e("img",{src:m,alt:t.label,className:"h-full w-full object-fill pointer-events-none",draggable:!1}):e("div",{className:"flex h-full w-full items-center justify-center bg-gray-100/80 text-xs text-gray-500",children:t.label}),c&&(n||i)&&e("div",{className:o("absolute -top-2 -right-2 flex h-5 w-5 items-center justify-center rounded-full shadow-md",n?"bg-green-500":"bg-red-500"),style:{zIndex:1},children:e(n?l:s,{className:"h-3 w-3 text-white"})})]})}export function ListenAndChooseObjectsInSceneClient({questionData:b,onSubmit:m,isReviewMode:h=!1,userAnswer:g,correctAnswer:u,explanation:f}){const[x,p]=t(g?.objectId||""),{previewUrl:w,isLoading:v}=a(b.backgroundImageUrl||""),{previewUrl:N}=a(b.audioUrl||"");d(()=>{g?.objectId&&p(g.objectId)},[g]);const y=""!==x,j=b.objectNodes.find(e=>e.id===x),C=h&&u&&x===u.objectId;return r("div",{className:"space-y-4",children:[b.isBackup&&r("div",{className:"inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm",children:[e("span",{children:"🔄"})," Câu hỏi dự phòng"]}),r("div",{className:"overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm",children:[r("div",{className:"flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3",children:[r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm",children:e(n,{className:"h-4 w-4"})}),e("span",{className:"text-sm font-medium text-gray-700",children:"Nghe và chọn đối tượng trong cảnh"})]}),h&&y&&r("div",{className:o("flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold",C?"bg-green-100 text-green-700":"bg-red-100 text-red-700"),children:[e(C?l:s,{className:"h-3.5 w-3.5"}),C?"Đúng":"Sai"]})]}),r("div",{className:"space-y-5 p-5",children:[N&&r("div",{className:"flex items-center gap-3 rounded-lg border border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3",children:[e("div",{className:"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-white",children:e(i,{className:"h-4 w-4"})}),e("audio",{controls:!0,className:"h-8 w-full",src:N,children:"Trình duyệt không hỗ trợ audio."})]}),r("div",{className:"overflow-hidden rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100",children:[e("div",{className:"border-b border-gray-100 px-3 py-1.5",children:r("div",{className:"flex items-center gap-1.5",children:[e("div",{className:"h-2 w-2 rounded-full bg-red-400"}),e("div",{className:"h-2 w-2 rounded-full bg-yellow-400"}),e("div",{className:"h-2 w-2 rounded-full bg-green-400"}),e("span",{className:"ml-2 text-xs font-medium text-gray-500",children:"Scene"})]})}),e("div",{className:"relative p-2",children:v?e("div",{className:"flex h-64 items-center justify-center",children:e("div",{className:"h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500"})}):w?r("div",{className:"relative w-full",children:[e("img",{src:w,alt:"Scene background",className:"w-full rounded-md object-contain",draggable:!1}),b.objectNodes.map(r=>{const t=x===r.id,d=h&&u?.objectId===r.id,l=h&&t&&u?.objectId!==r.id;return e(c,{node:r,isSelected:t,isCorrect:d,isWrong:l,isReviewMode:h,onClick:()=>{return e=r.id,void(h||(p(e),m?.({objectId:e})));var e}},r.id)})]}):e("div",{className:"flex h-64 items-center justify-center text-sm text-gray-400",children:"Chưa có ảnh nền"})})]}),y&&j&&r("div",{className:o("flex items-center gap-2 rounded-lg border p-3",h?C?"border-green-200 bg-green-50":"border-red-200 bg-red-50":"border-blue-200 bg-blue-50"),children:[r("span",{className:"text-sm font-medium text-gray-700",children:["Đã chọn: ",e("strong",{children:j.label})]}),h&&e("span",{className:o("text-xs font-semibold",C?"text-green-600":"text-red-600"),children:C?"✓ Chính xác":"✗ Không chính xác"})]}),!h&&!y&&r("div",{className:"flex items-center gap-2 rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3",children:[e("div",{className:"flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white",children:e("span",{className:"text-xs font-bold",children:"!"})}),e("span",{className:"text-sm text-amber-800",children:"Nghe audio và nhấp vào đối tượng đúng trong hình"})]}),h&&f&&r("div",{className:"rounded-lg border border-indigo-200 bg-indigo-50 p-4",children:[e("p",{className:"text-sm font-semibold text-indigo-800",children:"Giải thích:"}),e("p",{className:"mt-1 text-sm text-indigo-700",children:f})]})]})]})]})}
@@ -1,386 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useRef, useEffect, useCallback } from 'react';
4
- import { Button } from '../../../../components/ui/button';
5
- import { Label } from '../../../../components/ui/label';
6
- import { Input } from '../../../../components/ui/input';
7
- import { Textarea } from '../../../../components/ui/textarea';
8
- import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
9
- import { PointsInput } from '../../../../components/ui/points-input';
10
- import { RadioGroup, RadioGroupItem } from '../../../../components/ui/radio-group';
11
- import { Pencil, Eye, Plus, Trash2, Image as ImageIcon, Volume2, Lightbulb, MousePointerClick, GripVertical, ListChecks, BookOpen, } from 'lucide-react';
12
- import { FileUpload } from '../../../../components/ui/file-upload';
13
- import { usePresignedFileUrl, useDebouncedCallback } from '../../../../shared/lib/hooks';
14
- import { useReactHookForm } from '../../../../shared/lib/hooks/useReactHookForm';
15
- import { listenAndChooseObjectsInSceneFormSchema, } from '../../../../shared/lib/validation/listen-and-choose-objects-in-scene.validation';
16
- import { ListenAndChooseObjectsInSceneClient } from './ListenAndChooseObjectsInSceneClient';
17
- import { useSceneQuestionGroup } from '../../_shared/hooks/useSceneQuestionGroup';
18
- import { generateObjectId } from '../../_shared/utils/id-generator';
19
- import { useExamQuestionStore } from '../../../../shared/lib/stores/examQuestionStore';
20
- import { CreatorGuide } from '../../components/CreatorGuide';
21
- /**
22
- * Scene Object Editor - An individual object node editor row
23
- */
24
- function SceneObjectEditor({ node, index, isCorrect, onUpdate, onRemove, onSelectCorrect, errors, }) {
25
- const [objectPreviewUrl, setObjectPreviewUrl] = useState('');
26
- const { previewUrl: presignedObjectUrl } = usePresignedFileUrl(node.imageUrl || '');
27
- useEffect(() => {
28
- if (presignedObjectUrl) {
29
- setObjectPreviewUrl(presignedObjectUrl);
30
- }
31
- }, [presignedObjectUrl]);
32
- return (_jsx("div", { className: `rounded-lg border-2 p-4 transition-all ${isCorrect
33
- ? 'border-green-300 bg-green-50/50'
34
- : 'border-gray-200 bg-white hover:border-gray-300'}`, children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsxs("div", { className: "flex flex-col items-center gap-1 pt-1", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-sm font-bold text-white shadow-sm", children: index + 1 }), _jsx(GripVertical, { className: "h-4 w-4 text-gray-400" })] }), _jsxs("div", { className: "flex-1 space-y-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "flex-1", children: [_jsx(Label, { className: "text-xs font-medium text-gray-500", children: "T\u00EAn \u0111\u1ED1i t\u01B0\u1EE3ng" }), _jsx(Input, { value: node.label, onChange: (e) => onUpdate(node.id, { label: e.target.value }), placeholder: "V\u00ED d\u1EE5: cow, frog, horse...", className: `mt-1 ${errors?.label ? 'border-red-500' : 'border-gray-200'}` }), errors?.label && _jsx("p", { className: "mt-1 text-xs text-red-500", children: errors.label })] }), _jsx("div", { className: "flex items-center gap-2 pt-5" })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-xs font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh \u0111\u1ED1i t\u01B0\u1EE3ng" }), _jsxs("div", { className: "mt-1 flex items-center gap-3", children: [_jsxs("div", { className: "flex-1", children: [_jsx(FileUpload, { id: `object-image-${node.id}`, label: "", accept: "image/*", value: node.imageUrl || '', onChange: (url) => onUpdate(node.id, { imageUrl: url }), onPresignedUrlChange: setObjectPreviewUrl, maxSize: 5, placeholder: "Upload h\u00ECnh \u1EA3nh \u0111\u1ED1i t\u01B0\u1EE3ng", autoUpload: true, prefix: "questions/objects" }), errors?.imageUrl && _jsx("p", { className: "mt-1 text-xs text-red-500", children: errors.imageUrl })] }), objectPreviewUrl && (_jsx("div", { className: "h-16 w-16 flex-shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-gray-50", children: _jsx("img", { src: objectPreviewUrl, alt: node.label || 'Object', className: "h-full w-full object-contain" }) }))] })] }), _jsxs("div", { className: "grid grid-cols-4 gap-2", children: [_jsxs("div", { children: [_jsx(Label, { className: "text-xs font-medium text-gray-500", children: "X (%)" }), _jsx(Input, { type: "number", min: 0, max: 100, value: node.x, onChange: (e) => onUpdate(node.id, { x: parseFloat(e.target.value) || 0 }), className: "mt-1 border-gray-200" })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-xs font-medium text-gray-500", children: "Y (%)" }), _jsx(Input, { type: "number", min: 0, max: 100, value: node.y, onChange: (e) => onUpdate(node.id, { y: parseFloat(e.target.value) || 0 }), className: "mt-1 border-gray-200" })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-xs font-medium text-gray-500", children: "Width (%)" }), _jsx(Input, { type: "number", min: 1, max: 100, value: node.width, onChange: (e) => onUpdate(node.id, { width: parseFloat(e.target.value) || 10 }), className: "mt-1 border-gray-200" })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-xs font-medium text-gray-500", children: "Height (%)" }), _jsx(Input, { type: "number", min: 1, max: 100, value: node.height, onChange: (e) => onUpdate(node.id, { height: parseFloat(e.target.value) || 10 }), className: "mt-1 border-gray-200" })] })] })] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => onRemove(node.id), className: "mt-1 text-red-400 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }) }));
35
- }
36
- /**
37
- * Drag overlay for positioning objects on the background image
38
- */
39
- function ScenePreviewWithDrag({ backgroundUrl, objectNodes, correctObjectId, onNodeDrag, onNodeResize, }) {
40
- const containerRef = useRef(null);
41
- const [draggingId, setDraggingId] = useState(null);
42
- const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
43
- const [resizingId, setResizingId] = useState(null);
44
- const [resizeType, setResizeType] = useState(null);
45
- const [resizeStartPos, setResizeStartPos] = useState({ x: 0, y: 0 });
46
- const [resizeStartDim, setResizeStartDim] = useState({ width: 0, height: 0 });
47
- const handleMouseDown = (nodeId, nodeX, nodeY, e) => {
48
- e.preventDefault();
49
- if (e.target.dataset.resizeHandle)
50
- return; // Ignore drag if clicking resize handle
51
- setDraggingId(nodeId);
52
- if (containerRef.current) {
53
- const rect = containerRef.current.getBoundingClientRect();
54
- const mouseXPercent = ((e.clientX - rect.left) / rect.width) * 100;
55
- const mouseYPercent = ((e.clientY - rect.top) / rect.height) * 100;
56
- setDragOffset({
57
- x: mouseXPercent - nodeX,
58
- y: mouseYPercent - nodeY,
59
- });
60
- }
61
- };
62
- const handleResizeMouseDown = (nodeId, type, nodeWidth, nodeHeight, e) => {
63
- e.preventDefault();
64
- e.stopPropagation();
65
- setResizingId(nodeId);
66
- setResizeType(type);
67
- setResizeStartPos({ x: e.clientX, y: e.clientY });
68
- setResizeStartDim({ width: nodeWidth, height: nodeHeight });
69
- };
70
- const handleMouseMove = useCallback((e) => {
71
- if (!containerRef.current)
72
- return;
73
- if (draggingId) {
74
- const rect = containerRef.current.getBoundingClientRect();
75
- const mouseXPercent = ((e.clientX - rect.left) / rect.width) * 100;
76
- const mouseYPercent = ((e.clientY - rect.top) / rect.height) * 100;
77
- const x = Math.max(0, Math.min(100, mouseXPercent - dragOffset.x));
78
- const y = Math.max(0, Math.min(100, mouseYPercent - dragOffset.y));
79
- onNodeDrag(draggingId, Math.round(x * 10) / 10, Math.round(y * 10) / 10);
80
- }
81
- else if (resizingId && resizeType) {
82
- const rect = containerRef.current.getBoundingClientRect();
83
- const dx = ((e.clientX - resizeStartPos.x) / rect.width) * 100;
84
- const dy = ((e.clientY - resizeStartPos.y) / rect.height) * 100;
85
- let newWidth = resizeStartDim.width;
86
- let newHeight = resizeStartDim.height;
87
- if (resizeType === 'e' || resizeType === 'se') {
88
- newWidth = Math.max(1, resizeStartDim.width + dx);
89
- }
90
- if (resizeType === 's' || resizeType === 'se') {
91
- newHeight = Math.max(1, resizeStartDim.height + dy);
92
- }
93
- onNodeResize(resizingId, Math.round(newWidth * 10) / 10, Math.round(newHeight * 10) / 10);
94
- }
95
- }, [draggingId, resizingId, resizeType, resizeStartPos, resizeStartDim, dragOffset, onNodeDrag, onNodeResize]);
96
- const handleMouseUp = useCallback(() => {
97
- setDraggingId(null);
98
- setResizingId(null);
99
- setResizeType(null);
100
- }, []);
101
- useEffect(() => {
102
- if (draggingId || resizingId) {
103
- window.addEventListener('mousemove', handleMouseMove);
104
- window.addEventListener('mouseup', handleMouseUp);
105
- return () => {
106
- window.removeEventListener('mousemove', handleMouseMove);
107
- window.removeEventListener('mouseup', handleMouseUp);
108
- };
109
- }
110
- }, [draggingId, resizingId, handleMouseMove, handleMouseUp]);
111
- return (_jsxs("div", { ref: containerRef, className: "relative w-full select-none", children: [_jsx("img", { src: backgroundUrl, alt: "Scene background", className: "w-full rounded-md object-contain", draggable: false }), objectNodes.map((node) => {
112
- const isCorrect = node.id === correctObjectId;
113
- const isActive = draggingId === node.id || resizingId === node.id;
114
- return (_jsxs("div", { className: `absolute cursor-move rounded-md border-2 transition-colors group ${isActive
115
- ? 'border-blue-500 shadow-lg'
116
- : isCorrect
117
- ? 'border-green-400 shadow-md'
118
- : 'border-gray-300 hover:border-blue-400'}`, style: {
119
- left: `${node.x}%`,
120
- top: `${node.y}%`,
121
- width: `${node.width}%`,
122
- height: `${node.height}%`,
123
- zIndex: isActive ? 2 : 1,
124
- }, onMouseDown: (e) => handleMouseDown(node.id, node.x, node.y, e), children: [_jsxs("div", { className: "absolute -top-5 left-0 rounded bg-black/70 px-1.5 py-0.5 text-[10px] font-medium text-white whitespace-nowrap", children: [node.label || `Obj ${objectNodes.indexOf(node) + 1}`, isCorrect && ' ✓'] }), _jsx("div", { className: "flex h-full w-full items-center justify-center overflow-hidden rounded bg-transparent", children: node.imageUrl ? (_jsx(PresignedImage, { url: node.imageUrl, alt: node.label })) : (_jsx("div", { className: "flex bg-white/30 p-2 rounded items-center justify-center", children: _jsx(ImageIcon, { className: "h-4 w-4 text-gray-400" }) })) }), _jsx("div", { "data-resize-handle": "true", className: "absolute -right-1.5 top-1/2 h-3 w-3 -translate-y-1/2 cursor-e-resize rounded-full border border-blue-500 bg-white opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100", style: { opacity: isActive ? 1 : undefined }, onMouseDown: (e) => handleResizeMouseDown(node.id, 'e', node.width, node.height, e) }), _jsx("div", { "data-resize-handle": "true", className: "absolute -bottom-1.5 left-1/2 h-3 w-3 -translate-x-1/2 cursor-s-resize rounded-full border border-blue-500 bg-white opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100", style: { opacity: isActive ? 1 : undefined }, onMouseDown: (e) => handleResizeMouseDown(node.id, 's', node.width, node.height, e) }), _jsx("div", { "data-resize-handle": "true", className: "absolute -bottom-1.5 -right-1.5 h-3 w-3 cursor-se-resize rounded-full border border-blue-500 bg-white opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100", style: { opacity: isActive ? 1 : undefined }, onMouseDown: (e) => handleResizeMouseDown(node.id, 'se', node.width, node.height, e) })] }, node.id));
125
- })] }));
126
- }
127
- /** Helper component for presigned image display */
128
- function PresignedImage({ url, alt }) {
129
- const { previewUrl } = usePresignedFileUrl(url);
130
- if (!previewUrl)
131
- return _jsx(ImageIcon, { className: "h-4 w-4 text-gray-400" });
132
- return (_jsx("img", { src: previewUrl, alt: alt, className: "h-full w-full object-fill pointer-events-none", draggable: false }));
133
- }
134
- /**
135
- * ListenAndChooseObjectsInSceneCreator
136
- *
137
- * Admin creator component for the "Listen and Choose Objects in Scene" question type.
138
- * Allows uploading a background scene, adding object images with positions,
139
- * and selecting the correct answer.
140
- */
141
- function ListenAndChooseObjectsInSceneCreatorContent({ initialData, onSave, onSaveToQuestionBank, onCancel, onChange, onUnsavedChangesChange, validationRef, externalErrors, groups, questionIndexInPart = 0, partId = '', }) {
142
- console.log('[ListenAndChooseObjects_Creator] RENDER', { initialData, questionIndexInPart, partId, groups });
143
- // Get examId from global exam store for audio upload API
144
- const examId = useExamQuestionStore((state) => state.examId);
145
- // ============ SHARED GROUP DATA (via useSceneQuestionGroup hook) ============
146
- const sceneGroupInitialData = {
147
- backgroundImageUrl: initialData?.backgroundImageUrl || '',
148
- objectNodes: initialData?.objectNodes || [],
149
- };
150
- const { isGrouped, currentGroupNumber, backgroundImageUrl, objectNodes, setBackgroundImageUrl, setObjectNodes, } = useSceneQuestionGroup({
151
- partId,
152
- groups,
153
- questionIndexInPart,
154
- initialData: sceneGroupInitialData,
155
- });
156
- // ============ STATE ============
157
- // Audio, Points, CorrectObjectId, Explanation are SPECIFIC to the question
158
- const [audioUrl, setAudioUrl] = useState(initialData?.audioUrl || '');
159
- const [correctObjectId, setCorrectObjectId] = useState(initialData?.correctObjectId || '');
160
- const [points, setPoints] = useState(initialData?.points || 1);
161
- const [explanation, setExplanation] = useState(initialData?.explanation || '');
162
- const [isBackup, setIsBackup] = useState(initialData?.isBackup || false);
163
- // UI State
164
- const [isClientMode, setIsClientMode] = useState(false);
165
- const [backgroundPreviewUrl, setBackgroundPreviewUrl] = useState('');
166
- const [audioPreviewUrl, setAudioPreviewUrl] = useState('');
167
- // Use react-hook-form with Zod validation
168
- const form = useReactHookForm({
169
- defaultValues: {
170
- audioUrl,
171
- backgroundImageUrl,
172
- objectNodes,
173
- correctObjectId,
174
- isBackup,
175
- },
176
- zodSchema: listenAndChooseObjectsInSceneFormSchema,
177
- mode: 'onBlur',
178
- });
179
- // Refs
180
- const previousInitialDataRef = useRef(initialData);
181
- const isInitializingRef = useRef(true);
182
- const onChangeRef = useRef(onChange);
183
- const hasInitializedRef = useRef(false);
184
- // Presigned URLs
185
- const { previewUrl: presignedBgUrl } = usePresignedFileUrl(backgroundImageUrl);
186
- const { previewUrl: presignedAudioUrl } = usePresignedFileUrl(audioUrl);
187
- useEffect(() => {
188
- if (presignedBgUrl)
189
- setBackgroundPreviewUrl(presignedBgUrl);
190
- }, [presignedBgUrl]);
191
- useEffect(() => {
192
- if (presignedAudioUrl)
193
- setAudioPreviewUrl(presignedAudioUrl);
194
- }, [presignedAudioUrl]);
195
- // Update onChange ref
196
- useEffect(() => {
197
- onChangeRef.current = onChange;
198
- }, [onChange]);
199
- // Debounced onChange
200
- const debouncedOnChange = useDebouncedCallback((data) => {
201
- onChangeRef.current?.(data);
202
- }, 300);
203
- // Expose validation function via ref
204
- useEffect(() => {
205
- if (validationRef) {
206
- validationRef.current = {
207
- triggerValidation: async () => {
208
- return validateForm();
209
- },
210
- getFormData: () => ({
211
- audioUrl,
212
- backgroundImageUrl,
213
- objectNodes,
214
- correctObjectId,
215
- points,
216
- explanation,
217
- isBackup,
218
- }),
219
- };
220
- }
221
- }, [validationRef, audioUrl, backgroundImageUrl, objectNodes, correctObjectId, points, explanation, isBackup]);
222
- // Sync question-specific state from initialData
223
- useEffect(() => {
224
- if (!initialData) {
225
- if (!hasInitializedRef.current) {
226
- hasInitializedRef.current = true;
227
- isInitializingRef.current = false;
228
- }
229
- return;
230
- }
231
- const initialDataChanged = JSON.stringify(previousInitialDataRef.current) !== JSON.stringify(initialData);
232
- // On first mount, even if initialData hasn't changed from the ref's initial value,
233
- // we still need to initialize the flags so onChange can fire when user edits.
234
- if (!initialDataChanged && hasInitializedRef.current)
235
- return;
236
- previousInitialDataRef.current = initialData;
237
- const newAudioUrl = initialData?.audioUrl || '';
238
- const newCorrect = initialData?.correctObjectId || '';
239
- const newPoints = initialData?.points || 1;
240
- const newExplanation = initialData?.explanation || '';
241
- const newIsBackup = initialData?.isBackup || false;
242
- // Always apply new data when initialData changes (e.g. navigation)
243
- isInitializingRef.current = true;
244
- hasInitializedRef.current = true;
245
- setAudioUrl(newAudioUrl);
246
- setCorrectObjectId(newCorrect);
247
- setPoints(newPoints);
248
- setExplanation(newExplanation);
249
- setIsBackup(newIsBackup);
250
- // Give React time to batch before allowing onChange
251
- setTimeout(() => {
252
- isInitializingRef.current = false;
253
- }, 150);
254
- }, [initialData]);
255
- // Call onChange when form data changes
256
- useEffect(() => {
257
- if (isInitializingRef.current || !hasInitializedRef.current)
258
- return;
259
- const payload = {
260
- audioUrl,
261
- backgroundImageUrl,
262
- objectNodes,
263
- correctObjectId,
264
- points,
265
- explanation,
266
- isBackup,
267
- groups,
268
- groupNumber: currentGroupNumber,
269
- };
270
- debouncedOnChange(payload);
271
- }, [audioUrl, backgroundImageUrl, objectNodes, correctObjectId, points, explanation, isBackup, groups, currentGroupNumber, debouncedOnChange]);
272
- // ============ VALIDATION ============
273
- const validateForm = async () => {
274
- // Sync React state to react-hook-form before validation
275
- form.setValue('audioUrl', audioUrl);
276
- form.setValue('backgroundImageUrl', backgroundImageUrl);
277
- form.setValue('objectNodes', objectNodes);
278
- form.setValue('correctObjectId', correctObjectId);
279
- form.setValue('isBackup', isBackup);
280
- // Trigger Zod validation
281
- const isValid = await form.trigger();
282
- const formattedErrors = [];
283
- // Check custom business logic
284
- let isCustomValid = true;
285
- if (correctObjectId && !objectNodes.find((n) => n.id === correctObjectId)) {
286
- formattedErrors.push('Đáp án đúng không tồn tại trong danh sách đối tượng!');
287
- isCustomValid = false;
288
- }
289
- if (!isValid || !isCustomValid) {
290
- if (formattedErrors.length > 0) {
291
- // Set errors on form as a single merged string so we can display it simply
292
- form.setError('root', { message: formattedErrors.join('\n') });
293
- }
294
- return false;
295
- }
296
- form.clearErrors('root');
297
- return true;
298
- };
299
- const internalHandleSave = (callback) => {
300
- validateForm().then((isValid) => {
301
- if (isValid) {
302
- callback?.({
303
- audioUrl,
304
- backgroundImageUrl,
305
- objectNodes,
306
- correctObjectId,
307
- points,
308
- explanation,
309
- isBackup,
310
- groups,
311
- groupNumber: currentGroupNumber,
312
- });
313
- }
314
- });
315
- };
316
- // ============ OBJECT NODE HANDLERS ============
317
- const addObjectNode = () => {
318
- const newNode = {
319
- id: generateObjectId(),
320
- imageUrl: '',
321
- label: '',
322
- x: 20 + (objectNodes.length * 15) % 60,
323
- y: 30 + (objectNodes.length * 10) % 40,
324
- width: 12,
325
- height: 15,
326
- };
327
- setObjectNodes([...objectNodes, newNode]);
328
- form.clearErrors();
329
- };
330
- const updateObjectNode = (id, updates) => {
331
- setObjectNodes(objectNodes.map((node) => (node.id === id ? { ...node, ...updates } : node)));
332
- form.clearErrors();
333
- };
334
- const removeObjectNode = (id) => {
335
- setObjectNodes(objectNodes.filter((node) => node.id !== id));
336
- if (correctObjectId === id) {
337
- setCorrectObjectId('');
338
- }
339
- form.clearErrors();
340
- };
341
- const handleNodeDrag = useCallback((id, x, y) => {
342
- setObjectNodes((prev) => prev.map((node) => (node.id === id ? { ...node, x, y } : node)));
343
- }, []);
344
- const handleNodeResize = useCallback((id, width, height) => {
345
- setObjectNodes((prev) => prev.map((node) => (node.id === id ? { ...node, width, height } : node)));
346
- }, []);
347
- // ============ RENDER ============
348
- // Preview mode
349
- if (isClientMode) {
350
- return (_jsxs("div", { className: "space-y-6", children: [_jsx(Card, { children: _jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { children: "Nghe v\u00E0 ch\u1ECDn \u0111\u1ED1i t\u01B0\u1EE3ng (Preview)" }), _jsx("p", { className: "mt-1 text-sm text-gray-500", children: "Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc - Hi\u1EC3n th\u1ECB \u0111\u00E1p \u00E1n \u0111\u00FAng" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Quay l\u1EA1i ch\u1EC9nh s\u1EEDa"] })] }) }) }), _jsx(ListenAndChooseObjectsInSceneClient, { questionData: {
351
- audioUrl: audioPreviewUrl || audioUrl,
352
- backgroundImageUrl: backgroundPreviewUrl || backgroundImageUrl,
353
- objectNodes,
354
- }, isReviewMode: true, userAnswer: correctObjectId ? { objectId: correctObjectId } : undefined, correctAnswer: correctObjectId ? { objectId: correctObjectId } : undefined, explanation: explanation })] }));
355
- }
356
- const formErrors = form.formState.errors;
357
- // Creator mode
358
- return (_jsxs("div", { className: "space-y-6", children: [_jsx(CreatorGuide, { colorScheme: "purple", steps: [
359
- { text: 'Upload ảnh nền (Scene) — ảnh này dùng chung cho tất cả câu hỏi trong nhóm' },
360
- { text: 'Nhấn "Thêm đối tượng" để tạo các object trên ảnh nền. Mỗi đối tượng cần có tên và hình ảnh riêng' },
361
- { text: 'Kéo thả đối tượng trên ảnh nền để điều chỉnh vị trí, hoặc nhập tọa độ X, Y, Width, Height thủ công' },
362
- { text: 'Upload audio câu hỏi — học sinh sẽ nghe audio rồi chọn đúng đối tượng trên ảnh' },
363
- { text: 'Chọn đáp án đúng cho câu hỏi này từ danh sách đối tượng đã tạo', highlight: true },
364
- { text: 'Thêm giải thích (không bắt buộc) — sẽ hiển thị sau khi học sinh trả lời' },
365
- ], tips: [
366
- 'Số đối tượng nên nhiều hơn số câu hỏi trong part để tạo lựa chọn nhiễu. Đặt tên rõ ràng và chọn hình ảnh dễ phân biệt giữa các đối tượng',
367
- 'Kéo thả trực tiếp trên ảnh nền sẽ nhanh hơn nhập tọa độ thủ công',
368
- ] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-purple-100/50", children: [_jsx("div", { className: "h-1.5 bg-gradient-to-r from-purple-500 via-violet-500 to-fuchsia-500" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-violet-600 shadow-md", children: _jsx(ListChecks, { className: "h-5 w-5 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-xl font-bold text-gray-900", children: "Nh\u00F3m \u0111\u00E1p \u00E1n" }), _jsxs("div", { className: "mt-1.5 inline-flex items-center gap-1.5 rounded-full bg-purple-100 px-3 py-1 text-sm font-medium text-purple-700", children: [_jsx("span", { children: "\uD83D\uDDBC\uFE0F" }), _jsxs("span", { children: ["Nh\u00F3m ", currentGroupNumber] }), _jsx("span", { className: "text-xs font-normal opacity-75 ml-1", children: "(D\u00F9ng chung)" })] })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: !backgroundImageUrl || objectNodes.length < 2, className: "gap-2 border-purple-200 text-purple-600 hover:bg-purple-50 hover:text-purple-700", children: [_jsx(Eye, { className: "h-4 w-4" }), "Preview"] })] }) })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-purple-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-purple-400 to-violet-400" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-purple-100", children: _jsx(ImageIcon, { className: "h-3.5 w-3.5 text-purple-600" }) }), _jsx(CardTitle, { className: "text-lg font-semibold text-gray-800", children: "\u1EA2nh n\u1EC1n & \u0110\u1ED1i t\u01B0\u1EE3ng trong Scene" })] }) }), _jsxs(CardContent, { className: "space-y-6", children: [_jsx("div", { className: `rounded-xl border-2 p-5 bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-200`, children: _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-blue-800", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-blue-600" }), "\u1EA2nh n\u1EC1n (Scene) ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(FileUpload, { id: "scene-background", label: "", accept: "image/*", value: backgroundImageUrl, onChange: (url) => {
369
- setBackgroundImageUrl(url);
370
- form.clearErrors();
371
- }, onPresignedUrlChange: setBackgroundPreviewUrl, maxSize: 10, placeholder: "Upload h\u00ECnh \u1EA3nh n\u1EC1n c\u1EA3nh", autoUpload: true, prefix: "questions/scenes" }), formErrors.backgroundImageUrl && (_jsx("p", { className: "mt-1 text-sm text-red-500", children: formErrors.backgroundImageUrl.message })), backgroundPreviewUrl && (_jsxs("div", { className: "mt-2 overflow-hidden rounded-lg border border-gray-200 bg-gray-50 isolate", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-1.5", children: _jsx("span", { className: "text-xs font-medium text-gray-500", children: "\u1EA2nh n\u1EC1n - K\u00E9o th\u1EA3 \u0111\u1ED1i t\u01B0\u1EE3ng \u0111\u1EC3 \u0111\u1ECBnh v\u1ECB" }) }), _jsx("div", { className: "p-2", children: _jsx(ScenePreviewWithDrag, { backgroundUrl: backgroundPreviewUrl, objectNodes: objectNodes, correctObjectId: correctObjectId, onNodeDrag: handleNodeDrag, onNodeResize: handleNodeResize }) })] }))] }) }), _jsxs("div", { className: "pt-4 border-t border-gray-100", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-indigo-100", children: _jsx(MousePointerClick, { className: "h-3.5 w-3.5 text-indigo-600" }) }), _jsxs("h3", { className: "text-lg font-semibold text-gray-800", children: ["\u0110\u1ED1i t\u01B0\u1EE3ng (", objectNodes.length, " items)"] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: addObjectNode, className: "gap-2 border-green-200 text-green-600 hover:bg-green-50 hover:text-green-700", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm \u0111\u1ED1i t\u01B0\u1EE3ng"] })] }), _jsx("div", { className: "space-y-3", children: objectNodes.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center gap-3 rounded-xl border-2 border-dashed border-gray-200 bg-gray-50/50 py-10", children: [_jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-gray-100", children: _jsx(ImageIcon, { className: "h-6 w-6 text-gray-400" }) }), _jsx("p", { className: "text-sm text-gray-500", children: "Ch\u01B0a c\u00F3 \u0111\u1ED1i t\u01B0\u1EE3ng n\u00E0o. Nh\u1EA5n \"Th\u00EAm \u0111\u1ED1i t\u01B0\u1EE3ng\" \u0111\u1EC3 t\u1EA1o c\u00E1c \u0111\u1ED1i t\u01B0\u1EE3ng chung cho nh\u00F3m." })] })) : (objectNodes.map((node, index) => (_jsx(SceneObjectEditor, { node: node, index: index, isCorrect: false, onUpdate: updateObjectNode, onRemove: removeObjectNode, onSelectCorrect: () => { }, errors: {
372
- label: formErrors.objectNodes?.[index]?.label?.message,
373
- imageUrl: formErrors.objectNodes?.[index]?.imageUrl?.message,
374
- } }, node.id)))) })] })] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-green-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-green-400 to-emerald-400" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-green-100", children: _jsx(Volume2, { className: "h-4 w-4 text-green-600" }) }), _jsx(CardTitle, { className: "text-lg font-semibold text-gray-800", children: "Th\u00F4ng tin c\u00E2u h\u1ECFi" })] }) }), _jsxs(CardContent, { className: "space-y-6 pt-2", children: [_jsx("div", { className: "rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4", children: _jsxs("div", { className: "flex flex-col md:flex-row gap-6", children: [_jsx("div", { className: "flex-1", children: _jsx(PointsInput, { value: points, onChange: (e) => setPoints(parseFloat(e.target.value) || 1) }) }), _jsx("div", { className: "flex-1 flex flex-col justify-center" })] }) }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Volume2, { className: "h-4 w-4 text-indigo-500" }), "Audio c\u00E2u h\u1ECFi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(FileUpload, { id: "scene-audio", label: "", accept: "audio/*", value: audioUrl, onChange: (url) => {
375
- setAudioUrl(url);
376
- form.clearErrors();
377
- }, onPresignedUrlChange: setAudioPreviewUrl, maxSize: 10, placeholder: "Upload file audio c\u00E2u h\u1ECFi", autoUpload: true, prefix: "questions/audio", examId: examId || undefined, partId: partId || undefined }), formErrors.audioUrl && (_jsx("p", { className: "mt-1 text-sm text-red-500", children: formErrors.audioUrl.message })), audioPreviewUrl && (_jsx("audio", { controls: true, className: "mt-2 w-full", src: audioPreviewUrl, children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." }))] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-green-100", children: _jsx(Lightbulb, { className: "h-3.5 w-3.5 text-green-600" }) }), _jsxs(Label, { className: "text-base font-semibold text-gray-800", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng c\u1EE7a c\u00E2u h\u1ECFi n\u00E0y ", _jsx("span", { className: "text-red-500", children: "*" })] })] }), objectNodes.length === 0 ? (_jsx("p", { className: "text-sm text-amber-600 italic", children: "Vui l\u00F2ng th\u00EAm \u0111\u1ED1i t\u01B0\u1EE3ng \u1EDF b\u00EAn tr\u00EAn tr\u01B0\u1EDBc khi ch\u1ECDn \u0111\u00E1p \u00E1n." })) : (_jsx(RadioGroup, { value: correctObjectId, onValueChange: (val) => {
378
- setCorrectObjectId(val);
379
- form.clearErrors(); // Xóa lỗi validation khi người dùng đã chọn đáp án
380
- }, className: "grid gap-3 sm:grid-cols-2 md:grid-cols-3", children: objectNodes.map((node, index) => (_jsxs("label", { className: `flex cursor-pointer items-start gap-3 rounded-lg border-2 p-3 transition-all ${correctObjectId === node.id
381
- ? 'border-green-500 bg-green-50 ring-1 ring-green-500'
382
- : 'border-gray-200 hover:border-green-300 hover:bg-green-50/50'}`, children: [_jsx(RadioGroupItem, { value: node.id, id: `correct-${node.id}`, className: "mt-1 flex-shrink-0" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("p", { className: "font-medium text-gray-900 line-clamp-1 break-all", children: [index + 1, ". ", node.label || '(Chưa đặt tên)'] }), node.imageUrl ? (_jsx("div", { className: "mt-2 h-12 w-full max-w-16 rounded overflow-hidden bg-white border border-gray-200", children: _jsx(PresignedImage, { url: node.imageUrl, alt: node.label }) })) : (_jsx("div", { className: "mt-2 text-xs text-gray-400 italic", children: "Ch\u01B0a c\u00F3 h\u00ECnh" }))] })] }, node.id))) }))] }), formErrors.correctObjectId && (_jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-3 mt-3", children: _jsxs("p", { className: "text-sm text-red-700", children: ["\u26A0\uFE0F ", formErrors.correctObjectId.message] }) }))] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-amber-400 to-orange-400" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 shadow-sm", children: _jsx(BookOpen, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-lg font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" }), _jsx("span", { className: "inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2.5 py-0.5 text-xs font-medium text-amber-600", children: "Kh\u00F4ng b\u1EAFt bu\u1ED9c" })] }), _jsx("p", { className: "text-sm text-gray-500 mt-0.5", children: "Gi\u1EA3i th\u00EDch cho \u0111\u00E1p \u00E1n \u0111\u00FAng \u2014 hi\u1EC3n th\u1ECB sau khi h\u1ECDc sinh ho\u00E0n th\u00E0nh c\u00E2u h\u1ECFi" })] })] }) }), _jsx(CardContent, { children: _jsx(Textarea, { value: explanation, onChange: (e) => setExplanation(e.target.value), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", rows: 3, className: "min-h-[80px] border-gray-200 bg-white transition-all focus:border-amber-300 focus:ring-amber-200 resize-none" }) })] })] }));
383
- }
384
- export function ListenAndChooseObjectsInSceneCreator(props) {
385
- return _jsx(ListenAndChooseObjectsInSceneCreatorContent, { ...props });
386
- }
1
+ "use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useState as t,useRef as a,useEffect as n,useCallback as i}from"react";import{Button as l}from"../../../../components/ui/button";import{Label as s}from"../../../../components/ui/label";import{Input as o}from"../../../../components/ui/input";import{Textarea as d}from"../../../../components/ui/textarea";import{Card as c,CardContent as h,CardHeader as m,CardTitle as g}from"../../../../components/ui/card";import{PointsInput as u}from"../../../../components/ui/points-input";import{RadioGroup as p,RadioGroupItem as b}from"../../../../components/ui/radio-group";import{Pencil as x,Eye as f,Plus as v,Trash2 as N,Image as w,Volume2 as y,Lightbulb as j,MousePointerClick as U,GripVertical as C,ListChecks as k,BookOpen as I}from"lucide-react";import{FileUpload as S}from"../../../../components/ui/file-upload";import{usePresignedFileUrl as z,useDebouncedCallback as O}from"../../../../shared/lib/hooks";import{useReactHookForm as D}from"../../../../shared/lib/hooks/useReactHookForm";import{listenAndChooseObjectsInSceneFormSchema as M}from"../../../../shared/lib/validation/listen-and-choose-objects-in-scene.validation";import{ListenAndChooseObjectsInSceneClient as E}from"./ListenAndChooseObjectsInSceneClient";import{useSceneQuestionGroup as B}from"../../_shared/hooks/useSceneQuestionGroup";import{generateObjectId as R}from"../../_shared/utils/id-generator";import{useExamQuestionStore as V}from"../../../../shared/lib/stores/examQuestionStore";import{CreatorGuide as P}from"../../components/CreatorGuide";function T({node:a,index:i,isCorrect:d,onUpdate:c,onRemove:h,onSelectCorrect:m,errors:g}){const[u,p]=t(""),{previewUrl:b}=z(a.imageUrl||"");return n(()=>{b&&p(b)},[b]),e("div",{className:"rounded-lg border-2 p-4 transition-all "+(d?"border-green-300 bg-green-50/50":"border-gray-200 bg-white hover:border-gray-300"),children:r("div",{className:"flex items-start gap-3",children:[r("div",{className:"flex flex-col items-center gap-1 pt-1",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-sm font-bold text-white shadow-sm",children:i+1}),e(C,{className:"h-4 w-4 text-gray-400"})]}),r("div",{className:"flex-1 space-y-3",children:[r("div",{className:"flex items-center gap-3",children:[r("div",{className:"flex-1",children:[e(s,{className:"text-xs font-medium text-gray-500",children:"Tên đối tượng"}),e(o,{value:a.label,onChange:e=>c(a.id,{label:e.target.value}),placeholder:"Ví dụ: cow, frog, horse...",className:"mt-1 "+(g?.label?"border-red-500":"border-gray-200")}),g?.label&&e("p",{className:"mt-1 text-xs text-red-500",children:g.label})]}),e("div",{className:"flex items-center gap-2 pt-5"})]}),r("div",{children:[e(s,{className:"text-xs font-medium text-gray-500",children:"Hình ảnh đối tượng"}),r("div",{className:"mt-1 flex items-center gap-3",children:[r("div",{className:"flex-1",children:[e(S,{id:`object-image-${a.id}`,label:"",accept:"image/*",value:a.imageUrl||"",onChange:e=>c(a.id,{imageUrl:e}),onPresignedUrlChange:p,maxSize:5,placeholder:"Upload hình ảnh đối tượng",autoUpload:!0,prefix:"questions/objects"}),g?.imageUrl&&e("p",{className:"mt-1 text-xs text-red-500",children:g.imageUrl})]}),u&&e("div",{className:"h-16 w-16 flex-shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-gray-50",children:e("img",{src:u,alt:a.label||"Object",className:"h-full w-full object-contain"})})]})]}),r("div",{className:"grid grid-cols-4 gap-2",children:[r("div",{children:[e(s,{className:"text-xs font-medium text-gray-500",children:"X (%)"}),e(o,{type:"number",min:0,max:100,value:a.x,onChange:e=>c(a.id,{x:parseFloat(e.target.value)||0}),className:"mt-1 border-gray-200"})]}),r("div",{children:[e(s,{className:"text-xs font-medium text-gray-500",children:"Y (%)"}),e(o,{type:"number",min:0,max:100,value:a.y,onChange:e=>c(a.id,{y:parseFloat(e.target.value)||0}),className:"mt-1 border-gray-200"})]}),r("div",{children:[e(s,{className:"text-xs font-medium text-gray-500",children:"Width (%)"}),e(o,{type:"number",min:1,max:100,value:a.width,onChange:e=>c(a.id,{width:parseFloat(e.target.value)||10}),className:"mt-1 border-gray-200"})]}),r("div",{children:[e(s,{className:"text-xs font-medium text-gray-500",children:"Height (%)"}),e(o,{type:"number",min:1,max:100,value:a.height,onChange:e=>c(a.id,{height:parseFloat(e.target.value)||10}),className:"mt-1 border-gray-200"})]})]})]}),e(l,{variant:"ghost",size:"sm",onClick:()=>h(a.id),className:"mt-1 text-red-400 hover:bg-red-50 hover:text-red-600",children:e(N,{className:"h-4 w-4"})})]})})}function q({backgroundUrl:l,objectNodes:s,correctObjectId:o,onNodeDrag:d,onNodeResize:c}){const h=a(null),[m,g]=t(null),[u,p]=t({x:0,y:0}),[b,x]=t(null),[f,v]=t(null),[N,y]=t({x:0,y:0}),[j,U]=t({width:0,height:0}),C=(e,r,t,a,n)=>{n.preventDefault(),n.stopPropagation(),x(e),v(r),y({x:n.clientX,y:n.clientY}),U({width:t,height:a})},k=i(e=>{if(h.current)if(m){const r=h.current.getBoundingClientRect(),t=(e.clientX-r.left)/r.width*100,a=(e.clientY-r.top)/r.height*100,n=Math.max(0,Math.min(100,t-u.x)),i=Math.max(0,Math.min(100,a-u.y));d(m,Math.round(10*n)/10,Math.round(10*i)/10)}else if(b&&f){const r=h.current.getBoundingClientRect(),t=(e.clientX-N.x)/r.width*100,a=(e.clientY-N.y)/r.height*100;let n=j.width,i=j.height;"e"!==f&&"se"!==f||(n=Math.max(1,j.width+t)),"s"!==f&&"se"!==f||(i=Math.max(1,j.height+a)),c(b,Math.round(10*n)/10,Math.round(10*i)/10)}},[m,b,f,N,j,u,d,c]),I=i(()=>{g(null),x(null),v(null)},[]);return n(()=>{if(m||b)return window.addEventListener("mousemove",k),window.addEventListener("mouseup",I),()=>{window.removeEventListener("mousemove",k),window.removeEventListener("mouseup",I)}},[m,b,k,I]),r("div",{ref:h,className:"relative w-full select-none",children:[e("img",{src:l,alt:"Scene background",className:"w-full rounded-md object-contain",draggable:!1}),s.map(t=>{const a=t.id===o,n=m===t.id||b===t.id;return r("div",{className:"absolute cursor-move rounded-md border-2 transition-colors group "+(n?"border-blue-500 shadow-lg":a?"border-green-400 shadow-md":"border-gray-300 hover:border-blue-400"),style:{left:`${t.x}%`,top:`${t.y}%`,width:`${t.width}%`,height:`${t.height}%`,zIndex:n?2:1},onMouseDown:e=>((e,r,t,a)=>{if(a.preventDefault(),!a.target.dataset.resizeHandle&&(g(e),h.current)){const e=h.current.getBoundingClientRect(),n=(a.clientX-e.left)/e.width*100,i=(a.clientY-e.top)/e.height*100;p({x:n-r,y:i-t})}})(t.id,t.x,t.y,e),children:[r("div",{className:"absolute -top-5 left-0 rounded bg-black/70 px-1.5 py-0.5 text-[10px] font-medium text-white whitespace-nowrap",children:[t.label||`Obj ${s.indexOf(t)+1}`,a&&" ✓"]}),e("div",{className:"flex h-full w-full items-center justify-center overflow-hidden rounded bg-transparent",children:t.imageUrl?e(F,{url:t.imageUrl,alt:t.label}):e("div",{className:"flex bg-white/30 p-2 rounded items-center justify-center",children:e(w,{className:"h-4 w-4 text-gray-400"})})}),e("div",{"data-resize-handle":"true",className:"absolute -right-1.5 top-1/2 h-3 w-3 -translate-y-1/2 cursor-e-resize rounded-full border border-blue-500 bg-white opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100",style:{opacity:n?1:void 0},onMouseDown:e=>C(t.id,"e",t.width,t.height,e)}),e("div",{"data-resize-handle":"true",className:"absolute -bottom-1.5 left-1/2 h-3 w-3 -translate-x-1/2 cursor-s-resize rounded-full border border-blue-500 bg-white opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100",style:{opacity:n?1:void 0},onMouseDown:e=>C(t.id,"s",t.width,t.height,e)}),e("div",{"data-resize-handle":"true",className:"absolute -bottom-1.5 -right-1.5 h-3 w-3 cursor-se-resize rounded-full border border-blue-500 bg-white opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100",style:{opacity:n?1:void 0},onMouseDown:e=>C(t.id,"se",t.width,t.height,e)})]},t.id)})]})}function F({url:r,alt:t}){const{previewUrl:a}=z(r);return a?e("img",{src:a,alt:t,className:"h-full w-full object-fill pointer-events-none",draggable:!1}):e(w,{className:"h-4 w-4 text-gray-400"})}function L({initialData:o,onSave:N,onSaveToQuestionBank:C,onCancel:L,onChange:$,onUnsavedChangesChange:A,validationRef:G,externalErrors:H,groups:X,questionIndexInPart:Y=0,partId:K=""}){console.log("[ListenAndChooseObjects_Creator] RENDER",{initialData:o,questionIndexInPart:Y,partId:K,groups:X});const Q=V(e=>e.examId),_={backgroundImageUrl:o?.backgroundImageUrl||"",objectNodes:o?.objectNodes||[]},{isGrouped:J,currentGroupNumber:W,backgroundImageUrl:Z,objectNodes:ee,setBackgroundImageUrl:re,setObjectNodes:te}=B({partId:K,groups:X,questionIndexInPart:Y,initialData:_}),[ae,ne]=t(o?.audioUrl||""),[ie,le]=t(o?.correctObjectId||""),[se,oe]=t(o?.points||1),[de,ce]=t(o?.explanation||""),[he,me]=t(o?.isBackup||!1),[ge,ue]=t(!1),[pe,be]=t(""),[xe,fe]=t(""),ve=D({defaultValues:{audioUrl:ae,backgroundImageUrl:Z,objectNodes:ee,correctObjectId:ie,isBackup:he},zodSchema:M,mode:"onBlur"}),Ne=a(o),we=a(!0),ye=a($),je=a(!1),{previewUrl:Ue}=z(Z),{previewUrl:Ce}=z(ae);n(()=>{Ue&&be(Ue)},[Ue]),n(()=>{Ce&&fe(Ce)},[Ce]),n(()=>{ye.current=$},[$]);const ke=O(e=>{ye.current?.(e)},300);n(()=>{G&&(G.current={triggerValidation:async()=>Ie(),getFormData:()=>({audioUrl:ae,backgroundImageUrl:Z,objectNodes:ee,correctObjectId:ie,points:se,explanation:de,isBackup:he})})},[G,ae,Z,ee,ie,se,de,he]),n(()=>{if(!o)return void(je.current||(je.current=!0,we.current=!1));if(!(JSON.stringify(Ne.current)!==JSON.stringify(o))&&je.current)return;Ne.current=o;const e=o?.audioUrl||"",r=o?.correctObjectId||"",t=o?.points||1,a=o?.explanation||"",n=o?.isBackup||!1;we.current=!0,je.current=!0,ne(e),le(r),oe(t),ce(a),me(n),setTimeout(()=>{we.current=!1},150)},[o]),n(()=>{if(we.current||!je.current)return;ke({audioUrl:ae,backgroundImageUrl:Z,objectNodes:ee,correctObjectId:ie,points:se,explanation:de,isBackup:he,groups:X,groupNumber:W})},[ae,Z,ee,ie,se,de,he,X,W,ke]);const Ie=async()=>{ve.setValue("audioUrl",ae),ve.setValue("backgroundImageUrl",Z),ve.setValue("objectNodes",ee),ve.setValue("correctObjectId",ie),ve.setValue("isBackup",he);const e=await ve.trigger(),r=[];let t=!0;return ie&&!ee.find(e=>e.id===ie)&&(r.push("Đáp án đúng không tồn tại trong danh sách đối tượng!"),t=!1),e&&t?(ve.clearErrors("root"),!0):(r.length>0&&ve.setError("root",{message:r.join("\n")}),!1)},Se=(e,r)=>{te(ee.map(t=>t.id===e?{...t,...r}:t)),ve.clearErrors()},ze=e=>{te(ee.filter(r=>r.id!==e)),ie===e&&le(""),ve.clearErrors()},Oe=i((e,r,t)=>{te(a=>a.map(a=>a.id===e?{...a,x:r,y:t}:a))},[]),De=i((e,r,t)=>{te(a=>a.map(a=>a.id===e?{...a,width:r,height:t}:a))},[]);if(ge)return r("div",{className:"space-y-6",children:[e(c,{children:e(m,{children:r("div",{className:"flex items-center justify-between",children:[r("div",{children:[e(g,{children:"Nghe và chọn đối tượng (Preview)"}),e("p",{className:"mt-1 text-sm text-gray-500",children:"Chế độ xem trước - Hiển thị đáp án đúng"})]}),r(l,{variant:"outline",size:"sm",onClick:()=>ue(!1),children:[e(x,{className:"mr-2 h-4 w-4"}),"Quay lại chỉnh sửa"]})]})})}),e(E,{questionData:{audioUrl:xe||ae,backgroundImageUrl:pe||Z,objectNodes:ee},isReviewMode:!0,userAnswer:ie?{objectId:ie}:void 0,correctAnswer:ie?{objectId:ie}:void 0,explanation:de})]});const Me=ve.formState.errors;return r("div",{className:"space-y-6",children:[e(P,{colorScheme:"purple",steps:[{text:"Upload ảnh nền (Scene) — ảnh này dùng chung cho tất cả câu hỏi trong nhóm"},{text:'Nhấn "Thêm đối tượng" để tạo các object trên ảnh nền. Mỗi đối tượng cần có tên và hình ảnh riêng'},{text:"Kéo thả đối tượng trên ảnh nền để điều chỉnh vị trí, hoặc nhập tọa độ X, Y, Width, Height thủ công"},{text:"Upload audio câu hỏi — học sinh sẽ nghe audio rồi chọn đúng đối tượng trên ảnh"},{text:"Chọn đáp án đúng cho câu hỏi này từ danh sách đối tượng đã tạo",highlight:!0},{text:"Thêm giải thích (không bắt buộc) — sẽ hiển thị sau khi học sinh trả lời"}],tips:["Số đối tượng nên nhiều hơn số câu hỏi trong part để tạo lựa chọn nhiễu. Đặt tên rõ ràng và chọn hình ảnh dễ phân biệt giữa các đối tượng","Kéo thả trực tiếp trên ảnh nền sẽ nhanh hơn nhập tọa độ thủ công"]}),r(c,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-purple-100/50",children:[e("div",{className:"h-1.5 bg-gradient-to-r from-purple-500 via-violet-500 to-fuchsia-500"}),e(m,{className:"pb-4",children:r("div",{className:"flex items-center justify-between",children:[r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-violet-600 shadow-md",children:e(k,{className:"h-5 w-5 text-white"})}),r("div",{children:[e(g,{className:"text-xl font-bold text-gray-900",children:"Nhóm đáp án"}),r("div",{className:"mt-1.5 inline-flex items-center gap-1.5 rounded-full bg-purple-100 px-3 py-1 text-sm font-medium text-purple-700",children:[e("span",{children:"🖼️"}),r("span",{children:["Nhóm ",W]}),e("span",{className:"text-xs font-normal opacity-75 ml-1",children:"(Dùng chung)"})]})]})]}),r(l,{variant:"outline",size:"sm",onClick:()=>ue(!0),disabled:!Z||ee.length<2,className:"gap-2 border-purple-200 text-purple-600 hover:bg-purple-50 hover:text-purple-700",children:[e(f,{className:"h-4 w-4"}),"Preview"]})]})})]}),r(c,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-purple-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-purple-400 to-violet-400"}),e(m,{className:"pb-4",children:r("div",{className:"flex items-center gap-2",children:[e("div",{className:"flex h-6 w-6 items-center justify-center rounded-md bg-purple-100",children:e(w,{className:"h-3.5 w-3.5 text-purple-600"})}),e(g,{className:"text-lg font-semibold text-gray-800",children:"Ảnh nền & Đối tượng trong Scene"})]})}),r(h,{className:"space-y-6",children:[e("div",{className:"rounded-xl border-2 p-5 bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-200",children:r("div",{className:"space-y-2",children:[r(s,{className:"flex items-center gap-2 text-base font-semibold text-blue-800",children:[e(w,{className:"h-4 w-4 text-blue-600"}),"Ảnh nền (Scene) ",e("span",{className:"text-red-500",children:"*"})]}),e(S,{id:"scene-background",label:"",accept:"image/*",value:Z,onChange:e=>{re(e),ve.clearErrors()},onPresignedUrlChange:be,maxSize:10,placeholder:"Upload hình ảnh nền cảnh",autoUpload:!0,prefix:"questions/scenes"}),Me.backgroundImageUrl&&e("p",{className:"mt-1 text-sm text-red-500",children:Me.backgroundImageUrl.message}),pe&&r("div",{className:"mt-2 overflow-hidden rounded-lg border border-gray-200 bg-gray-50 isolate",children:[e("div",{className:"border-b border-gray-100 px-3 py-1.5",children:e("span",{className:"text-xs font-medium text-gray-500",children:"Ảnh nền - Kéo thả đối tượng để định vị"})}),e("div",{className:"p-2",children:e(q,{backgroundUrl:pe,objectNodes:ee,correctObjectId:ie,onNodeDrag:Oe,onNodeResize:De})})]})]})}),r("div",{className:"pt-4 border-t border-gray-100",children:[r("div",{className:"flex items-center justify-between mb-4",children:[r("div",{className:"flex items-center gap-2",children:[e("div",{className:"flex h-6 w-6 items-center justify-center rounded-md bg-indigo-100",children:e(U,{className:"h-3.5 w-3.5 text-indigo-600"})}),r("h3",{className:"text-lg font-semibold text-gray-800",children:["Đối tượng (",ee.length," items)"]})]}),r(l,{variant:"outline",size:"sm",onClick:()=>{const e={id:R(),imageUrl:"",label:"",x:20+15*ee.length%60,y:30+10*ee.length%40,width:12,height:15};te([...ee,e]),ve.clearErrors()},className:"gap-2 border-green-200 text-green-600 hover:bg-green-50 hover:text-green-700",children:[e(v,{className:"h-4 w-4"}),"Thêm đối tượng"]})]}),e("div",{className:"space-y-3",children:0===ee.length?r("div",{className:"flex flex-col items-center gap-3 rounded-xl border-2 border-dashed border-gray-200 bg-gray-50/50 py-10",children:[e("div",{className:"flex h-12 w-12 items-center justify-center rounded-full bg-gray-100",children:e(w,{className:"h-6 w-6 text-gray-400"})}),e("p",{className:"text-sm text-gray-500",children:'Chưa có đối tượng nào. Nhấn "Thêm đối tượng" để tạo các đối tượng chung cho nhóm.'})]}):ee.map((r,t)=>e(T,{node:r,index:t,isCorrect:!1,onUpdate:Se,onRemove:ze,onSelectCorrect:()=>{},errors:{label:Me.objectNodes?.[t]?.label?.message,imageUrl:Me.objectNodes?.[t]?.imageUrl?.message}},r.id))})]})]})]}),r(c,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-green-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-green-400 to-emerald-400"}),e(m,{className:"pb-4",children:r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-green-100",children:e(y,{className:"h-4 w-4 text-green-600"})}),e(g,{className:"text-lg font-semibold text-gray-800",children:"Thông tin câu hỏi"})]})}),r(h,{className:"space-y-6 pt-2",children:[e("div",{className:"rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4",children:r("div",{className:"flex flex-col md:flex-row gap-6",children:[e("div",{className:"flex-1",children:e(u,{value:se,onChange:e=>oe(parseFloat(e.target.value)||1)})}),e("div",{className:"flex-1 flex flex-col justify-center"})]})}),r("div",{className:"space-y-2",children:[r(s,{className:"flex items-center gap-2 text-base font-semibold text-gray-800",children:[e(y,{className:"h-4 w-4 text-indigo-500"}),"Audio câu hỏi ",e("span",{className:"text-red-500",children:"*"})]}),e(S,{id:"scene-audio",label:"",accept:"audio/*",value:ae,onChange:e=>{ne(e),ve.clearErrors()},onPresignedUrlChange:fe,maxSize:10,placeholder:"Upload file audio câu hỏi",autoUpload:!0,prefix:"questions/audio",examId:Q||void 0,partId:K||void 0}),Me.audioUrl&&e("p",{className:"mt-1 text-sm text-red-500",children:Me.audioUrl.message}),xe&&e("audio",{controls:!0,className:"mt-2 w-full",src:xe,children:"Trình duyệt không hỗ trợ audio."})]}),r("div",{className:"space-y-3",children:[r("div",{className:"flex items-center gap-2",children:[e("div",{className:"flex h-6 w-6 items-center justify-center rounded-md bg-green-100",children:e(j,{className:"h-3.5 w-3.5 text-green-600"})}),r(s,{className:"text-base font-semibold text-gray-800",children:["Đáp án đúng của câu hỏi này ",e("span",{className:"text-red-500",children:"*"})]})]}),0===ee.length?e("p",{className:"text-sm text-amber-600 italic",children:"Vui lòng thêm đối tượng ở bên trên trước khi chọn đáp án."}):e(p,{value:ie,onValueChange:e=>{le(e),ve.clearErrors()},className:"grid gap-3 sm:grid-cols-2 md:grid-cols-3",children:ee.map((t,a)=>r("label",{className:"flex cursor-pointer items-start gap-3 rounded-lg border-2 p-3 transition-all "+(ie===t.id?"border-green-500 bg-green-50 ring-1 ring-green-500":"border-gray-200 hover:border-green-300 hover:bg-green-50/50"),children:[e(b,{value:t.id,id:`correct-${t.id}`,className:"mt-1 flex-shrink-0"}),r("div",{className:"min-w-0 flex-1",children:[r("p",{className:"font-medium text-gray-900 line-clamp-1 break-all",children:[a+1,". ",t.label||"(Chưa đặt tên)"]}),t.imageUrl?e("div",{className:"mt-2 h-12 w-full max-w-16 rounded overflow-hidden bg-white border border-gray-200",children:e(F,{url:t.imageUrl,alt:t.label})}):e("div",{className:"mt-2 text-xs text-gray-400 italic",children:"Chưa có hình"})]})]},t.id))})]}),Me.correctObjectId&&e("div",{className:"rounded-lg border border-red-200 bg-red-50 p-3 mt-3",children:r("p",{className:"text-sm text-red-700",children:["⚠️ ",Me.correctObjectId.message]})})]})]}),r(c,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-amber-400 to-orange-400"}),e(m,{className:"pb-4",children:r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 shadow-sm",children:e(I,{className:"h-4 w-4 text-white"})}),r("div",{children:[r("div",{className:"flex items-center gap-2",children:[e(g,{className:"text-lg font-semibold text-gray-800",children:"Giải thích"}),e("span",{className:"inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2.5 py-0.5 text-xs font-medium text-amber-600",children:"Không bắt buộc"})]}),e("p",{className:"text-sm text-gray-500 mt-0.5",children:"Giải thích cho đáp án đúng — hiển thị sau khi học sinh hoàn thành câu hỏi"})]})]})}),e(h,{children:e(d,{value:de,onChange:e=>ce(e.target.value),placeholder:"VD: Câu trả lời đúng là... vì...",rows:3,className:"min-h-[80px] border-gray-200 bg-white transition-all focus:border-amber-300 focus:ring-amber-200 resize-none"})})]})]})}export function ListenAndChooseObjectsInSceneCreator(r){return e(L,{...r})}
@@ -1,2 +1 @@
1
- export * from './ListenAndChooseObjectsInSceneCreator';
2
- export * from './ListenAndChooseObjectsInSceneClient';
1
+ export*from"./ListenAndChooseObjectsInSceneCreator";export*from"./ListenAndChooseObjectsInSceneClient";