@tinyweb_dev/oe-exam-sdk 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (485) hide show
  1. package/dist/api/exam-create.js +1 -179
  2. package/dist/api/exam-create.types.js +1 -1
  3. package/dist/api/exam-questions.js +1 -98
  4. package/dist/api/exam-questions.types.js +1 -1
  5. package/dist/api/exam-taking.js +1 -73
  6. package/dist/api/exam-taking.types.js +1 -1
  7. package/dist/api/index.js +1 -6
  8. package/dist/api/result-review.js +1 -50
  9. package/dist/api/result-review.types.js +1 -1
  10. package/dist/components/CueCardEditor.js +1 -58
  11. package/dist/components/common/ResolvedImage.js +1 -15
  12. package/dist/components/exams/CreateExamInfoForm.js +1 -35
  13. package/dist/components/exams/CreateExamPageContainer.js +1 -103
  14. package/dist/components/exams/ExamCreator.js +1 -89
  15. package/dist/components/exams/ExamPartTabs.js +1 -40
  16. package/dist/components/exams/ExamQuestionGrid.js +1 -18
  17. package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
  18. package/dist/components/exams/PartAudioUploader.js +1 -9
  19. package/dist/components/exams/TemplateSelector.js +1 -66
  20. package/dist/components/exams/api.js +1 -3
  21. package/dist/components/exams/create-exam.validation.js +1 -18
  22. package/dist/components/exams/exam-template.utils.js +1 -18
  23. package/dist/components/exams/index.js +1 -10
  24. package/dist/components/exams/take/ExamTakingPageContainer.js +1 -153
  25. package/dist/components/exams/take/GenericQuestionRenderer.js +1 -14
  26. package/dist/components/exams/take/exam-taking.utils.js +1 -88
  27. package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -24
  28. package/dist/components/exams/take/index.js +1 -2
  29. package/dist/components/exams/take/types.js +1 -1
  30. package/dist/components/index.js +1 -42
  31. package/dist/components/questions/_shared/config/question-types.config.js +1 -333
  32. package/dist/components/questions/_shared/constants/color-palette.js +1 -21
  33. package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +1 -105
  34. package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +1 -222
  35. package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +1 -140
  36. package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +1 -123
  37. package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +1 -50
  38. package/dist/components/questions/_shared/hooks/useQuestionFormState.js +1 -54
  39. package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +1 -123
  40. package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +1 -121
  41. package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +1 -70
  42. package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +1 -194
  43. package/dist/components/questions/_shared/index.js +1 -7
  44. package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
  45. package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
  46. package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +1 -3
  47. package/dist/components/questions/_shared/types/color-region.config.js +1 -1
  48. package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
  49. package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
  50. package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
  51. package/dist/components/questions/_shared/types/index.js +1 -12
  52. package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
  53. package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
  54. package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
  55. package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
  56. package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
  57. package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
  58. package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
  59. package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
  60. package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +1 -3
  61. package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
  62. package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +1 -3
  63. package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
  64. package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
  65. package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
  66. package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
  67. package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
  68. package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
  69. package/dist/components/questions/_shared/types/question-group.type.js +1 -11
  70. package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
  71. package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
  72. package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
  73. package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
  74. package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
  75. package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
  76. package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
  77. package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
  78. package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
  79. package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
  80. package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +1 -3
  81. package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
  82. package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
  83. package/dist/components/questions/_shared/utils/answer-builder.js +1 -79
  84. package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +1 -39
  85. package/dist/components/questions/_shared/utils/id-generator.js +1 -3
  86. package/dist/components/questions/_shared/utils/question-validation.js +1 -21
  87. package/dist/components/questions/components/ColorNode.js +1 -85
  88. package/dist/components/questions/components/ColorRegionClient.js +1 -545
  89. package/dist/components/questions/components/ColorRegionCreator.js +1 -420
  90. package/dist/components/questions/components/ColorRegionNode.js +1 -59
  91. package/dist/components/questions/components/CreatorGuide.js +1 -103
  92. package/dist/components/questions/components/NameNode.js +1 -26
  93. package/dist/components/questions/components/QuestionBankOption.js +1 -46
  94. package/dist/components/questions/components/QuestionBasicInfoForm.js +1 -9
  95. package/dist/components/questions/components/QuestionCommonFields.js +1 -33
  96. package/dist/components/questions/components/QuestionSearchDropdown.js +1 -258
  97. package/dist/components/questions/components/QuestionTypeSelector.js +1 -32
  98. package/dist/components/questions/components/QuestionTypeSpecificForms.js +1 -6
  99. package/dist/components/questions/components/RegionNode.js +1 -94
  100. package/dist/components/questions/components/index.js +1 -14
  101. package/dist/components/questions/creator/QuestionCreator.js +1 -90
  102. package/dist/components/questions/creator/QuestionGroupCreator.js +1 -119
  103. package/dist/components/questions/creator/dedicated-component-router.js +1 -79
  104. package/dist/components/questions/creator/index.js +1 -3
  105. package/dist/components/questions/creator/question-type-registry.js +1 -80
  106. package/dist/components/questions/groups/ArticlesGroupCard.js +1 -121
  107. package/dist/components/questions/groups/BasicQuestionGroupCard.js +1 -50
  108. package/dist/components/questions/groups/DocumentGroupCard.js +1 -114
  109. package/dist/components/questions/groups/MatchAnswerGroupCard.js +1 -25
  110. package/dist/components/questions/index.js +1 -10
  111. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +1 -37
  112. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +1 -351
  113. package/dist/components/questions/types/answer-the-question/register.js +1 -67
  114. package/dist/components/questions/types/answer-the-question/transform.js +1 -36
  115. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +1 -141
  116. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +1 -118
  117. package/dist/components/questions/types/arrange-letters-into-words/index.js +1 -2
  118. package/dist/components/questions/types/arrange-letters-into-words/register.js +1 -4
  119. package/dist/components/questions/types/arrange-letters-into-words/transform.js +1 -24
  120. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +1 -64
  121. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +1 -169
  122. package/dist/components/questions/types/choose-correct-adjective/index.js +1 -2
  123. package/dist/components/questions/types/choose-correct-adjective/register.js +1 -55
  124. package/dist/components/questions/types/choose-correct-adjective/transform.js +1 -16
  125. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -131
  126. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -581
  127. package/dist/components/questions/types/choose-the-correct-answer/index.js +1 -2
  128. package/dist/components/questions/types/choose-the-correct-answer/register.js +1 -101
  129. package/dist/components/questions/types/choose-the-correct-answer/transform.js +1 -43
  130. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -275
  131. package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +1 -448
  132. package/dist/components/questions/types/fill-in-blank/index.js +1 -2
  133. package/dist/components/questions/types/fill-in-blank/register.js +1 -138
  134. package/dist/components/questions/types/fill-in-blank/transform.js +1 -59
  135. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +1 -117
  136. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +1 -671
  137. package/dist/components/questions/types/fill-missing-words-in-grid/index.js +1 -2
  138. package/dist/components/questions/types/fill-missing-words-in-grid/register.js +1 -37
  139. package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +1 -15
  140. package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +1 -262
  141. package/dist/components/questions/types/gn-speaking-interview/register.js +1 -121
  142. package/dist/components/questions/types/gn-speaking-interview/transform.js +1 -48
  143. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +1 -16
  144. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +1 -27
  145. package/dist/components/questions/types/image-object-matching/index.js +1 -2
  146. package/dist/components/questions/types/image-object-matching/register.js +1 -3
  147. package/dist/components/questions/types/image-object-matching/transform.js +1 -2
  148. package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +1 -102
  149. package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +1 -344
  150. package/dist/components/questions/types/label-the-picture/index.js +1 -2
  151. package/dist/components/questions/types/label-the-picture/register.js +1 -54
  152. package/dist/components/questions/types/label-the-picture/transform.js +1 -28
  153. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +1 -78
  154. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +1 -232
  155. package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +1 -2
  156. package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +1 -82
  157. package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +1 -27
  158. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +1 -70
  159. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +1 -386
  160. package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +1 -2
  161. package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +1 -100
  162. package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +1 -33
  163. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +1 -106
  164. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +1 -457
  165. package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +1 -2
  166. package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +1 -110
  167. package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +1 -35
  168. package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +1 -8
  169. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +1 -22
  170. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +1 -291
  171. package/dist/components/questions/types/listen-and-speak-answer/index.js +1 -2
  172. package/dist/components/questions/types/listen-and-speak-answer/register.js +1 -84
  173. package/dist/components/questions/types/listen-and-speak-answer/transform.js +1 -27
  174. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +1 -32
  175. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +1 -295
  176. package/dist/components/questions/types/listen-and-speak-compare-images/register.js +1 -79
  177. package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +1 -27
  178. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +1 -34
  179. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +1 -285
  180. package/dist/components/questions/types/listen-and-speak-image-group/register.js +1 -81
  181. package/dist/components/questions/types/listen-and-speak-image-group/transform.js +1 -26
  182. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +1 -38
  183. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +1 -385
  184. package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +1 -87
  185. package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +1 -32
  186. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +1 -26
  187. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +1 -247
  188. package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +1 -59
  189. package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +1 -18
  190. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +1 -26
  191. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +1 -256
  192. package/dist/components/questions/types/listen-and-speak-question-list/register.js +1 -81
  193. package/dist/components/questions/types/listen-and-speak-question-list/transform.js +1 -26
  194. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +1 -26
  195. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +1 -230
  196. package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +1 -56
  197. package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +1 -16
  198. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +1 -72
  199. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +1 -230
  200. package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
  201. package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +1 -66
  202. package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +1 -42
  203. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +1 -96
  204. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +1 -238
  205. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +1 -2
  206. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +1 -76
  207. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +1 -19
  208. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +1 -134
  209. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +1 -617
  210. package/dist/components/questions/types/look-picture-fill-word-hint/index.js +1 -2
  211. package/dist/components/questions/types/look-picture-fill-word-hint/register.js +1 -82
  212. package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +1 -29
  213. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +1 -38
  214. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +1 -359
  215. package/dist/components/questions/types/match-by-writing-answer/index.js +1 -3
  216. package/dist/components/questions/types/match-by-writing-answer/register.js +1 -58
  217. package/dist/components/questions/types/match-by-writing-answer/transform.js +1 -61
  218. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +1 -16
  219. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +1 -23
  220. package/dist/components/questions/types/read-and-color-objects/index.js +1 -2
  221. package/dist/components/questions/types/read-and-color-objects/register.js +1 -3
  222. package/dist/components/questions/types/read-and-color-objects/transform.js +1 -36
  223. package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +1 -125
  224. package/dist/components/questions/types/read-displayed-content/register.js +1 -64
  225. package/dist/components/questions/types/read-displayed-content/transform.js +1 -12
  226. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +1 -64
  227. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +1 -511
  228. package/dist/components/questions/types/read-passage-and-answer-questions/index.js +1 -2
  229. package/dist/components/questions/types/read-passage-and-answer-questions/register.js +1 -58
  230. package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +1 -28
  231. package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +1 -373
  232. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -41
  233. package/dist/components/questions/types/speaking-conversation/register.js +1 -110
  234. package/dist/components/questions/types/speaking-conversation/transform.js +1 -34
  235. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -74
  236. package/dist/components/questions/types/speaking-cue-card/register.js +1 -52
  237. package/dist/components/questions/types/speaking-cue-card/transform.js +1 -12
  238. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +1 -10
  239. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +1 -161
  240. package/dist/components/questions/types/speaking-describe-image/index.js +1 -2
  241. package/dist/components/questions/types/speaking-describe-image/register.js +1 -69
  242. package/dist/components/questions/types/speaking-describe-image/transform.js +1 -16
  243. package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +1 -234
  244. package/dist/components/questions/types/spontaneous-qa/register.js +1 -93
  245. package/dist/components/questions/types/spontaneous-qa/transform.js +1 -34
  246. package/dist/components/questions/types/true-false/TrueFalseClient.js +1 -75
  247. package/dist/components/questions/types/true-false/TrueFalseCreator.js +1 -244
  248. package/dist/components/questions/types/true-false/index.js +1 -2
  249. package/dist/components/questions/types/true-false/register.js +1 -77
  250. package/dist/components/questions/types/true-false/transform.js +1 -32
  251. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +1 -233
  252. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +1 -631
  253. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +1 -28
  254. package/dist/components/questions/types/word-fill-paragraph/index.js +1 -2
  255. package/dist/components/questions/types/word-fill-paragraph/register.js +1 -136
  256. package/dist/components/questions/types/word-fill-paragraph/transform.js +1 -56
  257. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +1 -138
  258. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +1 -261
  259. package/dist/components/questions/types/word-fill-structured-form/index.js +1 -2
  260. package/dist/components/questions/types/word-fill-structured-form/register.js +1 -67
  261. package/dist/components/questions/types/word-fill-structured-form/transform.js +1 -40
  262. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +1 -84
  263. package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +1 -192
  264. package/dist/components/questions/types/word-ordering/index.js +1 -2
  265. package/dist/components/questions/types/word-ordering/register.js +1 -46
  266. package/dist/components/questions/types/word-ordering/transform.js +1 -24
  267. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +1 -98
  268. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +1 -320
  269. package/dist/components/questions/types/write-a-short-letter/index.js +1 -2
  270. package/dist/components/questions/types/write-a-short-letter/register.js +1 -68
  271. package/dist/components/questions/types/write-a-short-letter/transform.js +1 -25
  272. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +1 -61
  273. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +1 -196
  274. package/dist/components/questions/types/write-correct-verb-form/index.js +1 -2
  275. package/dist/components/questions/types/write-correct-verb-form/register.js +1 -62
  276. package/dist/components/questions/types/write-correct-verb-form/transform.js +1 -17
  277. package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +1 -22
  278. package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +1 -174
  279. package/dist/components/questions/types/write-sentences/index.js +1 -2
  280. package/dist/components/questions/types/write-sentences/register.js +1 -58
  281. package/dist/components/questions/types/write-sentences/transform.js +1 -28
  282. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +1 -96
  283. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +1 -161
  284. package/dist/components/questions/types/write-short-paragraph/index.js +1 -2
  285. package/dist/components/questions/types/write-short-paragraph/register.js +1 -4
  286. package/dist/components/questions/types/write-short-paragraph/transform.js +1 -21
  287. package/dist/components/questions/viewer/QuestionGroupViewer.js +1 -36
  288. package/dist/components/questions/viewer/QuestionViewer.js +1 -446
  289. package/dist/components/questions/viewer/index.js +1 -2
  290. package/dist/components/results/ResultReviewPageContainer.js +1 -98
  291. package/dist/components/results/ReviewQuestionRenderer.js +1 -72
  292. package/dist/components/results/index.js +1 -2
  293. package/dist/components/results/result-review.utils.js +1 -6
  294. package/dist/components/results/review-data.js +1 -150
  295. package/dist/components/results/review-renderer.utils.js +1 -97
  296. package/dist/components/results/review-types.js +1 -1
  297. package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +1 -138
  298. package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +1 -12
  299. package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +1 -18
  300. package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +1 -11
  301. package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +1 -11
  302. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +1 -9
  303. package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +1 -13
  304. package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +1 -8
  305. package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +1 -6
  306. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +1 -5
  307. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +1 -9
  308. package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +1 -41
  309. package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -25
  310. package/dist/components/themes/cambridge-yle/index.js +1 -13
  311. package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +1 -24
  312. package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +1 -88
  313. package/dist/components/themes/english-certification/EcWordFillParagraph.js +1 -55
  314. package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +1 -156
  315. package/dist/components/themes/english-certification/EcWritingTask.js +1 -34
  316. package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -114
  317. package/dist/components/themes/english-certification/EnglishCertificationBrand.js +1 -11
  318. package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +1 -21
  319. package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +1 -15
  320. package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +1 -19
  321. package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +1 -148
  322. package/dist/components/themes/english-certification/EnglishCertificationHeader.js +1 -31
  323. package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +1 -71
  324. package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +1 -33
  325. package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +1 -16
  326. package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +1 -22
  327. package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +1 -37
  328. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -72
  329. package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +1 -90
  330. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +1 -18
  331. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +1 -29
  332. package/dist/components/themes/english-certification/english-certification-theme.js +1 -58
  333. package/dist/components/themes/english-certification/grouped-question-utils.js +1 -218
  334. package/dist/components/themes/english-certification/index.js +1 -21
  335. package/dist/components/themes/index.js +1 -6
  336. package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +1 -170
  337. package/dist/components/themes/summer-sky/SummerSkyLayout.js +1 -221
  338. package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +1 -18
  339. package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +1 -194
  340. package/dist/components/themes/types.js +1 -1
  341. package/dist/components/ui/alert-dialog.js +1 -26
  342. package/dist/components/ui/alert.js +1 -22
  343. package/dist/components/ui/avatar.js +1 -12
  344. package/dist/components/ui/badge.js +1 -38
  345. package/dist/components/ui/button.js +1 -35
  346. package/dist/components/ui/calendar.js +1 -76
  347. package/dist/components/ui/card.js +1 -16
  348. package/dist/components/ui/carousel.js +1 -91
  349. package/dist/components/ui/checkbox.js +1 -9
  350. package/dist/components/ui/collapsible.js +1 -13
  351. package/dist/components/ui/command.js +1 -34
  352. package/dist/components/ui/confirm-dialog.js +1 -49
  353. package/dist/components/ui/dialog.js +1 -23
  354. package/dist/components/ui/drawer.js +1 -35
  355. package/dist/components/ui/dropdown-menu.js +1 -33
  356. package/dist/components/ui/file-upload.js +1 -155
  357. package/dist/components/ui/form.js +1 -62
  358. package/dist/components/ui/hover-card.js +1 -10
  359. package/dist/components/ui/image-preview.js +1 -29
  360. package/dist/components/ui/input.js +1 -8
  361. package/dist/components/ui/label.js +1 -9
  362. package/dist/components/ui/language-switcher.js +1 -15
  363. package/dist/components/ui/multi-select.js +1 -90
  364. package/dist/components/ui/points-input.js +1 -33
  365. package/dist/components/ui/popover.js +1 -26
  366. package/dist/components/ui/progress.js +1 -16
  367. package/dist/components/ui/radio-group.js +1 -12
  368. package/dist/components/ui/scroll-area.js +1 -11
  369. package/dist/components/ui/select.js +1 -38
  370. package/dist/components/ui/separator.js +1 -8
  371. package/dist/components/ui/sheet.js +1 -40
  372. package/dist/components/ui/single-select.js +1 -76
  373. package/dist/components/ui/skeleton.js +1 -6
  374. package/dist/components/ui/switch.js +1 -8
  375. package/dist/components/ui/table.js +1 -20
  376. package/dist/components/ui/tabs.js +1 -17
  377. package/dist/components/ui/textarea.js +1 -8
  378. package/dist/components/ui/tooltip.js +1 -11
  379. package/dist/index.js +1 -19
  380. package/dist/mcp/config.js +1 -32
  381. package/dist/mcp/http-client.js +1 -48
  382. package/dist/mcp/index.js +1 -29
  383. package/dist/mcp/tools/_helpers.js +1 -25
  384. package/dist/mcp/tools/exams.js +1 -439
  385. package/dist/mcp/tools/index.js +1 -6
  386. package/dist/mcp/tools/results.js +1 -19
  387. package/dist/shared/constants/ApiConstant.js +1 -276
  388. package/dist/shared/constants/QueryKeyConstant.js +1 -112
  389. package/dist/shared/constants/exam-level.enum.js +1 -41
  390. package/dist/shared/constants/landings/placement-test.constants.js +1 -431
  391. package/dist/shared/constants/question-skills.js +1 -267
  392. package/dist/shared/constants/routes.js +1 -81
  393. package/dist/shared/index.js +1 -7
  394. package/dist/shared/lib/hooks/index.js +1 -10
  395. package/dist/shared/lib/hooks/useAlertDialog.js +1 -57
  396. package/dist/shared/lib/hooks/useAudioPlayCounter.js +1 -30
  397. package/dist/shared/lib/hooks/useAudioPreview.js +1 -81
  398. package/dist/shared/lib/hooks/useAudioUpload.js +1 -24
  399. package/dist/shared/lib/hooks/useConfirmDialog.js +1 -89
  400. package/dist/shared/lib/hooks/useDebouncedCallback.js +1 -32
  401. package/dist/shared/lib/hooks/useDelayedAutoPlay.js +1 -12
  402. package/dist/shared/lib/hooks/useFileUpload.js +1 -24
  403. package/dist/shared/lib/hooks/useImageUrl.js +1 -5
  404. package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -10
  405. package/dist/shared/lib/hooks/useReactHookForm.js +1 -31
  406. package/dist/shared/lib/hooks/useToast.js +1 -62
  407. package/dist/shared/lib/i18n/index.js +1 -54
  408. package/dist/shared/lib/i18n/messages/en/admin.js +1 -2104
  409. package/dist/shared/lib/i18n/messages/en/auth.js +1 -86
  410. package/dist/shared/lib/i18n/messages/en/common.js +1 -140
  411. package/dist/shared/lib/i18n/messages/en/file-upload.js +1 -18
  412. package/dist/shared/lib/i18n/messages/en/student.js +1 -133
  413. package/dist/shared/lib/i18n/messages/en/terms.js +1 -37
  414. package/dist/shared/lib/i18n/messages/en.js +1 -15
  415. package/dist/shared/lib/i18n/messages/vi/admin.js +1 -2104
  416. package/dist/shared/lib/i18n/messages/vi/auth.js +1 -86
  417. package/dist/shared/lib/i18n/messages/vi/common.js +1 -140
  418. package/dist/shared/lib/i18n/messages/vi/file-upload.js +1 -18
  419. package/dist/shared/lib/i18n/messages/vi/student.js +1 -133
  420. package/dist/shared/lib/i18n/messages/vi/terms.js +1 -37
  421. package/dist/shared/lib/i18n/messages/vi.js +1 -15
  422. package/dist/shared/lib/i18n/types.js +1 -1
  423. package/dist/shared/lib/stores/examQuestionStore.js +1 -212
  424. package/dist/shared/lib/stores/localeStore.js +1 -10
  425. package/dist/shared/lib/utils/public-page.js +1 -44
  426. package/dist/shared/lib/utils/question-transform-types.js +1 -1
  427. package/dist/shared/lib/utils/question-transform.js +1 -174
  428. package/dist/shared/lib/utils.js +1 -168
  429. package/dist/shared/lib/validation/choose-correct-answer.validation.js +1 -111
  430. package/dist/shared/lib/validation/label-the-picture.validation.js +1 -19
  431. package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +1 -46
  432. package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +1 -21
  433. package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +1 -34
  434. package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +1 -40
  435. package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +1 -45
  436. package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +1 -19
  437. package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +1 -39
  438. package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +1 -32
  439. package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +1 -19
  440. package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +1 -28
  441. package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +1 -12
  442. package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +1 -47
  443. package/dist/shared/lib/validation/read-and-color-objects.validation.js +1 -29
  444. package/dist/shared/lib/validation/read-displayed-content.validation.js +1 -28
  445. package/dist/shared/lib/validation/speaking-conversation.validation.js +1 -75
  446. package/dist/shared/lib/validation/speaking-describe-image.validation.js +1 -31
  447. package/dist/shared/lib/validation/spontaneous-qa.validation.js +1 -48
  448. package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +1 -38
  449. package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +1 -39
  450. package/dist/shared/lib/validation/word-fill-paragraph.validation.js +1 -43
  451. package/dist/shared/lib/validation/word-ordering.validation.js +1 -33
  452. package/dist/shared/types/branch.types.js +1 -6
  453. package/dist/shared/types/common.types.js +1 -50
  454. package/dist/shared/types/entities/exam-schedule.types.js +1 -5
  455. package/dist/shared/types/entities/exam.types.js +1 -1
  456. package/dist/shared/types/entities/question.types.js +1 -4
  457. package/dist/shared/types/entities/result.types.js +1 -4
  458. package/dist/shared/types/entities/user.types.js +1 -4
  459. package/dist/shared/types/exam-taking.types.js +1 -2
  460. package/dist/shared/types/history.types.js +1 -34
  461. package/dist/shared/types/index.js +1 -1
  462. package/dist/shared/types/questions/choose-correct-adjective.js +1 -4
  463. package/dist/shared/types/questions/choose-the-correct-answer.js +1 -9
  464. package/dist/shared/types/questions/fill-missing-words-in-grid.js +1 -11
  465. package/dist/shared/types/questions/index.js +1 -40
  466. package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
  467. package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
  468. package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
  469. package/dist/shared/types/questions/listen-and-speak-info-exchange.js +1 -3
  470. package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +1 -3
  471. package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
  472. package/dist/shared/types/questions/listen-and-speak-with-story-images.js +1 -3
  473. package/dist/shared/types/questions/look-picture-choose-correct-answer.js +1 -5
  474. package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +1 -9
  475. package/dist/shared/types/questions/look-picture-fill-word-hint.js +1 -7
  476. package/dist/shared/types/questions/read-displayed-content.js +1 -1
  477. package/dist/shared/types/questions/speaking-conversation.js +1 -7
  478. package/dist/shared/types/questions/speaking-describe-image.js +1 -1
  479. package/dist/shared/types/questions/spontaneous-qa.js +1 -1
  480. package/dist/shared/types/questions/write-correct-verb-form.js +1 -12
  481. package/dist/shared/types/role.types.js +1 -1
  482. package/dist/shared/types/structure-management.js +1 -28
  483. package/dist/shared/types/student.types.js +1 -54
  484. package/dist/shared/types/teacher-dashboard-rooms.js +1 -2
  485. package/package.json +4 -2
@@ -1,420 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useState, useRef, useCallback, useEffect } from 'react';
4
- import { Button } from '../../../components/ui/button';
5
- import { Label } from '../../../components/ui/label';
6
- import { Card, CardContent, CardHeader, CardTitle } from '../../../components/ui/card';
7
- import { PointsInput } from '../../../components/ui/points-input';
8
- import { X, Pencil, Eye, Plus, Trash2, MessageSquare, Palette, Lightbulb, Image } from 'lucide-react';
9
- import { Input } from '../../../components/ui/input';
10
- import { Textarea } from '../../../components/ui/textarea';
11
- import { FileUpload } from '../../../components/ui/file-upload';
12
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../../components/ui/select';
13
- import { ColorRegionNode } from './ColorRegionNode';
14
- import { usePresignedFileUrl } from '../../../shared/lib/hooks';
15
- import { COLOR_PALETTE } from '../_shared/constants/color-palette';
16
- import { useReactHookForm } from '../../../shared/lib/hooks/useReactHookForm';
17
- import { readAndColorObjectsFormSchema, } from '../../../shared/lib/validation/read-and-color-objects.validation';
18
- function ColorRegionCreatorContent({ initialData, onSave, onCancel, onChange, onUnsavedChangesChange, validationRef, config, ClientComponent, }) {
19
- // State
20
- const [backgroundImageUrl, setBackgroundImageUrl] = useState(initialData?.backgroundImageUrl || '');
21
- const [colorNodes, setColorNodes] = useState(initialData?.colorNodes || []);
22
- const [regionNodes, setRegionNodes] = useState(initialData?.regionNodes || []);
23
- // Instructions state
24
- const [instructions, setInstructions] = useState(Array.isArray(initialData?.instructions) ? initialData.instructions : []);
25
- // Add node state - selected color from palette
26
- const [selectedColor, setSelectedColor] = useState('');
27
- const [colorLabelValue, setColorLabelValue] = useState('');
28
- const [showAddColorPanel, setShowAddColorPanel] = useState(false);
29
- // Whether to show label input alongside color picker
30
- const showColorLabel = config.showColorLabel || false;
31
- // Points state
32
- const [points, setPoints] = useState(initialData?.points || 1);
33
- // Explanation state
34
- const [explanation, setExplanation] = useState(initialData?.explanation || '');
35
- // Use react-hook-form with Zod validation
36
- const form = useReactHookForm({
37
- defaultValues: {
38
- backgroundImageUrl: backgroundImageUrl,
39
- colorNodes: colorNodes,
40
- regionNodes: regionNodes,
41
- instructions: instructions,
42
- },
43
- zodSchema: readAndColorObjectsFormSchema,
44
- mode: 'onBlur',
45
- });
46
- // Track previous initialData to detect changes
47
- const previousInitialDataRef = useRef(initialData);
48
- const isInitializingRef = useRef(true); // Start as true to prevent onChange on first render
49
- const hasInitializedRef = useRef(false); // Track if we've initialized from initialData
50
- // NOTE: Status tracking is now handled entirely by ExamCreator via localStorage (exam_question_status_{id})
51
- // The onChange callback triggers handleQuestionDraftChange in ExamCreator which updates localStorage
52
- // Expose validation function via ref
53
- useEffect(() => {
54
- if (validationRef) {
55
- validationRef.current = {
56
- triggerValidation: async () => {
57
- return validateForm();
58
- },
59
- // Return current form data immediately (not debounced)
60
- getFormData: () => ({
61
- backgroundImageUrl,
62
- colorNodes,
63
- regionNodes,
64
- instructions,
65
- points,
66
- explanation,
67
- }),
68
- };
69
- }
70
- }, [validationRef, backgroundImageUrl, colorNodes, regionNodes, instructions, points, explanation]);
71
- // Use custom hooks for presigned URLs
72
- const { previewUrl: backgroundImagePreviewUrl } = usePresignedFileUrl(backgroundImageUrl);
73
- const [backgroundPreviewUrl, setBackgroundPreviewUrl] = useState('');
74
- // Sync presigned background URL → backgroundPreviewUrl (matches LACOIS pattern)
75
- useEffect(() => {
76
- if (backgroundImagePreviewUrl)
77
- setBackgroundPreviewUrl(backgroundImagePreviewUrl);
78
- }, [backgroundImagePreviewUrl]);
79
- // Sync state from initialData when it changes (e.g., when switching back to this question)
80
- useEffect(() => {
81
- const initialDataChanged = JSON.stringify(previousInitialDataRef.current) !== JSON.stringify(initialData);
82
- if (initialDataChanged || !hasInitializedRef.current) {
83
- isInitializingRef.current = true;
84
- previousInitialDataRef.current = initialData;
85
- hasInitializedRef.current = true;
86
- // Update all state from initialData (handle both defined and undefined cases)
87
- if (initialData) {
88
- if (initialData.backgroundImageUrl !== undefined) {
89
- setBackgroundImageUrl(initialData.backgroundImageUrl);
90
- }
91
- if (initialData.colorNodes !== undefined) {
92
- setColorNodes(initialData.colorNodes);
93
- }
94
- if (initialData.regionNodes !== undefined) {
95
- setRegionNodes(initialData.regionNodes);
96
- }
97
- if (initialData.instructions !== undefined) {
98
- setInstructions(Array.isArray(initialData.instructions) ? initialData.instructions : []);
99
- }
100
- if (initialData.points !== undefined) {
101
- setPoints(initialData.points);
102
- }
103
- if (initialData.explanation !== undefined) {
104
- setExplanation(initialData.explanation);
105
- }
106
- }
107
- else {
108
- // Reset to empty state if initialData is undefined
109
- setBackgroundImageUrl('');
110
- setColorNodes([]);
111
- setRegionNodes([]);
112
- setInstructions([]);
113
- setPoints(1);
114
- setExplanation('');
115
- }
116
- // Reset flag after state updates
117
- setTimeout(() => {
118
- isInitializingRef.current = false;
119
- }, 150); // Increased timeout to ensure state is fully updated
120
- }
121
- }, [initialData]);
122
- // Store onChange callback in ref to avoid dependency issues
123
- const onChangeRef = useRef(onChange);
124
- useEffect(() => {
125
- onChangeRef.current = onChange;
126
- }, [onChange]);
127
- // Track previous values to prevent unnecessary onChange calls
128
- const previousPayloadRef = useRef('');
129
- useEffect(() => {
130
- // Skip onChange during initialization to prevent infinite loop
131
- if (isInitializingRef.current || !hasInitializedRef.current) {
132
- // Still update previous payload ref to track current state
133
- const currentPayload = JSON.stringify({
134
- backgroundImageUrl,
135
- colorNodes,
136
- regionNodes,
137
- instructions,
138
- explanation,
139
- });
140
- previousPayloadRef.current = currentPayload;
141
- return;
142
- }
143
- // Don't call onChange if we have no meaningful data yet
144
- // This prevents overwriting API data with empty values
145
- if (!backgroundImageUrl && colorNodes.length === 0 && regionNodes.length === 0) {
146
- return;
147
- }
148
- // Create payload
149
- const payload = {
150
- backgroundImageUrl,
151
- colorNodes,
152
- regionNodes,
153
- instructions,
154
- points,
155
- explanation,
156
- };
157
- // Serialize payload for comparison
158
- const payloadString = JSON.stringify(payload);
159
- // Only call onChange if payload actually changed
160
- if (previousPayloadRef.current !== payloadString) {
161
- previousPayloadRef.current = payloadString;
162
- // Use ref to call onChange to avoid dependency on onChange function reference
163
- onChangeRef.current?.(payload);
164
- }
165
- }, [backgroundImageUrl, colorNodes, regionNodes, instructions, points, explanation]);
166
- // UI State
167
- const [isClientMode, setIsClientMode] = useState(false);
168
- const [imageLoaded, setImageLoaded] = useState(false);
169
- // Refs
170
- const workspaceRef = useRef(null);
171
- // ── LACOIS-style custom drag state ────────────────────────────────────────
172
- const [draggingId, setDraggingId] = useState(null);
173
- const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
174
- const handleRegionMouseDown = useCallback((regionId, regionX, regionY, e) => {
175
- if (isClientMode)
176
- return;
177
- e.preventDefault();
178
- setDraggingId(regionId);
179
- if (workspaceRef.current) {
180
- const rect = workspaceRef.current.getBoundingClientRect();
181
- const mouseXPercent = ((e.clientX - rect.left) / rect.width) * 100;
182
- const mouseYPercent = ((e.clientY - rect.top) / rect.height) * 100;
183
- setDragOffset({ x: mouseXPercent - regionX, y: mouseYPercent - regionY });
184
- }
185
- }, [isClientMode]);
186
- const handleMouseMove = useCallback((e) => {
187
- if (!draggingId || !workspaceRef.current)
188
- return;
189
- const rect = workspaceRef.current.getBoundingClientRect();
190
- const mouseXPercent = ((e.clientX - rect.left) / rect.width) * 100;
191
- const mouseYPercent = ((e.clientY - rect.top) / rect.height) * 100;
192
- setRegionNodes((prev) => prev.map((region) => {
193
- if (region.id !== draggingId)
194
- return region;
195
- const x = Math.max(0, Math.min(100 - region.width, mouseXPercent - dragOffset.x));
196
- const y = Math.max(0, Math.min(100 - region.height, mouseYPercent - dragOffset.y));
197
- return { ...region, x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 };
198
- }));
199
- }, [draggingId, dragOffset]);
200
- const handleMouseUp = useCallback(() => {
201
- setDraggingId(null);
202
- }, []);
203
- useEffect(() => {
204
- if (draggingId) {
205
- window.addEventListener('mousemove', handleMouseMove);
206
- window.addEventListener('mouseup', handleMouseUp);
207
- return () => {
208
- window.removeEventListener('mousemove', handleMouseMove);
209
- window.removeEventListener('mouseup', handleMouseUp);
210
- };
211
- }
212
- }, [draggingId, handleMouseMove, handleMouseUp]);
213
- // ─────────────────────────────────────────────────────────────────────────
214
- // Remove color node and its linked region
215
- const removeColorNode = useCallback((id) => {
216
- // Find the region linked to this color node
217
- const linkedRegion = regionNodes.find((r) => r.linkedColorNodeId === id);
218
- if (linkedRegion) {
219
- setRegionNodes((prev) => prev.filter((r) => r.id !== linkedRegion.id));
220
- }
221
- setColorNodes((prev) => prev.filter((n) => n.id !== id));
222
- }, [regionNodes]);
223
- // Remove region
224
- const removeRegion = useCallback((id) => {
225
- setRegionNodes((prev) => prev.filter((r) => r.id !== id));
226
- }, []);
227
- // Add new color node with auto-generated region
228
- const addNewColorNode = useCallback(() => {
229
- if (!selectedColor) {
230
- form.setError('colorNodes', { message: 'Vui lòng chọn màu!' });
231
- return;
232
- }
233
- // Check if color already exists
234
- if (colorNodes.some((n) => n.color === selectedColor)) {
235
- form.setError('colorNodes', { message: 'Màu này đã được thêm!' });
236
- return;
237
- }
238
- if (!imageLoaded) {
239
- form.setError('backgroundImageUrl', { message: 'Vui lòng upload ảnh nền trước!' });
240
- return;
241
- }
242
- const timestamp = Date.now();
243
- // Create color node (with optional label)
244
- const newColorNode = {
245
- id: `color-${timestamp}`,
246
- color: selectedColor,
247
- ...(showColorLabel && colorLabelValue.trim() ? { label: colorLabelValue.trim() } : {}),
248
- };
249
- // Create region at random position (10-80% to avoid edges)
250
- const randomX = Math.random() * 70 + 10; // 10-80%
251
- const randomY = Math.random() * 70 + 10; // 10-80%
252
- const newRegion = {
253
- id: `region-${timestamp}`,
254
- x: randomX,
255
- y: randomY,
256
- width: 6, // Smaller default width (6% of image width)
257
- height: 5, // Smaller default height (5% of image height)
258
- color: selectedColor,
259
- linkedColorNodeId: newColorNode.id,
260
- };
261
- setColorNodes((prev) => [...prev, newColorNode]);
262
- setRegionNodes((prev) => [...prev, newRegion]);
263
- // Reset selection
264
- setSelectedColor('');
265
- setColorLabelValue('');
266
- setShowAddColorPanel(false);
267
- form.clearErrors();
268
- }, [selectedColor, colorLabelValue, showColorLabel, imageLoaded, colorNodes, form]);
269
- // Get validation errors
270
- const getErrors = () => {
271
- const errors = {};
272
- const bgError = form.formState.errors.backgroundImageUrl;
273
- if (bgError) {
274
- errors.backgroundImageUrl = bgError.message;
275
- }
276
- const colorNodesError = form.formState.errors.colorNodes;
277
- if (colorNodesError && typeof colorNodesError.message === 'string') {
278
- errors.colorNodes = colorNodesError.message;
279
- }
280
- const regionNodesError = form.formState.errors.regionNodes;
281
- if (regionNodesError && typeof regionNodesError.message === 'string') {
282
- errors.regionNodes = regionNodesError.message;
283
- }
284
- const instructionsError = form.formState.errors.instructions;
285
- if (instructionsError && typeof instructionsError.message === 'string') {
286
- errors.instructions = instructionsError.message;
287
- }
288
- // Check for individual instruction errors (array item errors)
289
- if (instructionsError && Array.isArray(instructionsError)) {
290
- errors.instructionItems = {};
291
- instructionsError.forEach((err, index) => {
292
- if (err && typeof err.message === 'string') {
293
- errors.instructionItems[index] = err.message;
294
- }
295
- });
296
- }
297
- return errors;
298
- };
299
- // Determine if instructions are required based on config (default: true)
300
- const instructionsRequired = config.instructionsRequired !== false;
301
- // Validate form data
302
- const validateForm = () => {
303
- // Update form values before validation
304
- form.setValue('backgroundImageUrl', backgroundImageUrl);
305
- form.setValue('colorNodes', colorNodes);
306
- form.setValue('regionNodes', regionNodes);
307
- form.setValue('instructions', instructions);
308
- // Trigger validation
309
- const result = readAndColorObjectsFormSchema.safeParse({
310
- backgroundImageUrl,
311
- colorNodes,
312
- regionNodes,
313
- instructions: instructionsRequired ? instructions : ['placeholder'],
314
- });
315
- if (!result.success) {
316
- // Set errors from Zod validation
317
- result.error.issues.forEach((issue) => {
318
- // Skip instructions errors when not required
319
- if (!instructionsRequired && issue.path[0] === 'instructions')
320
- return;
321
- const path = issue.path.join('.');
322
- form.setError(path, { message: issue.message });
323
- });
324
- // Check if there are any non-instructions errors
325
- const hasRealErrors = result.error.issues.some((issue) => instructionsRequired || issue.path[0] !== 'instructions');
326
- return !hasRealErrors;
327
- }
328
- return true;
329
- };
330
- // Handle save with validation
331
- const handleSave = () => {
332
- if (!validateForm()) {
333
- return;
334
- }
335
- // NOTE: Save status is now managed by ExamCreator via localStorage
336
- onSave?.({
337
- backgroundImageUrl,
338
- colorNodes,
339
- regionNodes,
340
- instructions,
341
- points,
342
- explanation,
343
- });
344
- };
345
- const errors = getErrors();
346
- // If in client mode, render the Client component
347
- if (isClientMode) {
348
- const questionData = {
349
- backgroundImageUrl: backgroundImagePreviewUrl || backgroundImageUrl,
350
- colorNodes,
351
- regionNodes,
352
- instructions,
353
- explanation,
354
- };
355
- return (_jsxs("div", { className: "space-y-6", children: [_jsx(Card, { children: _jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { children: _jsx(CardTitle, { children: config.previewTitle }) }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Quay l\u1EA1i Edit Mode"] })] }) }) }), _jsx(ClientComponent, { questionData: questionData, onSubmit: () => { }, isReviewMode: true, explanation: explanation })] }));
356
- }
357
- // Get available colors (not yet added)
358
- const availableColors = COLOR_PALETTE.filter((color) => !colorNodes.some((node) => node.color === color));
359
- return (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50", children: [_jsx("div", { className: "h-1.5 bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-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-pink-500 to-purple-600 shadow-md", children: _jsx(Palette, { className: "h-5 w-5 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-xl font-bold text-gray-900", children: config.creatorTitle }), _jsx("p", { className: "text-sm text-gray-500", children: config.creatorSubtitle })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(!isClientMode), className: "gap-2 border-indigo-200 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(Eye, { className: "h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] })] }) }), _jsxs(CardContent, { className: "space-y-6", children: [!isClientMode && (_jsx("div", { className: "rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4", children: _jsx(PointsInput, { value: points, onChange: (e) => setPoints(Number(e.target.value) || 1) }) })), !isClientMode && (_jsx("div", { className: "rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-5 shadow-sm", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100", children: _jsx(Lightbulb, { className: "h-4 w-4 text-blue-600" }) }), _jsxs("div", { className: "text-sm text-blue-900", children: [_jsx("p", { className: "font-semibold text-blue-800", children: "H\u01B0\u1EDBng d\u1EABn t\u1EA1o c\u00E2u h\u1ECFi" }), _jsx("ul", { className: "mt-2 space-y-1.5 text-blue-700", children: config.instructionsTips.map((tip, index) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), tip] }, index))) })] })] }) })), !isClientMode && (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-indigo-500" }), config.instructionsLabel, " ", instructionsRequired && _jsx("span", { className: "text-red-500", children: "*" })] }), _jsxs(Button, { onClick: () => {
360
- setInstructions([...instructions, '']);
361
- form.clearErrors('instructions');
362
- }, className: "gap-2 border-green-200 text-green-600 hover:bg-green-50 hover:text-green-700", size: "sm", variant: "outline", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm c\u00E2u l\u1EC7nh"] })] }), errors.instructions && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4", children: [_jsx("span", { className: "text-lg", children: "\u274C" }), _jsx("p", { className: "text-sm text-red-600", children: errors.instructions })] })), _jsx("div", { className: "space-y-3", children: instructions.map((instruction, index) => {
363
- const hasError = errors.instructionItems?.[index];
364
- return (_jsxs("div", { className: `group relative rounded-xl border-2 p-3 transition-all duration-200 ${instruction.trim()
365
- ? 'border-green-200 bg-gradient-to-r from-green-50 to-emerald-50'
366
- : 'border-gray-200 bg-white hover:border-gray-300'}`, children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: `flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg font-bold transition-all ${instruction.trim()
367
- ? 'bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-md'
368
- : 'bg-gray-100 text-gray-600'}`, children: index + 1 }), _jsx(Input, { value: instruction, onChange: (e) => {
369
- const newInstructions = [...instructions];
370
- newInstructions[index] = e.target.value;
371
- setInstructions(newInstructions);
372
- form.clearErrors('instructions');
373
- }, placeholder: `Ví dụ: ${config.instructionPlaceholder}`, className: `flex-1 border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${hasError ? 'border-red-400' : ''}` }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => {
374
- setInstructions(instructions.filter((_, i) => i !== index));
375
- }, className: "text-gray-400 hover:bg-red-50 hover:text-red-500", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), hasError && (_jsx("p", { className: "ml-11 mt-2 text-sm text-red-600", children: hasError }))] }, index));
376
- }) }), instructions.length === 0 && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-4", children: [_jsx("span", { className: "text-lg", children: "\u26A0\uFE0F" }), _jsx("p", { className: "text-sm text-amber-800", children: "Ch\u01B0a c\u00F3 h\u01B0\u1EDBng d\u1EABn n\u00E0o. Nh\u1EA5n \"Th\u00EAm c\u00E2u l\u1EC7nh\" \u0111\u1EC3 b\u1EAFt \u0111\u1EA7u." })] }))] })), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Image, { className: "h-4 w-4 text-indigo-500" }), "\u1EA2nh n\u1EC1n ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(FileUpload, { id: "image-upload", label: "\u1EA2nh n\u1EC1n", accept: "image/*", value: backgroundImageUrl, onChange: (key) => {
377
- setBackgroundImageUrl(key);
378
- setImageLoaded(false);
379
- form.clearErrors();
380
- }, onPresignedUrlChange: (url) => {
381
- setBackgroundPreviewUrl(url);
382
- setImageLoaded(false);
383
- }, disabled: isClientMode, maxSize: 5, placeholder: "Paste URL ho\u1EB7c upload file", required: true, autoUpload: true, prefix: "questions", onFileLoad: () => {
384
- setImageLoaded(false);
385
- } }), errors.backgroundImageUrl && (_jsx("p", { className: "mt-1 text-sm text-red-600", children: errors.backgroundImageUrl }))] }), !isClientMode && (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Palette, { className: "h-4 w-4 text-purple-500" }), config.addItemLabel || 'Thêm Màu', " ", _jsx("span", { className: "text-red-500", children: "*" })] }), !showAddColorPanel && (_jsxs(Button, { onClick: () => setShowAddColorPanel(true), className: "gap-2 border-purple-200 text-purple-600 hover:bg-purple-50 hover:text-purple-700", size: "sm", variant: "outline", disabled: availableColors.length === 0, children: [_jsx(Plus, { className: "h-4 w-4" }), config.addItemLabel || 'Thêm màu'] }))] }), errors.colorNodes && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4", children: [_jsx("span", { className: "text-lg", children: "\u274C" }), _jsx("p", { className: "text-sm text-red-600", children: errors.colorNodes })] })), showAddColorPanel && (_jsxs("div", { className: "flex flex-col gap-3 rounded-xl border-2 border-purple-200 bg-gradient-to-r from-purple-50 to-indigo-50 p-4", children: [showColorLabel && (_jsx(Input, { value: colorLabelValue, onChange: (e) => setColorLabelValue(e.target.value), placeholder: config.colorLabelPlaceholder || 'Nhập label (ví dụ: Tom, Apple...)', className: "h-9 border-gray-200 bg-white" })), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Select, { value: selectedColor, onValueChange: setSelectedColor, children: [_jsx(SelectTrigger, { className: "flex-1 border-gray-200 bg-white", children: _jsx(SelectValue, { placeholder: "Ch\u1ECDn m\u00E0u..." }) }), _jsx(SelectContent, { children: availableColors.map((color) => (_jsx(SelectItem, { value: color, children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "h-4 w-4 rounded border border-gray-300", style: { backgroundColor: color } }), _jsx("span", { className: "font-mono text-xs", children: color })] }) }, color))) })] }), _jsx(Button, { size: "sm", onClick: addNewColorNode, disabled: !selectedColor ||
386
- availableColors.length === 0 ||
387
- (showColorLabel && !colorLabelValue.trim()), className: "bg-purple-600 hover:bg-purple-700", children: "T\u1EA1o" }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => {
388
- setShowAddColorPanel(false);
389
- setSelectedColor('');
390
- setColorLabelValue('');
391
- }, children: "H\u1EE7y" })] })] })), colorNodes.length === 0 && !showAddColorPanel && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-4", children: [_jsx("span", { className: "text-lg", children: "\u26A0\uFE0F" }), _jsx("p", { className: "text-sm text-amber-800", children: config.emptyItemMessage || 'Chưa có màu nào. Nhấn "Thêm màu" để bắt đầu.' })] }))] })), errors.regionNodes && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4", children: [_jsx("span", { className: "text-lg", children: "\u274C" }), _jsx("p", { className: "text-sm text-red-600", children: errors.regionNodes })] }))] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-indigo-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-indigo-400 to-purple-500" }), _jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "Workspace \u2014 \u1EA2nh n\u1EC1n" }), _jsx("span", { className: "text-xs text-gray-500", children: "K\u00E9o th\u1EA3 region \u0111\u1EC3 \u0111\u1ECBnh v\u1ECB" })] }) }), _jsxs(CardContent, { className: "p-0", children: [colorNodes.length > 0 && (_jsx("div", { className: "border-b border-gray-100 px-4 py-3 flex flex-wrap gap-2", children: colorNodes.map((node) => (_jsxs("div", { className: "inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-900", children: [_jsx("div", { className: "h-4 w-4 rounded border border-gray-300", style: { backgroundColor: node.color } }), node.label ? (_jsx("span", { className: "text-xs", children: node.label })) : (_jsx("span", { className: "font-mono text-xs", children: node.color })), !isClientMode && (_jsx("button", { onClick: () => removeColorNode(node.id), className: "group rounded p-0.5 transition-all hover:scale-110 hover:bg-red-100", type: "button", children: _jsx(X, { className: "h-3 w-3 text-gray-500 transition-colors group-hover:text-red-600" }) }))] }, node.id))) })), _jsx("div", { ref: workspaceRef, className: "relative w-full overflow-hidden select-none isolate", style: { cursor: draggingId ? 'grabbing' : 'default' }, children: backgroundPreviewUrl ? (_jsxs(_Fragment, { children: [_jsx("img", { src: backgroundPreviewUrl, alt: "Background", className: "block w-full h-auto", draggable: false, onLoad: () => setImageLoaded(true), onError: () => {
392
- form.setError('backgroundImageUrl', { message: 'Không thể load ảnh!' });
393
- setImageLoaded(false);
394
- } }), imageLoaded &&
395
- regionNodes.map((region) => {
396
- const linkedColorNode = colorNodes.find((n) => n.id === region.linkedColorNodeId);
397
- return (_jsx("div", { style: {
398
- position: 'absolute',
399
- left: `${region.x}%`,
400
- top: `${region.y}%`,
401
- width: `${region.width}%`,
402
- height: `${region.height}%`,
403
- cursor: isClientMode ? 'default' : 'grab',
404
- zIndex: draggingId === region.id ? 2 : 1,
405
- }, onMouseDown: (e) => handleRegionMouseDown(region.id, region.x, region.y, e), children: _jsx(ColorRegionNode, { data: {
406
- color: region.color,
407
- x: region.x,
408
- y: region.y,
409
- width: region.width,
410
- height: region.height,
411
- onRemove: () => removeRegion(region.id),
412
- isEditMode: !isClientMode,
413
- isClientMode: isClientMode,
414
- label: linkedColorNode?.label || '',
415
- } }) }, region.id));
416
- })] })) : (_jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-20 text-gray-400", children: [_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", className: "h-10 w-10 text-gray-300", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" }) }), _jsx("p", { className: "text-sm", children: "Ch\u01B0a c\u00F3 \u1EA3nh n\u1EC1n. H\u00E3y upload \u1EA3nh \u1EDF tr\u00EAn." })] })) })] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-gray-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-gray-300 to-gray-400" }), _jsx(CardHeader, { className: "pb-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-gray-100", children: _jsx(MessageSquare, { className: "h-3.5 w-3.5 text-gray-600" }) }), _jsx(CardTitle, { className: "text-lg font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" })] }) }), _jsx(CardContent, { children: _jsx(Textarea, { id: "explanation", value: explanation, onChange: (e) => setExplanation(e.target.value), placeholder: "Nh\u1EADp gi\u1EA3i th\u00EDch cho \u0111\u00E1p \u00E1n \u0111\u00FAng...", rows: 3, className: "border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200" }) })] })] }));
417
- }
418
- export function ColorRegionCreator(props) {
419
- return _jsx(ColorRegionCreatorContent, { ...props });
420
- }
1
+ "use client";import{jsx as e,jsxs as r,Fragment as t}from"react/jsx-runtime";import{useState as o,useRef as a,useCallback as n,useEffect as s}from"react";import{Button as i}from"../../../components/ui/button";import{Label as l}from"../../../components/ui/label";import{Card as d,CardContent as c,CardHeader as m,CardTitle as g}from"../../../components/ui/card";import{PointsInput as h}from"../../../components/ui/points-input";import{X as u,Pencil as p,Eye as b,Plus as x,Trash2 as f,MessageSquare as N,Palette as v,Lightbulb as y,Image as w}from"lucide-react";import{Input as k}from"../../../components/ui/input";import{Textarea as C}from"../../../components/ui/textarea";import{FileUpload as I}from"../../../components/ui/file-upload";import{Select as U,SelectContent as E,SelectItem as M,SelectTrigger as L,SelectValue as j}from"../../../components/ui/select";import{ColorRegionNode as S}from"./ColorRegionNode";import{usePresignedFileUrl as z}from"../../../shared/lib/hooks";import{COLOR_PALETTE as T}from"../_shared/constants/color-palette";import{useReactHookForm as V}from"../../../shared/lib/hooks/useReactHookForm";import{readAndColorObjectsFormSchema as R}from"../../../shared/lib/validation/read-and-color-objects.validation";function A({initialData:A,onSave:$,onCancel:D,onChange:B,onUnsavedChangesChange:H,validationRef:P,config:q,ClientComponent:F}){const[J,O]=o(A?.backgroundImageUrl||""),[X,K]=o(A?.colorNodes||[]),[W,Y]=o(A?.regionNodes||[]),[G,Q]=o(Array.isArray(A?.instructions)?A.instructions:[]),[_,Z]=o(""),[ee,re]=o(""),[te,oe]=o(!1),ae=q.showColorLabel||!1,[ne,se]=o(A?.points||1),[ie,le]=o(A?.explanation||""),de=V({defaultValues:{backgroundImageUrl:J,colorNodes:X,regionNodes:W,instructions:G},zodSchema:R,mode:"onBlur"}),ce=a(A),me=a(!0),ge=a(!1);s(()=>{P&&(P.current={triggerValidation:async()=>Ve(),getFormData:()=>({backgroundImageUrl:J,colorNodes:X,regionNodes:W,instructions:G,points:ne,explanation:ie})})},[P,J,X,W,G,ne,ie]);const{previewUrl:he}=z(J),[ue,pe]=o("");s(()=>{he&&pe(he)},[he]),s(()=>{!(JSON.stringify(ce.current)!==JSON.stringify(A))&&ge.current||(me.current=!0,ce.current=A,ge.current=!0,A?(void 0!==A.backgroundImageUrl&&O(A.backgroundImageUrl),void 0!==A.colorNodes&&K(A.colorNodes),void 0!==A.regionNodes&&Y(A.regionNodes),void 0!==A.instructions&&Q(Array.isArray(A.instructions)?A.instructions:[]),void 0!==A.points&&se(A.points),void 0!==A.explanation&&le(A.explanation)):(O(""),K([]),Y([]),Q([]),se(1),le("")),setTimeout(()=>{me.current=!1},150))},[A]);const be=a(B);s(()=>{be.current=B},[B]);const xe=a("");s(()=>{if(me.current||!ge.current){const e=JSON.stringify({backgroundImageUrl:J,colorNodes:X,regionNodes:W,instructions:G,explanation:ie});return void(xe.current=e)}if(!J&&0===X.length&&0===W.length)return;const e={backgroundImageUrl:J,colorNodes:X,regionNodes:W,instructions:G,points:ne,explanation:ie},r=JSON.stringify(e);xe.current!==r&&(xe.current=r,be.current?.(e))},[J,X,W,G,ne,ie]);const[fe,Ne]=o(!1),[ve,ye]=o(!1),we=a(null),[ke,Ce]=o(null),[Ie,Ue]=o({x:0,y:0}),Ee=n((e,r,t,o)=>{if(!fe&&(o.preventDefault(),Ce(e),we.current)){const e=we.current.getBoundingClientRect(),a=(o.clientX-e.left)/e.width*100,n=(o.clientY-e.top)/e.height*100;Ue({x:a-r,y:n-t})}},[fe]),Me=n(e=>{if(!ke||!we.current)return;const r=we.current.getBoundingClientRect(),t=(e.clientX-r.left)/r.width*100,o=(e.clientY-r.top)/r.height*100;Y(e=>e.map(e=>{if(e.id!==ke)return e;const r=Math.max(0,Math.min(100-e.width,t-Ie.x)),a=Math.max(0,Math.min(100-e.height,o-Ie.y));return{...e,x:Math.round(10*r)/10,y:Math.round(10*a)/10}}))},[ke,Ie]),Le=n(()=>{Ce(null)},[]);s(()=>{if(ke)return window.addEventListener("mousemove",Me),window.addEventListener("mouseup",Le),()=>{window.removeEventListener("mousemove",Me),window.removeEventListener("mouseup",Le)}},[ke,Me,Le]);const je=n(e=>{const r=W.find(r=>r.linkedColorNodeId===e);r&&Y(e=>e.filter(e=>e.id!==r.id)),K(r=>r.filter(r=>r.id!==e))},[W]),Se=n(e=>{Y(r=>r.filter(r=>r.id!==e))},[]),ze=n(()=>{if(!_)return void de.setError("colorNodes",{message:"Vui lòng chọn màu!"});if(X.some(e=>e.color===_))return void de.setError("colorNodes",{message:"Màu này đã được thêm!"});if(!ve)return void de.setError("backgroundImageUrl",{message:"Vui lòng upload ảnh nền trước!"});const e=Date.now(),r={id:`color-${e}`,color:_,...ae&&ee.trim()?{label:ee.trim()}:{}},t={id:`region-${e}`,x:70*Math.random()+10,y:70*Math.random()+10,width:6,height:5,color:_,linkedColorNodeId:r.id};K(e=>[...e,r]),Y(e=>[...e,t]),Z(""),re(""),oe(!1),de.clearErrors()},[_,ee,ae,ve,X,de]),Te=!1!==q.instructionsRequired,Ve=()=>{de.setValue("backgroundImageUrl",J),de.setValue("colorNodes",X),de.setValue("regionNodes",W),de.setValue("instructions",G);const e=R.safeParse({backgroundImageUrl:J,colorNodes:X,regionNodes:W,instructions:Te?G:["placeholder"]});if(!e.success){e.error.issues.forEach(e=>{if(!Te&&"instructions"===e.path[0])return;const r=e.path.join(".");de.setError(r,{message:e.message})});return!e.error.issues.some(e=>Te||"instructions"!==e.path[0])}return!0},Re=(()=>{const e={},r=de.formState.errors.backgroundImageUrl;r&&(e.backgroundImageUrl=r.message);const t=de.formState.errors.colorNodes;t&&"string"==typeof t.message&&(e.colorNodes=t.message);const o=de.formState.errors.regionNodes;o&&"string"==typeof o.message&&(e.regionNodes=o.message);const a=de.formState.errors.instructions;return a&&"string"==typeof a.message&&(e.instructions=a.message),a&&Array.isArray(a)&&(e.instructionItems={},a.forEach((r,t)=>{r&&"string"==typeof r.message&&(e.instructionItems[t]=r.message)})),e})();if(fe){const t={backgroundImageUrl:he||J,colorNodes:X,regionNodes:W,instructions:G,explanation:ie};return r("div",{className:"space-y-6",children:[e(d,{children:e(m,{children:r("div",{className:"flex items-center justify-between",children:[e("div",{children:e(g,{children:q.previewTitle})}),r(i,{variant:"outline",size:"sm",onClick:()=>Ne(!1),children:[e(p,{className:"mr-2 h-4 w-4"}),"Quay lại Edit Mode"]})]})})}),e(F,{questionData:t,onSubmit:()=>{},isReviewMode:!0,explanation:ie})]})}const Ae=T.filter(e=>!X.some(r=>r.color===e));return r("div",{className:"space-y-6",children:[r(d,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50",children:[e("div",{className:"h-1.5 bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-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-pink-500 to-purple-600 shadow-md",children:e(v,{className:"h-5 w-5 text-white"})}),r("div",{children:[e(g,{className:"text-xl font-bold text-gray-900",children:q.creatorTitle}),e("p",{className:"text-sm text-gray-500",children:q.creatorSubtitle})]})]}),r(i,{variant:"outline",size:"sm",onClick:()=>Ne(!fe),className:"gap-2 border-indigo-200 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700",children:[e(b,{className:"h-4 w-4"}),"Xem trước"]})]})}),r(c,{className:"space-y-6",children:[!fe&&e("div",{className:"rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4",children:e(h,{value:ne,onChange:e=>se(Number(e.target.value)||1)})}),!fe&&e("div",{className:"rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-5 shadow-sm",children:r("div",{className:"flex items-start gap-3",children:[e("div",{className:"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100",children:e(y,{className:"h-4 w-4 text-blue-600"})}),r("div",{className:"text-sm text-blue-900",children:[e("p",{className:"font-semibold text-blue-800",children:"Hướng dẫn tạo câu hỏi"}),e("ul",{className:"mt-2 space-y-1.5 text-blue-700",children:q.instructionsTips.map((t,o)=>r("li",{className:"flex items-center gap-2",children:[e("span",{className:"h-1.5 w-1.5 rounded-full bg-blue-400"}),t]},o))})]})]})}),!fe&&r("div",{className:"space-y-3",children:[r("div",{className:"flex items-center justify-between",children:[r(l,{className:"flex items-center gap-2 text-base font-semibold text-gray-800",children:[e(N,{className:"h-4 w-4 text-indigo-500"}),q.instructionsLabel," ",Te&&e("span",{className:"text-red-500",children:"*"})]}),r(i,{onClick:()=>{Q([...G,""]),de.clearErrors("instructions")},className:"gap-2 border-green-200 text-green-600 hover:bg-green-50 hover:text-green-700",size:"sm",variant:"outline",children:[e(x,{className:"h-4 w-4"}),"Thêm câu lệnh"]})]}),Re.instructions&&r("div",{className:"flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4",children:[e("span",{className:"text-lg",children:"❌"}),e("p",{className:"text-sm text-red-600",children:Re.instructions})]}),e("div",{className:"space-y-3",children:G.map((t,o)=>{const a=Re.instructionItems?.[o];return r("div",{className:"group relative rounded-xl border-2 p-3 transition-all duration-200 "+(t.trim()?"border-green-200 bg-gradient-to-r from-green-50 to-emerald-50":"border-gray-200 bg-white hover:border-gray-300"),children:[r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg font-bold transition-all "+(t.trim()?"bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-md":"bg-gray-100 text-gray-600"),children:o+1}),e(k,{value:t,onChange:e=>{const r=[...G];r[o]=e.target.value,Q(r),de.clearErrors("instructions")},placeholder:`Ví dụ: ${q.instructionPlaceholder}`,className:"flex-1 border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 "+(a?"border-red-400":"")}),e(i,{size:"sm",variant:"ghost",onClick:()=>{Q(G.filter((e,r)=>r!==o))},className:"text-gray-400 hover:bg-red-50 hover:text-red-500",children:e(f,{className:"h-4 w-4"})})]}),a&&e("p",{className:"ml-11 mt-2 text-sm text-red-600",children:a})]},o)})}),0===G.length&&r("div",{className:"flex items-start gap-3 rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-4",children:[e("span",{className:"text-lg",children:"⚠️"}),e("p",{className:"text-sm text-amber-800",children:'Chưa có hướng dẫn nào. Nhấn "Thêm câu lệnh" để bắt đầu.'})]})]}),r("div",{className:"space-y-2",children:[r(l,{className:"flex items-center gap-2 text-base font-semibold text-gray-800",children:[e(w,{className:"h-4 w-4 text-indigo-500"}),"Ảnh nền ",e("span",{className:"text-red-500",children:"*"})]}),e(I,{id:"image-upload",label:"Ảnh nền",accept:"image/*",value:J,onChange:e=>{O(e),ye(!1),de.clearErrors()},onPresignedUrlChange:e=>{pe(e),ye(!1)},disabled:fe,maxSize:5,placeholder:"Paste URL hoặc upload file",required:!0,autoUpload:!0,prefix:"questions",onFileLoad:()=>{ye(!1)}}),Re.backgroundImageUrl&&e("p",{className:"mt-1 text-sm text-red-600",children:Re.backgroundImageUrl})]}),!fe&&r("div",{className:"space-y-3",children:[r("div",{className:"flex items-center justify-between",children:[r(l,{className:"flex items-center gap-2 text-base font-semibold text-gray-800",children:[e(v,{className:"h-4 w-4 text-purple-500"}),q.addItemLabel||"Thêm Màu"," ",e("span",{className:"text-red-500",children:"*"})]}),!te&&r(i,{onClick:()=>oe(!0),className:"gap-2 border-purple-200 text-purple-600 hover:bg-purple-50 hover:text-purple-700",size:"sm",variant:"outline",disabled:0===Ae.length,children:[e(x,{className:"h-4 w-4"}),q.addItemLabel||"Thêm màu"]})]}),Re.colorNodes&&r("div",{className:"flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4",children:[e("span",{className:"text-lg",children:"❌"}),e("p",{className:"text-sm text-red-600",children:Re.colorNodes})]}),te&&r("div",{className:"flex flex-col gap-3 rounded-xl border-2 border-purple-200 bg-gradient-to-r from-purple-50 to-indigo-50 p-4",children:[ae&&e(k,{value:ee,onChange:e=>re(e.target.value),placeholder:q.colorLabelPlaceholder||"Nhập label (ví dụ: Tom, Apple...)",className:"h-9 border-gray-200 bg-white"}),r("div",{className:"flex gap-2",children:[r(U,{value:_,onValueChange:Z,children:[e(L,{className:"flex-1 border-gray-200 bg-white",children:e(j,{placeholder:"Chọn màu..."})}),e(E,{children:Ae.map(t=>e(M,{value:t,children:r("div",{className:"flex items-center gap-2",children:[e("div",{className:"h-4 w-4 rounded border border-gray-300",style:{backgroundColor:t}}),e("span",{className:"font-mono text-xs",children:t})]})},t))})]}),e(i,{size:"sm",onClick:ze,disabled:!_||0===Ae.length||ae&&!ee.trim(),className:"bg-purple-600 hover:bg-purple-700",children:"Tạo"}),e(i,{size:"sm",variant:"outline",onClick:()=>{oe(!1),Z(""),re("")},children:"Hủy"})]})]}),0===X.length&&!te&&r("div",{className:"flex items-start gap-3 rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-4",children:[e("span",{className:"text-lg",children:"⚠️"}),e("p",{className:"text-sm text-amber-800",children:q.emptyItemMessage||'Chưa có màu nào. Nhấn "Thêm màu" để bắt đầu.'})]})]}),Re.regionNodes&&r("div",{className:"flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4",children:[e("span",{className:"text-lg",children:"❌"}),e("p",{className:"text-sm text-red-600",children:Re.regionNodes})]})]})]}),r(d,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-indigo-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-indigo-400 to-purple-500"}),e(m,{className:"pb-3",children:r("div",{className:"flex items-center gap-2",children:[e(g,{className:"text-base font-semibold text-gray-800",children:"Workspace — Ảnh nền"}),e("span",{className:"text-xs text-gray-500",children:"Kéo thả region để định vị"})]})}),r(c,{className:"p-0",children:[X.length>0&&e("div",{className:"border-b border-gray-100 px-4 py-3 flex flex-wrap gap-2",children:X.map(t=>r("div",{className:"inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-900",children:[e("div",{className:"h-4 w-4 rounded border border-gray-300",style:{backgroundColor:t.color}}),t.label?e("span",{className:"text-xs",children:t.label}):e("span",{className:"font-mono text-xs",children:t.color}),!fe&&e("button",{onClick:()=>je(t.id),className:"group rounded p-0.5 transition-all hover:scale-110 hover:bg-red-100",type:"button",children:e(u,{className:"h-3 w-3 text-gray-500 transition-colors group-hover:text-red-600"})})]},t.id))}),e("div",{ref:we,className:"relative w-full overflow-hidden select-none isolate",style:{cursor:ke?"grabbing":"default"},children:ue?r(t,{children:[e("img",{src:ue,alt:"Background",className:"block w-full h-auto",draggable:!1,onLoad:()=>ye(!0),onError:()=>{de.setError("backgroundImageUrl",{message:"Không thể load ảnh!"}),ye(!1)}}),ve&&W.map(r=>{const t=X.find(e=>e.id===r.linkedColorNodeId);return e("div",{style:{position:"absolute",left:`${r.x}%`,top:`${r.y}%`,width:`${r.width}%`,height:`${r.height}%`,cursor:fe?"default":"grab",zIndex:ke===r.id?2:1},onMouseDown:e=>Ee(r.id,r.x,r.y,e),children:e(S,{data:{color:r.color,x:r.x,y:r.y,width:r.width,height:r.height,onRemove:()=>Se(r.id),isEditMode:!fe,isClientMode:fe,label:t?.label||""}})},r.id)})]}):r("div",{className:"flex flex-col items-center justify-center gap-2 py-20 text-gray-400",children:[e("svg",{xmlns:"http://www.w3.org/2000/svg",className:"h-10 w-10 text-gray-300",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:e("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:1.5,d:"M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"})}),e("p",{className:"text-sm",children:"Chưa có ảnh nền. Hãy upload ảnh ở trên."})]})})]})]}),r(d,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-gray-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-gray-300 to-gray-400"}),e(m,{className:"pb-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-gray-100",children:e(N,{className:"h-3.5 w-3.5 text-gray-600"})}),e(g,{className:"text-lg font-semibold text-gray-800",children:"Giải thích"})]})}),e(c,{children:e(C,{id:"explanation",value:ie,onChange:e=>le(e.target.value),placeholder:"Nhập giải thích cho đáp án đúng...",rows:3,className:"border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200"})})]})]})}export function ColorRegionCreator(r){return e(A,{...r})}
@@ -1,59 +1 @@
1
- 'use client';
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { memo, useState, useEffect, useRef } from 'react';
4
- import { ColorNode } from './ColorNode';
5
- export const ColorRegionNode = memo(({ data }) => {
6
- const { color: propColor, width, height, isClientMode, savedColor: propSavedColor, isUnassignedRegion, isHighlighted, label, } = data;
7
- // In creator mode, use propColor directly (no state needed to avoid re-render loops)
8
- // In client mode, use state to track propSavedColor changes from drag & drop
9
- const [savedColor, setSavedColor] = useState(propSavedColor || '');
10
- const prevPropSavedColorRef = useRef(propSavedColor || '');
11
- // Only sync state in client mode when propSavedColor actually changes
12
- // In creator mode, always use propColor directly - no state updates needed
13
- useEffect(() => {
14
- if (isClientMode) {
15
- // Only update if propSavedColor actually changed
16
- if (prevPropSavedColorRef.current !== propSavedColor) {
17
- prevPropSavedColorRef.current = propSavedColor || '';
18
- setSavedColor(propSavedColor || '');
19
- }
20
- }
21
- else {
22
- // Reset ref when switching to creator mode
23
- prevPropSavedColorRef.current = '';
24
- }
25
- }, [isClientMode, propSavedColor]);
26
- // Determine the color to use: in creator mode use propColor directly, in client mode use savedColor
27
- const displayColor = isClientMode ? savedColor : propColor || '';
28
- return (_jsx("div", { className: `relative rounded-2xl transition-all ${
29
- // Unassigned region has no visible styling (transparent)
30
- isUnassignedRegion
31
- ? '' // No border/background for unassigned region
32
- : // In client mode, show transparent region (ColorNode will be rendered separately)
33
- isClientMode
34
- ? displayColor || isHighlighted
35
- ? '' // No border/background when ColorNode is assigned or when highlighted (node đang drag vào)
36
- : 'rounded-2xl border border-dashed border-red-300 bg-black/10' // Client mode - subtle red border when empty
37
- : displayColor
38
- ? '' // Match client mode when region đã có màu
39
- : 'rounded-2xl border border-dashed border-red-300 bg-black/5' // Creator mode empty — thinner, lighter border
40
- }`, style: isUnassignedRegion
41
- ? {
42
- width: '100%',
43
- height: '100%',
44
- }
45
- : {
46
- width: `${width}%`,
47
- height: `${height}%`,
48
- minWidth: '60px',
49
- minHeight: '40px',
50
- borderRadius: '16px',
51
- }, children: !isClientMode && displayColor && (_jsx("div", { className: "pointer-events-none h-full w-full", children: _jsx(ColorNode, { data: {
52
- color: displayColor,
53
- isConnected: true,
54
- onRemove: () => { },
55
- displayMode: 'compact',
56
- label: label,
57
- }, variant: "client" }) })) }));
58
- });
59
- ColorRegionNode.displayName = 'ColorRegionNode';
1
+ "use client";import{jsx as e}from"react/jsx-runtime";import{memo as o,useState as r,useEffect as d,useRef as i}from"react";import{ColorNode as t}from"./ColorNode";export const ColorRegionNode=o(({data:o})=>{const{color:l,width:n,height:a,isClientMode:s,savedColor:c,isUnassignedRegion:h,isHighlighted:m,label:b}=o,[g,u]=r(c||""),p=i(c||"");d(()=>{s?p.current!==c&&(p.current=c||"",u(c||"")):p.current=""},[s,c]);const x=s?g:l||"";return e("div",{className:"relative rounded-2xl transition-all "+(h?"":s?x||m?"":"rounded-2xl border border-dashed border-red-300 bg-black/10":x?"":"rounded-2xl border border-dashed border-red-300 bg-black/5"),style:h?{width:"100%",height:"100%"}:{width:`${n}%`,height:`${a}%`,minWidth:"60px",minHeight:"40px",borderRadius:"16px"},children:!s&&x&&e("div",{className:"pointer-events-none h-full w-full",children:e(t,{data:{color:x,isConnected:!0,onRemove:()=>{},displayMode:"compact",label:b},variant:"client"})})})});ColorRegionNode.displayName="ColorRegionNode";