@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.
- package/dist/api/exam-create.js +1 -179
- package/dist/api/exam-create.types.js +1 -1
- package/dist/api/exam-questions.js +1 -98
- package/dist/api/exam-questions.types.js +1 -1
- package/dist/api/exam-taking.js +1 -73
- package/dist/api/exam-taking.types.js +1 -1
- package/dist/api/index.js +1 -6
- package/dist/api/result-review.js +1 -50
- package/dist/api/result-review.types.js +1 -1
- package/dist/components/CueCardEditor.js +1 -58
- package/dist/components/common/ResolvedImage.js +1 -15
- package/dist/components/exams/CreateExamInfoForm.js +1 -35
- package/dist/components/exams/CreateExamPageContainer.js +1 -103
- package/dist/components/exams/ExamCreator.js +1 -89
- package/dist/components/exams/ExamPartTabs.js +1 -40
- package/dist/components/exams/ExamQuestionGrid.js +1 -18
- package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
- package/dist/components/exams/PartAudioUploader.js +1 -9
- package/dist/components/exams/TemplateSelector.js +1 -66
- package/dist/components/exams/api.js +1 -3
- package/dist/components/exams/create-exam.validation.js +1 -18
- package/dist/components/exams/exam-template.utils.js +1 -18
- package/dist/components/exams/index.js +1 -10
- package/dist/components/exams/take/ExamTakingPageContainer.js +1 -153
- package/dist/components/exams/take/GenericQuestionRenderer.js +1 -14
- package/dist/components/exams/take/exam-taking.utils.js +1 -88
- package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -24
- package/dist/components/exams/take/index.js +1 -2
- package/dist/components/exams/take/types.js +1 -1
- package/dist/components/index.js +1 -42
- package/dist/components/questions/_shared/config/question-types.config.js +1 -333
- package/dist/components/questions/_shared/constants/color-palette.js +1 -21
- package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +1 -105
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +1 -222
- package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +1 -140
- package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +1 -123
- package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +1 -50
- package/dist/components/questions/_shared/hooks/useQuestionFormState.js +1 -54
- package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +1 -123
- package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +1 -121
- package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +1 -70
- package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +1 -194
- package/dist/components/questions/_shared/index.js +1 -7
- package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
- package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
- package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +1 -3
- package/dist/components/questions/_shared/types/color-region.config.js +1 -1
- package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
- package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
- package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
- package/dist/components/questions/_shared/types/index.js +1 -12
- package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +1 -3
- package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +1 -3
- package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
- package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
- package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
- package/dist/components/questions/_shared/types/question-group.type.js +1 -11
- package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
- package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
- package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
- package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
- package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
- package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
- package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
- package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +1 -3
- package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
- package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
- package/dist/components/questions/_shared/utils/answer-builder.js +1 -79
- package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +1 -39
- package/dist/components/questions/_shared/utils/id-generator.js +1 -3
- package/dist/components/questions/_shared/utils/question-validation.js +1 -21
- package/dist/components/questions/components/ColorNode.js +1 -85
- package/dist/components/questions/components/ColorRegionClient.js +1 -545
- package/dist/components/questions/components/ColorRegionCreator.js +1 -420
- package/dist/components/questions/components/ColorRegionNode.js +1 -59
- package/dist/components/questions/components/CreatorGuide.js +1 -103
- package/dist/components/questions/components/NameNode.js +1 -26
- package/dist/components/questions/components/QuestionBankOption.js +1 -46
- package/dist/components/questions/components/QuestionBasicInfoForm.js +1 -9
- package/dist/components/questions/components/QuestionCommonFields.js +1 -33
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -258
- package/dist/components/questions/components/QuestionTypeSelector.js +1 -32
- package/dist/components/questions/components/QuestionTypeSpecificForms.js +1 -6
- package/dist/components/questions/components/RegionNode.js +1 -94
- package/dist/components/questions/components/index.js +1 -14
- package/dist/components/questions/creator/QuestionCreator.js +1 -90
- package/dist/components/questions/creator/QuestionGroupCreator.js +1 -119
- package/dist/components/questions/creator/dedicated-component-router.js +1 -79
- package/dist/components/questions/creator/index.js +1 -3
- package/dist/components/questions/creator/question-type-registry.js +1 -80
- package/dist/components/questions/groups/ArticlesGroupCard.js +1 -121
- package/dist/components/questions/groups/BasicQuestionGroupCard.js +1 -50
- package/dist/components/questions/groups/DocumentGroupCard.js +1 -114
- package/dist/components/questions/groups/MatchAnswerGroupCard.js +1 -25
- package/dist/components/questions/index.js +1 -10
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +1 -37
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +1 -351
- package/dist/components/questions/types/answer-the-question/register.js +1 -67
- package/dist/components/questions/types/answer-the-question/transform.js +1 -36
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +1 -141
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +1 -118
- package/dist/components/questions/types/arrange-letters-into-words/index.js +1 -2
- package/dist/components/questions/types/arrange-letters-into-words/register.js +1 -4
- package/dist/components/questions/types/arrange-letters-into-words/transform.js +1 -24
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +1 -64
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +1 -169
- package/dist/components/questions/types/choose-correct-adjective/index.js +1 -2
- package/dist/components/questions/types/choose-correct-adjective/register.js +1 -55
- package/dist/components/questions/types/choose-correct-adjective/transform.js +1 -16
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -131
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -581
- package/dist/components/questions/types/choose-the-correct-answer/index.js +1 -2
- package/dist/components/questions/types/choose-the-correct-answer/register.js +1 -101
- package/dist/components/questions/types/choose-the-correct-answer/transform.js +1 -43
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -275
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +1 -448
- package/dist/components/questions/types/fill-in-blank/index.js +1 -2
- package/dist/components/questions/types/fill-in-blank/register.js +1 -138
- package/dist/components/questions/types/fill-in-blank/transform.js +1 -59
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +1 -117
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +1 -671
- package/dist/components/questions/types/fill-missing-words-in-grid/index.js +1 -2
- package/dist/components/questions/types/fill-missing-words-in-grid/register.js +1 -37
- package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +1 -15
- package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +1 -262
- package/dist/components/questions/types/gn-speaking-interview/register.js +1 -121
- package/dist/components/questions/types/gn-speaking-interview/transform.js +1 -48
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +1 -16
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +1 -27
- package/dist/components/questions/types/image-object-matching/index.js +1 -2
- package/dist/components/questions/types/image-object-matching/register.js +1 -3
- package/dist/components/questions/types/image-object-matching/transform.js +1 -2
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +1 -102
- package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +1 -344
- package/dist/components/questions/types/label-the-picture/index.js +1 -2
- package/dist/components/questions/types/label-the-picture/register.js +1 -54
- package/dist/components/questions/types/label-the-picture/transform.js +1 -28
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +1 -78
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +1 -232
- package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +1 -2
- package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +1 -82
- package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +1 -27
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +1 -70
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +1 -386
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +1 -2
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +1 -100
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +1 -33
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +1 -106
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +1 -457
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +1 -2
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +1 -110
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +1 -35
- package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +1 -8
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +1 -22
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +1 -291
- package/dist/components/questions/types/listen-and-speak-answer/index.js +1 -2
- package/dist/components/questions/types/listen-and-speak-answer/register.js +1 -84
- package/dist/components/questions/types/listen-and-speak-answer/transform.js +1 -27
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +1 -32
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +1 -295
- package/dist/components/questions/types/listen-and-speak-compare-images/register.js +1 -79
- package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +1 -27
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +1 -34
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +1 -285
- package/dist/components/questions/types/listen-and-speak-image-group/register.js +1 -81
- package/dist/components/questions/types/listen-and-speak-image-group/transform.js +1 -26
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +1 -38
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +1 -385
- package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +1 -87
- package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +1 -32
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +1 -247
- package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +1 -59
- package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +1 -18
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +1 -256
- package/dist/components/questions/types/listen-and-speak-question-list/register.js +1 -81
- package/dist/components/questions/types/listen-and-speak-question-list/transform.js +1 -26
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +1 -230
- package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +1 -56
- package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +1 -16
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +1 -72
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +1 -230
- package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +1 -66
- package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +1 -42
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +1 -96
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +1 -238
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +1 -2
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +1 -76
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +1 -19
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +1 -134
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +1 -617
- package/dist/components/questions/types/look-picture-fill-word-hint/index.js +1 -2
- package/dist/components/questions/types/look-picture-fill-word-hint/register.js +1 -82
- package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +1 -29
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +1 -38
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +1 -359
- package/dist/components/questions/types/match-by-writing-answer/index.js +1 -3
- package/dist/components/questions/types/match-by-writing-answer/register.js +1 -58
- package/dist/components/questions/types/match-by-writing-answer/transform.js +1 -61
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +1 -16
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +1 -23
- package/dist/components/questions/types/read-and-color-objects/index.js +1 -2
- package/dist/components/questions/types/read-and-color-objects/register.js +1 -3
- package/dist/components/questions/types/read-and-color-objects/transform.js +1 -36
- package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +1 -125
- package/dist/components/questions/types/read-displayed-content/register.js +1 -64
- package/dist/components/questions/types/read-displayed-content/transform.js +1 -12
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +1 -64
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +1 -511
- package/dist/components/questions/types/read-passage-and-answer-questions/index.js +1 -2
- package/dist/components/questions/types/read-passage-and-answer-questions/register.js +1 -58
- package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +1 -28
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +1 -373
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -41
- package/dist/components/questions/types/speaking-conversation/register.js +1 -110
- package/dist/components/questions/types/speaking-conversation/transform.js +1 -34
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -74
- package/dist/components/questions/types/speaking-cue-card/register.js +1 -52
- package/dist/components/questions/types/speaking-cue-card/transform.js +1 -12
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +1 -10
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +1 -161
- package/dist/components/questions/types/speaking-describe-image/index.js +1 -2
- package/dist/components/questions/types/speaking-describe-image/register.js +1 -69
- package/dist/components/questions/types/speaking-describe-image/transform.js +1 -16
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +1 -234
- package/dist/components/questions/types/spontaneous-qa/register.js +1 -93
- package/dist/components/questions/types/spontaneous-qa/transform.js +1 -34
- package/dist/components/questions/types/true-false/TrueFalseClient.js +1 -75
- package/dist/components/questions/types/true-false/TrueFalseCreator.js +1 -244
- package/dist/components/questions/types/true-false/index.js +1 -2
- package/dist/components/questions/types/true-false/register.js +1 -77
- package/dist/components/questions/types/true-false/transform.js +1 -32
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +1 -233
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +1 -631
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +1 -28
- package/dist/components/questions/types/word-fill-paragraph/index.js +1 -2
- package/dist/components/questions/types/word-fill-paragraph/register.js +1 -136
- package/dist/components/questions/types/word-fill-paragraph/transform.js +1 -56
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +1 -138
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +1 -261
- package/dist/components/questions/types/word-fill-structured-form/index.js +1 -2
- package/dist/components/questions/types/word-fill-structured-form/register.js +1 -67
- package/dist/components/questions/types/word-fill-structured-form/transform.js +1 -40
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +1 -84
- package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +1 -192
- package/dist/components/questions/types/word-ordering/index.js +1 -2
- package/dist/components/questions/types/word-ordering/register.js +1 -46
- package/dist/components/questions/types/word-ordering/transform.js +1 -24
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +1 -98
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +1 -320
- package/dist/components/questions/types/write-a-short-letter/index.js +1 -2
- package/dist/components/questions/types/write-a-short-letter/register.js +1 -68
- package/dist/components/questions/types/write-a-short-letter/transform.js +1 -25
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +1 -61
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +1 -196
- package/dist/components/questions/types/write-correct-verb-form/index.js +1 -2
- package/dist/components/questions/types/write-correct-verb-form/register.js +1 -62
- package/dist/components/questions/types/write-correct-verb-form/transform.js +1 -17
- package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +1 -22
- package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +1 -174
- package/dist/components/questions/types/write-sentences/index.js +1 -2
- package/dist/components/questions/types/write-sentences/register.js +1 -58
- package/dist/components/questions/types/write-sentences/transform.js +1 -28
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +1 -96
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +1 -161
- package/dist/components/questions/types/write-short-paragraph/index.js +1 -2
- package/dist/components/questions/types/write-short-paragraph/register.js +1 -4
- package/dist/components/questions/types/write-short-paragraph/transform.js +1 -21
- package/dist/components/questions/viewer/QuestionGroupViewer.js +1 -36
- package/dist/components/questions/viewer/QuestionViewer.js +1 -446
- package/dist/components/questions/viewer/index.js +1 -2
- package/dist/components/results/ResultReviewPageContainer.js +1 -98
- package/dist/components/results/ReviewQuestionRenderer.js +1 -72
- package/dist/components/results/index.js +1 -2
- package/dist/components/results/result-review.utils.js +1 -6
- package/dist/components/results/review-data.js +1 -150
- package/dist/components/results/review-renderer.utils.js +1 -97
- package/dist/components/results/review-types.js +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +1 -138
- package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +1 -12
- package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +1 -18
- package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +1 -11
- package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +1 -11
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +1 -9
- package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +1 -13
- package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +1 -8
- package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +1 -6
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +1 -5
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +1 -9
- package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +1 -41
- package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -25
- package/dist/components/themes/cambridge-yle/index.js +1 -13
- package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +1 -24
- package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +1 -88
- package/dist/components/themes/english-certification/EcWordFillParagraph.js +1 -55
- package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +1 -156
- package/dist/components/themes/english-certification/EcWritingTask.js +1 -34
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -114
- package/dist/components/themes/english-certification/EnglishCertificationBrand.js +1 -11
- package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +1 -21
- package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +1 -15
- package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +1 -19
- package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +1 -148
- package/dist/components/themes/english-certification/EnglishCertificationHeader.js +1 -31
- package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +1 -71
- package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +1 -33
- package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +1 -16
- package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +1 -22
- package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +1 -37
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -72
- package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +1 -90
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +1 -18
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +1 -29
- package/dist/components/themes/english-certification/english-certification-theme.js +1 -58
- package/dist/components/themes/english-certification/grouped-question-utils.js +1 -218
- package/dist/components/themes/english-certification/index.js +1 -21
- package/dist/components/themes/index.js +1 -6
- package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +1 -170
- package/dist/components/themes/summer-sky/SummerSkyLayout.js +1 -221
- package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +1 -18
- package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +1 -194
- package/dist/components/themes/types.js +1 -1
- package/dist/components/ui/alert-dialog.js +1 -26
- package/dist/components/ui/alert.js +1 -22
- package/dist/components/ui/avatar.js +1 -12
- package/dist/components/ui/badge.js +1 -38
- package/dist/components/ui/button.js +1 -35
- package/dist/components/ui/calendar.js +1 -76
- package/dist/components/ui/card.js +1 -16
- package/dist/components/ui/carousel.js +1 -91
- package/dist/components/ui/checkbox.js +1 -9
- package/dist/components/ui/collapsible.js +1 -13
- package/dist/components/ui/command.js +1 -34
- package/dist/components/ui/confirm-dialog.js +1 -49
- package/dist/components/ui/dialog.js +1 -23
- package/dist/components/ui/drawer.js +1 -35
- package/dist/components/ui/dropdown-menu.js +1 -33
- package/dist/components/ui/file-upload.js +1 -155
- package/dist/components/ui/form.js +1 -62
- package/dist/components/ui/hover-card.js +1 -10
- package/dist/components/ui/image-preview.js +1 -29
- package/dist/components/ui/input.js +1 -8
- package/dist/components/ui/label.js +1 -9
- package/dist/components/ui/language-switcher.js +1 -15
- package/dist/components/ui/multi-select.js +1 -90
- package/dist/components/ui/points-input.js +1 -33
- package/dist/components/ui/popover.js +1 -26
- package/dist/components/ui/progress.js +1 -16
- package/dist/components/ui/radio-group.js +1 -12
- package/dist/components/ui/scroll-area.js +1 -11
- package/dist/components/ui/select.js +1 -38
- package/dist/components/ui/separator.js +1 -8
- package/dist/components/ui/sheet.js +1 -40
- package/dist/components/ui/single-select.js +1 -76
- package/dist/components/ui/skeleton.js +1 -6
- package/dist/components/ui/switch.js +1 -8
- package/dist/components/ui/table.js +1 -20
- package/dist/components/ui/tabs.js +1 -17
- package/dist/components/ui/textarea.js +1 -8
- package/dist/components/ui/tooltip.js +1 -11
- package/dist/index.js +1 -19
- package/dist/mcp/config.js +1 -32
- package/dist/mcp/http-client.js +1 -48
- package/dist/mcp/index.js +1 -29
- package/dist/mcp/tools/_helpers.js +1 -25
- package/dist/mcp/tools/exams.js +1 -439
- package/dist/mcp/tools/index.js +1 -6
- package/dist/mcp/tools/results.js +1 -19
- package/dist/shared/constants/ApiConstant.js +1 -276
- package/dist/shared/constants/QueryKeyConstant.js +1 -112
- package/dist/shared/constants/exam-level.enum.js +1 -41
- package/dist/shared/constants/landings/placement-test.constants.js +1 -431
- package/dist/shared/constants/question-skills.js +1 -267
- package/dist/shared/constants/routes.js +1 -81
- package/dist/shared/index.js +1 -7
- package/dist/shared/lib/hooks/index.js +1 -10
- package/dist/shared/lib/hooks/useAlertDialog.js +1 -57
- package/dist/shared/lib/hooks/useAudioPlayCounter.js +1 -30
- package/dist/shared/lib/hooks/useAudioPreview.js +1 -81
- package/dist/shared/lib/hooks/useAudioUpload.js +1 -24
- package/dist/shared/lib/hooks/useConfirmDialog.js +1 -89
- package/dist/shared/lib/hooks/useDebouncedCallback.js +1 -32
- package/dist/shared/lib/hooks/useDelayedAutoPlay.js +1 -12
- package/dist/shared/lib/hooks/useFileUpload.js +1 -24
- package/dist/shared/lib/hooks/useImageUrl.js +1 -5
- package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -10
- package/dist/shared/lib/hooks/useReactHookForm.js +1 -31
- package/dist/shared/lib/hooks/useToast.js +1 -62
- package/dist/shared/lib/i18n/index.js +1 -54
- package/dist/shared/lib/i18n/messages/en/admin.js +1 -2104
- package/dist/shared/lib/i18n/messages/en/auth.js +1 -86
- package/dist/shared/lib/i18n/messages/en/common.js +1 -140
- package/dist/shared/lib/i18n/messages/en/file-upload.js +1 -18
- package/dist/shared/lib/i18n/messages/en/student.js +1 -133
- package/dist/shared/lib/i18n/messages/en/terms.js +1 -37
- package/dist/shared/lib/i18n/messages/en.js +1 -15
- package/dist/shared/lib/i18n/messages/vi/admin.js +1 -2104
- package/dist/shared/lib/i18n/messages/vi/auth.js +1 -86
- package/dist/shared/lib/i18n/messages/vi/common.js +1 -140
- package/dist/shared/lib/i18n/messages/vi/file-upload.js +1 -18
- package/dist/shared/lib/i18n/messages/vi/student.js +1 -133
- package/dist/shared/lib/i18n/messages/vi/terms.js +1 -37
- package/dist/shared/lib/i18n/messages/vi.js +1 -15
- package/dist/shared/lib/i18n/types.js +1 -1
- package/dist/shared/lib/stores/examQuestionStore.js +1 -212
- package/dist/shared/lib/stores/localeStore.js +1 -10
- package/dist/shared/lib/utils/public-page.js +1 -44
- package/dist/shared/lib/utils/question-transform-types.js +1 -1
- package/dist/shared/lib/utils/question-transform.js +1 -174
- package/dist/shared/lib/utils.js +1 -168
- package/dist/shared/lib/validation/choose-correct-answer.validation.js +1 -111
- package/dist/shared/lib/validation/label-the-picture.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +1 -46
- package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +1 -21
- package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +1 -34
- package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +1 -40
- package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +1 -45
- package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +1 -39
- package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +1 -32
- package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +1 -28
- package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +1 -12
- package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +1 -47
- package/dist/shared/lib/validation/read-and-color-objects.validation.js +1 -29
- package/dist/shared/lib/validation/read-displayed-content.validation.js +1 -28
- package/dist/shared/lib/validation/speaking-conversation.validation.js +1 -75
- package/dist/shared/lib/validation/speaking-describe-image.validation.js +1 -31
- package/dist/shared/lib/validation/spontaneous-qa.validation.js +1 -48
- package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +1 -38
- package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +1 -39
- package/dist/shared/lib/validation/word-fill-paragraph.validation.js +1 -43
- package/dist/shared/lib/validation/word-ordering.validation.js +1 -33
- package/dist/shared/types/branch.types.js +1 -6
- package/dist/shared/types/common.types.js +1 -50
- package/dist/shared/types/entities/exam-schedule.types.js +1 -5
- package/dist/shared/types/entities/exam.types.js +1 -1
- package/dist/shared/types/entities/question.types.js +1 -4
- package/dist/shared/types/entities/result.types.js +1 -4
- package/dist/shared/types/entities/user.types.js +1 -4
- package/dist/shared/types/exam-taking.types.js +1 -2
- package/dist/shared/types/history.types.js +1 -34
- package/dist/shared/types/index.js +1 -1
- package/dist/shared/types/questions/choose-correct-adjective.js +1 -4
- package/dist/shared/types/questions/choose-the-correct-answer.js +1 -9
- package/dist/shared/types/questions/fill-missing-words-in-grid.js +1 -11
- package/dist/shared/types/questions/index.js +1 -40
- package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-info-exchange.js +1 -3
- package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +1 -3
- package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-with-story-images.js +1 -3
- package/dist/shared/types/questions/look-picture-choose-correct-answer.js +1 -5
- package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +1 -9
- package/dist/shared/types/questions/look-picture-fill-word-hint.js +1 -7
- package/dist/shared/types/questions/read-displayed-content.js +1 -1
- package/dist/shared/types/questions/speaking-conversation.js +1 -7
- package/dist/shared/types/questions/speaking-describe-image.js +1 -1
- package/dist/shared/types/questions/spontaneous-qa.js +1 -1
- package/dist/shared/types/questions/write-correct-verb-form.js +1 -12
- package/dist/shared/types/role.types.js +1 -1
- package/dist/shared/types/structure-management.js +1 -28
- package/dist/shared/types/student.types.js +1 -54
- package/dist/shared/types/teacher-dashboard-rooms.js +1 -2
- 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
|
-
|
|
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";
|