@tinyweb_dev/oe-exam-sdk 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/exam-create.js +1 -179
- package/dist/api/exam-create.types.js +1 -1
- package/dist/api/exam-questions.d.ts +1 -1
- package/dist/api/exam-questions.js +1 -98
- package/dist/api/exam-questions.types.d.ts +45 -1
- 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/CompletionDialog.d.ts +8 -0
- package/dist/components/exams/CompletionDialog.js +1 -0
- package/dist/components/exams/CreateExamInfoForm.d.ts +4 -1
- package/dist/components/exams/CreateExamInfoForm.js +1 -35
- package/dist/components/exams/CreateExamPageContainer.d.ts +3 -0
- package/dist/components/exams/CreateExamPageContainer.js +1 -103
- package/dist/components/exams/ExamCreator.d.ts +9 -3
- package/dist/components/exams/ExamCreator.js +1 -89
- package/dist/components/exams/ExamPartTabs.d.ts +1 -1
- package/dist/components/exams/ExamPartTabs.js +1 -40
- package/dist/components/exams/ExamPreviewDialog.d.ts +20 -0
- package/dist/components/exams/ExamPreviewDialog.js +1 -0
- package/dist/components/exams/ExamQuestionGrid.d.ts +1 -1
- package/dist/components/exams/ExamQuestionGrid.js +1 -18
- package/dist/components/exams/ExamQuestionsPageContainer.d.ts +3 -0
- package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
- package/dist/components/exams/ExamViewDialog.d.ts +24 -0
- package/dist/components/exams/ExamViewDialog.js +1 -0
- package/dist/components/exams/PartAudioUploader.js +1 -9
- package/dist/components/exams/TemplateSelector.js +1 -66
- package/dist/components/exams/UnsavedChangesDialog.d.ts +8 -0
- package/dist/components/exams/UnsavedChangesDialog.js +1 -0
- package/dist/components/exams/api.d.ts +1 -1
- package/dist/components/exams/api.js +1 -3
- package/dist/components/exams/create-exam.texts.d.ts +58 -0
- package/dist/components/exams/create-exam.texts.js +1 -0
- package/dist/components/exams/create-exam.validation.js +1 -18
- package/dist/components/exams/exam-question-status.d.ts +9 -0
- package/dist/components/exams/exam-question-status.js +1 -0
- package/dist/components/exams/exam-template.utils.js +1 -18
- package/dist/components/exams/index.d.ts +11 -1
- 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.d.ts +3 -3
- 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.d.ts +3 -0
- 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-reverse-transform.d.ts +6 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +1 -0
- package/dist/shared/lib/utils/question-transform-types.js +1 -1
- package/dist/shared/lib/utils/question-transform-validation.d.ts +6 -0
- package/dist/shared/lib/utils/question-transform-validation.js +1 -0
- package/dist/shared/lib/utils/question-transform.d.ts +6 -2
- 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 +12 -3
|
@@ -1,457 +1 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
|
-
import { Button } from '../../../../components/ui/button';
|
|
5
|
-
import { Label } from '../../../../components/ui/label';
|
|
6
|
-
import { Input } from '../../../../components/ui/input';
|
|
7
|
-
import { Textarea } from '../../../../components/ui/textarea';
|
|
8
|
-
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
9
|
-
import { PointsInput } from '../../../../components/ui/points-input';
|
|
10
|
-
import { RadioGroup, RadioGroupItem } from '../../../../components/ui/radio-group';
|
|
11
|
-
import { Pencil, Eye, Plus, Trash2, Image as ImageIcon, Volume2, Lightbulb, GripVertical, Target, Move, ChevronDown, ChevronUp, MapPin, ArrowRight, AlertTriangle, ListChecks, BookOpen, } from 'lucide-react';
|
|
12
|
-
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
13
|
-
import { usePresignedFileUrl, useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
14
|
-
import { useReactHookForm } from '../../../../shared/lib/hooks/useReactHookForm';
|
|
15
|
-
import { listenAndDragObjectsIntoSceneFormSchema, } from '../../../../shared/lib/validation/listen-and-drag-objects-into-scene.validation';
|
|
16
|
-
import { ListenAndDragObjectsIntoSceneClient } from './ListenAndDragObjectsIntoSceneClient';
|
|
17
|
-
import { useSceneQuestionGroup } from '../../_shared/hooks/useSceneQuestionGroup';
|
|
18
|
-
import { generateObjectId } from '../../_shared/utils/id-generator';
|
|
19
|
-
import { CreatorGuide } from '../../components/CreatorGuide';
|
|
20
|
-
import { useExamQuestionStore } from '../../../../shared/lib/stores/examQuestionStore';
|
|
21
|
-
// Drop zone color palette for visual correlation between editor and preview
|
|
22
|
-
const DROP_ZONE_COLORS = [
|
|
23
|
-
{ bg: 'bg-blue-500', border: 'border-blue-400', light: 'bg-blue-100', text: 'text-blue-700', dot: 'bg-blue-500' },
|
|
24
|
-
{ bg: 'bg-emerald-500', border: 'border-emerald-400', light: 'bg-emerald-100', text: 'text-emerald-700', dot: 'bg-emerald-500' },
|
|
25
|
-
{ bg: 'bg-purple-500', border: 'border-purple-400', light: 'bg-purple-100', text: 'text-purple-700', dot: 'bg-purple-500' },
|
|
26
|
-
{ bg: 'bg-rose-500', border: 'border-rose-400', light: 'bg-rose-100', text: 'text-rose-700', dot: 'bg-rose-500' },
|
|
27
|
-
{ bg: 'bg-amber-500', border: 'border-amber-400', light: 'bg-amber-100', text: 'text-amber-700', dot: 'bg-amber-500' },
|
|
28
|
-
{ bg: 'bg-cyan-500', border: 'border-cyan-400', light: 'bg-cyan-100', text: 'text-cyan-700', dot: 'bg-cyan-500' },
|
|
29
|
-
];
|
|
30
|
-
function getZoneColor(index) {
|
|
31
|
-
return DROP_ZONE_COLORS[index % DROP_ZONE_COLORS.length];
|
|
32
|
-
}
|
|
33
|
-
/** Shared group badge component */
|
|
34
|
-
function SharedGroupBadge({ groupNumber }) {
|
|
35
|
-
return (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-orange-100 px-2.5 py-0.5 text-xs font-medium text-orange-700", children: [_jsx("span", { children: "\uD83D\uDD17" }), "Nh\u00F3m ", groupNumber, " \u2014 D\u00F9ng chung"] }));
|
|
36
|
-
}
|
|
37
|
-
/** Helper component for presigned image display */
|
|
38
|
-
function PresignedImage({ url, alt }) {
|
|
39
|
-
const { previewUrl } = usePresignedFileUrl(url);
|
|
40
|
-
if (!previewUrl)
|
|
41
|
-
return _jsx(ImageIcon, { className: "h-4 w-4 text-gray-400" });
|
|
42
|
-
return (_jsx("img", { src: previewUrl, alt: alt, className: "h-full w-full object-contain pointer-events-none", draggable: false }));
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* DragObjectEditor - An individual drag object editor row (sidebar objects, no position)
|
|
46
|
-
* Enhanced with larger preview, delete confirmation, and consistent styling
|
|
47
|
-
*/
|
|
48
|
-
function DragObjectEditor({ node, index, onUpdate, onRemove, }) {
|
|
49
|
-
const [objectPreviewUrl, setObjectPreviewUrl] = useState('');
|
|
50
|
-
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
51
|
-
const { previewUrl: presignedObjectUrl } = usePresignedFileUrl(node.imageUrl || '');
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (presignedObjectUrl) {
|
|
54
|
-
setObjectPreviewUrl(presignedObjectUrl);
|
|
55
|
-
}
|
|
56
|
-
}, [presignedObjectUrl]);
|
|
57
|
-
const handleDelete = () => {
|
|
58
|
-
if (confirmDelete) {
|
|
59
|
-
onRemove(node.id);
|
|
60
|
-
setConfirmDelete(false);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
setConfirmDelete(true);
|
|
64
|
-
setTimeout(() => setConfirmDelete(false), 3000);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
return (_jsx("div", { className: "rounded-lg border-2 border-gray-200 bg-white p-4 transition-all hover:border-gray-300 hover:shadow-sm", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsxs("div", { className: "flex flex-col items-center gap-1 pt-1", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-orange-500 to-amber-600 text-sm font-bold text-white shadow-sm", children: index + 1 }), _jsx(GripVertical, { className: "h-4 w-4 text-gray-400" })] }), _jsx("div", { className: "flex-shrink-0", children: objectPreviewUrl ? (_jsx("div", { className: "h-20 w-20 overflow-hidden rounded-lg border-2 border-gray-200 bg-gray-50 transition-all hover:border-orange-300", children: _jsx("img", { src: objectPreviewUrl, alt: node.label || 'Object', className: "h-full w-full object-contain p-1" }) })) : (_jsx("div", { className: "flex h-20 w-20 items-center justify-center rounded-lg border-2 border-dashed border-gray-200 bg-gray-50", children: _jsx(ImageIcon, { className: "h-6 w-6 text-gray-300" }) })) }), _jsxs("div", { className: "flex-1 space-y-3", children: [_jsxs("div", { children: [_jsx(Label, { className: "text-xs font-medium text-gray-500", children: "T\u00EAn \u0111\u1ED1i t\u01B0\u1EE3ng" }), _jsx(Input, { value: node.label, onChange: (e) => onUpdate(node.id, { label: e.target.value }), placeholder: "V\u00ED d\u1EE5: lemon, kite, car...", className: "mt-1 border-gray-200" })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-xs font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh \u0111\u1ED1i t\u01B0\u1EE3ng" }), _jsx("div", { className: "mt-1", children: _jsx(FileUpload, { id: `drag-object-image-${node.id}`, label: "", accept: "image/*", value: node.imageUrl || '', onChange: (url) => onUpdate(node.id, { imageUrl: url }), onPresignedUrlChange: setObjectPreviewUrl, maxSize: 5, placeholder: "Upload h\u00ECnh \u1EA3nh \u0111\u1ED1i t\u01B0\u1EE3ng", autoUpload: true, prefix: "questions/objects" }) })] })] }), _jsxs("div", { className: "flex flex-col items-center gap-1 pt-1", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: handleDelete, className: `transition-all ${confirmDelete
|
|
68
|
-
? 'bg-red-100 text-red-600 hover:bg-red-200 hover:text-red-700 ring-1 ring-red-300'
|
|
69
|
-
: 'text-red-400 hover:bg-red-50 hover:text-red-600'}`, title: confirmDelete ? 'Nhấn lần nữa để xác nhận xóa' : 'Xóa đối tượng', children: _jsx(Trash2, { className: "h-4 w-4" }) }), confirmDelete && (_jsx("span", { className: "text-[10px] font-medium text-red-500 whitespace-nowrap", children: "Nh\u1EA5n l\u1EA1i?" }))] })] }) }));
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* DropZoneEditor - An individual drop zone editor row
|
|
73
|
-
* Enhanced with color-coded indicator matching the scene preview
|
|
74
|
-
*/
|
|
75
|
-
function DropZoneEditor({ zone, index, onUpdate, onRemove, isActive, onSelect, }) {
|
|
76
|
-
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
77
|
-
const color = getZoneColor(index);
|
|
78
|
-
const handleDelete = () => {
|
|
79
|
-
if (confirmDelete) {
|
|
80
|
-
onRemove(zone.id);
|
|
81
|
-
setConfirmDelete(false);
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
setConfirmDelete(true);
|
|
85
|
-
setTimeout(() => setConfirmDelete(false), 3000);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
return (_jsx("div", { className: `rounded-lg border-2 border-dashed p-3 transition-all cursor-pointer ${isActive
|
|
89
|
-
? 'border-blue-400 bg-blue-50/50 shadow-sm ring-1 ring-blue-200'
|
|
90
|
-
: 'border-gray-200 bg-white hover:border-blue-200 hover:bg-blue-50/20'}`, onClick: () => onSelect?.(zone.id), children: _jsxs("div", { className: "flex items-start gap-2.5", children: [_jsx("div", { className: "flex flex-col items-center gap-1 pt-1.5", children: _jsx("div", { className: `h-6 w-6 rounded-md ${color.dot} flex items-center justify-center text-[10px] font-bold text-white shadow-sm`, children: index + 1 }) }), _jsxs("div", { className: "flex-1 space-y-2", children: [_jsx(Input, { value: zone.label, onChange: (e) => onUpdate(zone.id, { label: e.target.value }), placeholder: "V\u00ED d\u1EE5: near the pond, on the tree...", className: "h-8 border-gray-200 text-sm", onClick: (e) => e.stopPropagation() }), _jsxs("div", { className: "grid grid-cols-4 gap-1.5", children: [_jsxs("div", { children: [_jsx(Label, { className: "text-[10px] font-medium text-gray-400", children: "X%" }), _jsx(Input, { type: "number", min: 0, max: 100, value: zone.x, onChange: (e) => onUpdate(zone.id, { x: parseFloat(e.target.value) || 0 }), className: "h-7 border-gray-200 text-xs px-1 text-center", onClick: (e) => e.stopPropagation() })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-[10px] font-medium text-gray-400", children: "Y%" }), _jsx(Input, { type: "number", min: 0, max: 100, value: zone.y, onChange: (e) => onUpdate(zone.id, { y: parseFloat(e.target.value) || 0 }), className: "h-7 border-gray-200 text-xs px-1 text-center", onClick: (e) => e.stopPropagation() })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-[10px] font-medium text-gray-400", children: "W%" }), _jsx(Input, { type: "number", min: 1, max: 100, value: zone.width, onChange: (e) => onUpdate(zone.id, { width: parseFloat(e.target.value) || 10 }), className: "h-7 border-gray-200 text-xs px-1 text-center", onClick: (e) => e.stopPropagation() })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-[10px] font-medium text-gray-400", children: "H%" }), _jsx(Input, { type: "number", min: 1, max: 100, value: zone.height, onChange: (e) => onUpdate(zone.id, { height: parseFloat(e.target.value) || 10 }), className: "h-7 border-gray-200 text-xs px-1 text-center", onClick: (e) => e.stopPropagation() })] })] })] }), _jsxs("div", { className: "flex flex-col items-center pt-1", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => {
|
|
91
|
-
e.stopPropagation();
|
|
92
|
-
handleDelete();
|
|
93
|
-
}, className: `h-7 w-7 p-0 transition-all ${confirmDelete
|
|
94
|
-
? 'bg-red-100 text-red-600 hover:bg-red-200 ring-1 ring-red-300'
|
|
95
|
-
: 'text-red-400 hover:bg-red-50 hover:text-red-600'}`, title: confirmDelete ? 'Nhấn lần nữa để xác nhận xóa' : 'Xóa vị trí', children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }), confirmDelete && (_jsx("span", { className: "text-[9px] font-medium text-red-500 mt-0.5", children: "X\u00F3a?" }))] })] }) }));
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Scene preview overlay for drop zones (drag-to-position)
|
|
99
|
-
* Enhanced with color-coded zones matching the editor list
|
|
100
|
-
*/
|
|
101
|
-
function DropZonePreviewWithDrag({ backgroundUrl, dropZones, onZoneDrag, onZoneResize, activeZoneId, onSelectZone, }) {
|
|
102
|
-
const containerRef = useRef(null);
|
|
103
|
-
const [draggingId, setDraggingId] = useState(null);
|
|
104
|
-
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
|
105
|
-
const [resizingId, setResizingId] = useState(null);
|
|
106
|
-
const [resizeType, setResizeType] = useState(null);
|
|
107
|
-
const [resizeStartPos, setResizeStartPos] = useState({ x: 0, y: 0 });
|
|
108
|
-
const [resizeStartDim, setResizeStartDim] = useState({ width: 0, height: 0 });
|
|
109
|
-
const handleMouseDown = (zoneId, zoneX, zoneY, e) => {
|
|
110
|
-
e.preventDefault();
|
|
111
|
-
if (e.target.dataset.resizeHandle)
|
|
112
|
-
return;
|
|
113
|
-
setDraggingId(zoneId);
|
|
114
|
-
onSelectZone?.(zoneId);
|
|
115
|
-
if (containerRef.current) {
|
|
116
|
-
const rect = containerRef.current.getBoundingClientRect();
|
|
117
|
-
const mouseXPercent = ((e.clientX - rect.left) / rect.width) * 100;
|
|
118
|
-
const mouseYPercent = ((e.clientY - rect.top) / rect.height) * 100;
|
|
119
|
-
setDragOffset({
|
|
120
|
-
x: mouseXPercent - zoneX,
|
|
121
|
-
y: mouseYPercent - zoneY,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
const handleResizeMouseDown = (zoneId, type, zoneWidth, zoneHeight, e) => {
|
|
126
|
-
e.preventDefault();
|
|
127
|
-
e.stopPropagation();
|
|
128
|
-
setResizingId(zoneId);
|
|
129
|
-
setResizeType(type);
|
|
130
|
-
setResizeStartPos({ x: e.clientX, y: e.clientY });
|
|
131
|
-
setResizeStartDim({ width: zoneWidth, height: zoneHeight });
|
|
132
|
-
};
|
|
133
|
-
const handleMouseMove = useCallback((e) => {
|
|
134
|
-
if (!containerRef.current)
|
|
135
|
-
return;
|
|
136
|
-
if (draggingId) {
|
|
137
|
-
const rect = containerRef.current.getBoundingClientRect();
|
|
138
|
-
const mouseXPercent = ((e.clientX - rect.left) / rect.width) * 100;
|
|
139
|
-
const mouseYPercent = ((e.clientY - rect.top) / rect.height) * 100;
|
|
140
|
-
const x = Math.max(0, Math.min(100, mouseXPercent - dragOffset.x));
|
|
141
|
-
const y = Math.max(0, Math.min(100, mouseYPercent - dragOffset.y));
|
|
142
|
-
onZoneDrag(draggingId, Math.round(x * 10) / 10, Math.round(y * 10) / 10);
|
|
143
|
-
}
|
|
144
|
-
else if (resizingId && resizeType) {
|
|
145
|
-
const rect = containerRef.current.getBoundingClientRect();
|
|
146
|
-
const dx = ((e.clientX - resizeStartPos.x) / rect.width) * 100;
|
|
147
|
-
const dy = ((e.clientY - resizeStartPos.y) / rect.height) * 100;
|
|
148
|
-
let newWidth = resizeStartDim.width;
|
|
149
|
-
let newHeight = resizeStartDim.height;
|
|
150
|
-
if (resizeType === 'e' || resizeType === 'se') {
|
|
151
|
-
newWidth = Math.max(1, resizeStartDim.width + dx);
|
|
152
|
-
}
|
|
153
|
-
if (resizeType === 's' || resizeType === 'se') {
|
|
154
|
-
newHeight = Math.max(1, resizeStartDim.height + dy);
|
|
155
|
-
}
|
|
156
|
-
onZoneResize(resizingId, Math.round(newWidth * 10) / 10, Math.round(newHeight * 10) / 10);
|
|
157
|
-
}
|
|
158
|
-
}, [draggingId, resizingId, resizeType, resizeStartPos, resizeStartDim, dragOffset, onZoneDrag, onZoneResize]);
|
|
159
|
-
const handleMouseUp = useCallback(() => {
|
|
160
|
-
setDraggingId(null);
|
|
161
|
-
setResizingId(null);
|
|
162
|
-
setResizeType(null);
|
|
163
|
-
}, []);
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
if (draggingId || resizingId) {
|
|
166
|
-
window.addEventListener('mousemove', handleMouseMove);
|
|
167
|
-
window.addEventListener('mouseup', handleMouseUp);
|
|
168
|
-
return () => {
|
|
169
|
-
window.removeEventListener('mousemove', handleMouseMove);
|
|
170
|
-
window.removeEventListener('mouseup', handleMouseUp);
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
}, [draggingId, resizingId, handleMouseMove, handleMouseUp]);
|
|
174
|
-
return (_jsxs("div", { ref: containerRef, className: "relative w-full select-none", children: [_jsx("img", { src: backgroundUrl, alt: "Scene background", className: "w-full rounded-md object-contain", draggable: false }), dropZones.map((zone, zoneIndex) => {
|
|
175
|
-
const isActive = draggingId === zone.id || resizingId === zone.id || activeZoneId === zone.id;
|
|
176
|
-
const color = getZoneColor(zoneIndex);
|
|
177
|
-
return (_jsxs("div", { className: `absolute cursor-move rounded-md border-2 border-dashed transition-colors group ${isActive
|
|
178
|
-
? `${color.border} bg-blue-200/30 shadow-lg z-20`
|
|
179
|
-
: `${color.border} bg-blue-100/20 hover:bg-blue-200/30 z-10`}`, style: {
|
|
180
|
-
left: `${zone.x}%`,
|
|
181
|
-
top: `${zone.y}%`,
|
|
182
|
-
width: `${zone.width}%`,
|
|
183
|
-
height: `${zone.height}%`,
|
|
184
|
-
}, onMouseDown: (e) => handleMouseDown(zone.id, zone.x, zone.y, e), children: [_jsxs("div", { className: `absolute -top-5 left-0 rounded ${color.dot} px-1.5 py-0.5 text-[10px] font-medium text-white whitespace-nowrap shadow-sm`, children: [zoneIndex + 1, ". ", zone.label || `Zone ${zoneIndex + 1}`] }), _jsx("div", { className: "flex h-full w-full items-center justify-center", children: _jsx(Target, { className: "h-4 w-4 text-blue-400/60" }) }), _jsx("div", { "data-resize-handle": "true", className: "absolute -right-1.5 top-1/2 h-3 w-3 -translate-y-1/2 cursor-e-resize rounded-full border border-blue-500 bg-white opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100", style: { opacity: isActive ? 1 : undefined }, onMouseDown: (e) => handleResizeMouseDown(zone.id, 'e', zone.width, zone.height, e) }), _jsx("div", { "data-resize-handle": "true", className: "absolute -bottom-1.5 left-1/2 h-3 w-3 -translate-x-1/2 cursor-s-resize rounded-full border border-blue-500 bg-white opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100", style: { opacity: isActive ? 1 : undefined }, onMouseDown: (e) => handleResizeMouseDown(zone.id, 's', zone.width, zone.height, e) }), _jsx("div", { "data-resize-handle": "true", className: "absolute -bottom-1.5 -right-1.5 h-3 w-3 cursor-se-resize rounded-full border border-blue-500 bg-white opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100", style: { opacity: isActive ? 1 : undefined }, onMouseDown: (e) => handleResizeMouseDown(zone.id, 'se', zone.width, zone.height, e) })] }, zone.id));
|
|
185
|
-
})] }));
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* ListenAndDragObjectsIntoSceneCreator
|
|
189
|
-
*
|
|
190
|
-
* Admin creator component for the "Listen and Drag Objects into Scene" question type.
|
|
191
|
-
* Allows uploading a background scene, adding object images in sidebar,
|
|
192
|
-
* defining drop zones on the scene, and selecting the correct object+dropzone pair.
|
|
193
|
-
*
|
|
194
|
-
* UI/UX improvements:
|
|
195
|
-
* - Consolidated from 5 cards to 3 cards (Group Data + Question Details + Explanation)
|
|
196
|
-
* - Side-by-side scene preview + drop zone editor panel
|
|
197
|
-
* - Image thumbnails in correct answer selection
|
|
198
|
-
* - Color-coded drop zone indicators
|
|
199
|
-
* - Shared group badges on all group sections
|
|
200
|
-
* - Delete confirmation for objects and drop zones
|
|
201
|
-
* - Enlarged object image previews
|
|
202
|
-
* - Answer summary card
|
|
203
|
-
*/
|
|
204
|
-
function ListenAndDragObjectsIntoSceneCreatorContent({ initialData, onSave, onSaveToQuestionBank, onCancel, onChange, onUnsavedChangesChange, validationRef, externalErrors, groups, questionIndexInPart = 0, partId = '', }) {
|
|
205
|
-
console.log('[ListenAndDragObjectsIntoScene_Creator] RENDER', { initialData, questionIndexInPart, partId, groups });
|
|
206
|
-
// Get examId from global exam store for audio upload API
|
|
207
|
-
const examId = useExamQuestionStore((state) => state.examId);
|
|
208
|
-
// ============ SHARED GROUP DATA (via useSceneQuestionGroup hook) ============
|
|
209
|
-
const sceneGroupInitialData = {
|
|
210
|
-
backgroundImageUrl: initialData?.backgroundImageUrl || '',
|
|
211
|
-
objectNodes: initialData?.objectNodes || [],
|
|
212
|
-
dropZones: initialData?.dropZones || [],
|
|
213
|
-
};
|
|
214
|
-
const { isGrouped, currentGroupNumber, backgroundImageUrl, setBackgroundImageUrl, objectNodes, setObjectNodes, dropZones, setDropZones, } = useSceneQuestionGroup({
|
|
215
|
-
partId,
|
|
216
|
-
groups,
|
|
217
|
-
questionIndexInPart,
|
|
218
|
-
initialData: sceneGroupInitialData,
|
|
219
|
-
});
|
|
220
|
-
// ============ STATE ============
|
|
221
|
-
// Question-specific data
|
|
222
|
-
const [audioUrl, setAudioUrl] = useState(initialData?.audioUrl || '');
|
|
223
|
-
const [correctObjectId, setCorrectObjectId] = useState(initialData?.correctObjectId !== undefined && initialData?.correctObjectId !== null
|
|
224
|
-
? String(initialData.correctObjectId)
|
|
225
|
-
: '');
|
|
226
|
-
const [correctDropZoneId, setCorrectDropZoneId] = useState(initialData?.correctDropZoneId !== undefined && initialData?.correctDropZoneId !== null
|
|
227
|
-
? String(initialData.correctDropZoneId)
|
|
228
|
-
: '');
|
|
229
|
-
const [points, setPoints] = useState(initialData?.points);
|
|
230
|
-
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
231
|
-
const [isBackup, setIsBackup] = useState(initialData?.isBackup || false);
|
|
232
|
-
// UI State
|
|
233
|
-
const [isClientMode, setIsClientMode] = useState(false);
|
|
234
|
-
const [backgroundPreviewUrl, setBackgroundPreviewUrl] = useState('');
|
|
235
|
-
const [audioPreviewUrl, setAudioPreviewUrl] = useState('');
|
|
236
|
-
const [activeDropZoneId, setActiveDropZoneId] = useState(null);
|
|
237
|
-
const [isGroupDataCollapsed, setIsGroupDataCollapsed] = useState(false);
|
|
238
|
-
// Use react-hook-form with Zod validation
|
|
239
|
-
const form = useReactHookForm({
|
|
240
|
-
defaultValues: {
|
|
241
|
-
audioUrl,
|
|
242
|
-
backgroundImageUrl,
|
|
243
|
-
objectNodes,
|
|
244
|
-
dropZones,
|
|
245
|
-
correctObjectId,
|
|
246
|
-
correctDropZoneId,
|
|
247
|
-
isBackup,
|
|
248
|
-
},
|
|
249
|
-
zodSchema: listenAndDragObjectsIntoSceneFormSchema,
|
|
250
|
-
mode: 'onBlur',
|
|
251
|
-
});
|
|
252
|
-
// Refs
|
|
253
|
-
const previousInitialDataRef = useRef(initialData);
|
|
254
|
-
const isInitializingRef = useRef(true);
|
|
255
|
-
const onChangeRef = useRef(onChange);
|
|
256
|
-
const hasInitializedRef = useRef(false);
|
|
257
|
-
// Presigned URLs
|
|
258
|
-
const { previewUrl: presignedBgUrl } = usePresignedFileUrl(backgroundImageUrl);
|
|
259
|
-
const { previewUrl: presignedAudioUrl } = usePresignedFileUrl(audioUrl);
|
|
260
|
-
useEffect(() => {
|
|
261
|
-
if (presignedBgUrl)
|
|
262
|
-
setBackgroundPreviewUrl(presignedBgUrl);
|
|
263
|
-
}, [presignedBgUrl]);
|
|
264
|
-
useEffect(() => {
|
|
265
|
-
if (presignedAudioUrl)
|
|
266
|
-
setAudioPreviewUrl(presignedAudioUrl);
|
|
267
|
-
}, [presignedAudioUrl]);
|
|
268
|
-
// Update onChange ref
|
|
269
|
-
useEffect(() => {
|
|
270
|
-
onChangeRef.current = onChange;
|
|
271
|
-
}, [onChange]);
|
|
272
|
-
// Debounced onChange
|
|
273
|
-
const debouncedOnChange = useDebouncedCallback((data) => {
|
|
274
|
-
onChangeRef.current?.(data);
|
|
275
|
-
}, 300);
|
|
276
|
-
// Expose validation function via ref
|
|
277
|
-
useEffect(() => {
|
|
278
|
-
if (validationRef) {
|
|
279
|
-
validationRef.current = {
|
|
280
|
-
triggerValidation: async () => {
|
|
281
|
-
return validateForm();
|
|
282
|
-
},
|
|
283
|
-
getFormData: () => ({
|
|
284
|
-
audioUrl,
|
|
285
|
-
backgroundImageUrl,
|
|
286
|
-
objectNodes,
|
|
287
|
-
dropZones,
|
|
288
|
-
correctObjectId,
|
|
289
|
-
correctDropZoneId,
|
|
290
|
-
points,
|
|
291
|
-
explanation,
|
|
292
|
-
isBackup,
|
|
293
|
-
}),
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
}, [validationRef, audioUrl, backgroundImageUrl, objectNodes, dropZones, correctObjectId, correctDropZoneId, points, explanation, isBackup]);
|
|
297
|
-
// Sync question-specific state from initialData
|
|
298
|
-
useEffect(() => {
|
|
299
|
-
if (!initialData) {
|
|
300
|
-
if (!hasInitializedRef.current) {
|
|
301
|
-
hasInitializedRef.current = true;
|
|
302
|
-
isInitializingRef.current = false;
|
|
303
|
-
}
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
const initialDataChanged = JSON.stringify(previousInitialDataRef.current) !== JSON.stringify(initialData);
|
|
307
|
-
if (!initialDataChanged && hasInitializedRef.current)
|
|
308
|
-
return;
|
|
309
|
-
previousInitialDataRef.current = initialData;
|
|
310
|
-
const newAudioUrl = initialData?.audioUrl || '';
|
|
311
|
-
const newCorrectObjectId = initialData?.correctObjectId !== undefined && initialData?.correctObjectId !== null ? String(initialData.correctObjectId) : '';
|
|
312
|
-
const newCorrectDropZoneId = initialData?.correctDropZoneId !== undefined && initialData?.correctDropZoneId !== null ? String(initialData.correctDropZoneId) : '';
|
|
313
|
-
const newPoints = initialData?.points || 1;
|
|
314
|
-
const newExplanation = initialData?.explanation || '';
|
|
315
|
-
const newIsBackup = initialData?.isBackup || false;
|
|
316
|
-
isInitializingRef.current = true;
|
|
317
|
-
hasInitializedRef.current = true;
|
|
318
|
-
setAudioUrl(newAudioUrl);
|
|
319
|
-
setCorrectObjectId(newCorrectObjectId);
|
|
320
|
-
setCorrectDropZoneId(newCorrectDropZoneId);
|
|
321
|
-
setPoints(newPoints);
|
|
322
|
-
setExplanation(newExplanation);
|
|
323
|
-
setIsBackup(newIsBackup);
|
|
324
|
-
setTimeout(() => {
|
|
325
|
-
isInitializingRef.current = false;
|
|
326
|
-
}, 150);
|
|
327
|
-
}, [initialData]);
|
|
328
|
-
// Call onChange when form data changes
|
|
329
|
-
useEffect(() => {
|
|
330
|
-
if (isInitializingRef.current || !hasInitializedRef.current)
|
|
331
|
-
return;
|
|
332
|
-
const payload = {
|
|
333
|
-
audioUrl,
|
|
334
|
-
backgroundImageUrl,
|
|
335
|
-
objectNodes,
|
|
336
|
-
dropZones,
|
|
337
|
-
correctObjectId,
|
|
338
|
-
correctDropZoneId,
|
|
339
|
-
points,
|
|
340
|
-
explanation,
|
|
341
|
-
isBackup,
|
|
342
|
-
groups,
|
|
343
|
-
groupNumber: currentGroupNumber,
|
|
344
|
-
};
|
|
345
|
-
debouncedOnChange(payload);
|
|
346
|
-
}, [audioUrl, backgroundImageUrl, objectNodes, dropZones, correctObjectId, correctDropZoneId, points, explanation, isBackup, groups, currentGroupNumber, debouncedOnChange]);
|
|
347
|
-
// ============ VALIDATION ============
|
|
348
|
-
const validateForm = async () => {
|
|
349
|
-
form.setValue('audioUrl', audioUrl);
|
|
350
|
-
form.setValue('backgroundImageUrl', backgroundImageUrl);
|
|
351
|
-
form.setValue('objectNodes', objectNodes);
|
|
352
|
-
form.setValue('dropZones', dropZones);
|
|
353
|
-
form.setValue('correctObjectId', String(correctObjectId));
|
|
354
|
-
form.setValue('correctDropZoneId', String(correctDropZoneId));
|
|
355
|
-
form.setValue('isBackup', isBackup);
|
|
356
|
-
const isValid = await form.trigger();
|
|
357
|
-
return isValid;
|
|
358
|
-
};
|
|
359
|
-
// ============ OBJECT NODE HANDLERS ============
|
|
360
|
-
const addObjectNode = () => {
|
|
361
|
-
const newNode = {
|
|
362
|
-
id: generateObjectId(),
|
|
363
|
-
imageUrl: '',
|
|
364
|
-
label: '',
|
|
365
|
-
};
|
|
366
|
-
setObjectNodes([...objectNodes, newNode]);
|
|
367
|
-
form.clearErrors();
|
|
368
|
-
};
|
|
369
|
-
const updateObjectNode = (id, updates) => {
|
|
370
|
-
setObjectNodes(objectNodes.map((node) => (node.id === id ? { ...node, ...updates } : node)));
|
|
371
|
-
form.clearErrors();
|
|
372
|
-
};
|
|
373
|
-
const removeObjectNode = (id) => {
|
|
374
|
-
setObjectNodes(objectNodes.filter((node) => node.id !== id));
|
|
375
|
-
if (correctObjectId === id) {
|
|
376
|
-
setCorrectObjectId('');
|
|
377
|
-
}
|
|
378
|
-
form.clearErrors();
|
|
379
|
-
};
|
|
380
|
-
// ============ DROP ZONE HANDLERS ============
|
|
381
|
-
const addDropZone = () => {
|
|
382
|
-
const newZone = {
|
|
383
|
-
id: generateObjectId(),
|
|
384
|
-
label: '',
|
|
385
|
-
x: 20 + (dropZones.length * 20) % 60,
|
|
386
|
-
y: 30 + (dropZones.length * 15) % 40,
|
|
387
|
-
width: 12,
|
|
388
|
-
height: 12,
|
|
389
|
-
};
|
|
390
|
-
setDropZones([...dropZones, newZone]);
|
|
391
|
-
form.clearErrors();
|
|
392
|
-
};
|
|
393
|
-
const updateDropZone = (id, updates) => {
|
|
394
|
-
setDropZones(dropZones.map((zone) => (zone.id === id ? { ...zone, ...updates } : zone)));
|
|
395
|
-
form.clearErrors();
|
|
396
|
-
};
|
|
397
|
-
const removeDropZone = (id) => {
|
|
398
|
-
setDropZones(dropZones.filter((zone) => zone.id !== id));
|
|
399
|
-
if (correctDropZoneId === id) {
|
|
400
|
-
setCorrectDropZoneId('');
|
|
401
|
-
}
|
|
402
|
-
if (activeDropZoneId === id) {
|
|
403
|
-
setActiveDropZoneId(null);
|
|
404
|
-
}
|
|
405
|
-
form.clearErrors();
|
|
406
|
-
};
|
|
407
|
-
const handleZoneDrag = useCallback((id, x, y) => {
|
|
408
|
-
setDropZones((prev) => prev.map((zone) => (zone.id === id ? { ...zone, x, y } : zone)));
|
|
409
|
-
}, []);
|
|
410
|
-
const handleZoneResize = useCallback((id, width, height) => {
|
|
411
|
-
setDropZones((prev) => prev.map((zone) => (zone.id === id ? { ...zone, width, height } : zone)));
|
|
412
|
-
}, []);
|
|
413
|
-
// ============ DERIVED DATA ============
|
|
414
|
-
const selectedObject = objectNodes.find((n) => n.id === correctObjectId);
|
|
415
|
-
const selectedDropZone = dropZones.find((z) => z.id === correctDropZoneId);
|
|
416
|
-
const selectedDropZoneIndex = dropZones.findIndex((z) => z.id === correctDropZoneId);
|
|
417
|
-
// ============ RENDER ============
|
|
418
|
-
// Preview mode
|
|
419
|
-
if (isClientMode) {
|
|
420
|
-
return (_jsxs("div", { className: "space-y-6", children: [_jsx(Card, { children: _jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { children: "Nghe v\u00E0 k\u00E9o th\u1EA3 \u0111\u1ED1i t\u01B0\u1EE3ng (Preview)" }), _jsx("p", { className: "mt-1 text-sm text-gray-500", children: "Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc - Hi\u1EC3n th\u1ECB \u0111\u00E1p \u00E1n \u0111\u00FAng" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Quay l\u1EA1i ch\u1EC9nh s\u1EEDa"] })] }) }) }), _jsx(ListenAndDragObjectsIntoSceneClient, { questionData: {
|
|
421
|
-
audioUrl: audioPreviewUrl || audioUrl,
|
|
422
|
-
backgroundImageUrl: backgroundPreviewUrl || backgroundImageUrl,
|
|
423
|
-
objectNodes,
|
|
424
|
-
dropZones,
|
|
425
|
-
}, isReviewMode: true, userAnswer: correctObjectId && correctDropZoneId ? { objectId: correctObjectId, dropZoneId: correctDropZoneId } : undefined, correctAnswer: correctObjectId && correctDropZoneId ? { objectId: correctObjectId, dropZoneId: correctDropZoneId } : undefined, explanation: explanation })] }));
|
|
426
|
-
}
|
|
427
|
-
// Creator mode
|
|
428
|
-
return (_jsxs("div", { className: "space-y-6", children: [_jsx(CreatorGuide, { colorScheme: "rose", steps: [
|
|
429
|
-
{ text: 'Upload hình nền (Scene) chung cho nhóm câu hỏi' },
|
|
430
|
-
{ text: 'Thêm các đối tượng kéo-thả (draggable objects) với hình ảnh và tên' },
|
|
431
|
-
{ text: 'Thiết lập vùng thả (drop zones) trên hình nền — vị trí mà học sinh cần kéo đối tượng vào' },
|
|
432
|
-
{ text: 'Upload audio hướng dẫn cho từng câu' },
|
|
433
|
-
{ text: 'Ghép đúng đối tượng với vùng thả tương ứng', highlight: true },
|
|
434
|
-
], tips: ['Vùng thả nên đủ lớn và rõ ràng, tránh chồng chéo'] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-rose-100/50", children: [_jsx("div", { className: "h-1.5 bg-gradient-to-r from-rose-500 via-pink-500 to-fuchsia-500" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-rose-500 to-pink-600 shadow-md", children: _jsx(ListChecks, { className: "h-5 w-5 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-xl font-bold text-gray-900", children: "Nh\u00F3m \u0111\u00E1p \u00E1n" }), _jsxs("div", { className: "mt-1.5 inline-flex items-center gap-1.5 rounded-full bg-rose-100 px-3 py-1 text-sm font-medium text-rose-700", children: [_jsx("span", { children: "\uD83D\uDDBC\uFE0F" }), _jsxs("span", { children: ["Nh\u00F3m ", currentGroupNumber] }), _jsx("span", { className: "text-xs font-normal opacity-75 ml-1", children: "(D\u00F9ng chung)" })] })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: !backgroundImageUrl || objectNodes.length < 2 || dropZones.length < 1, className: "gap-2 border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700", children: [_jsx(Eye, { className: "h-4 w-4" }), "Preview"] })] }) })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-rose-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-rose-400 to-pink-400" }), _jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center justify-between", 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-rose-100", children: _jsx(ImageIcon, { className: "h-3.5 w-3.5 text-rose-600" }) }), _jsx(CardTitle, { className: "text-lg font-semibold text-gray-800", children: "\u1EA2nh n\u1EC1n & \u0110\u1ED1i t\u01B0\u1EE3ng k\u00E9o th\u1EA3" })] }), _jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setIsGroupDataCollapsed(!isGroupDataCollapsed), className: "gap-1 text-gray-500 hover:text-gray-700", children: [isGroupDataCollapsed ? _jsx(ChevronDown, { className: "h-4 w-4" }) : _jsx(ChevronUp, { className: "h-4 w-4" }), isGroupDataCollapsed ? 'Mở rộng' : 'Thu gọn'] })] }) }), !isGroupDataCollapsed && (_jsxs(CardContent, { className: "space-y-6", children: [_jsx("div", { className: "rounded-xl border-2 p-5 bg-gradient-to-r from-orange-50 to-amber-50 border-orange-200", children: _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-orange-800", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-orange-600" }), "\u1EA2nh n\u1EC1n (Scene) ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(FileUpload, { id: "drag-scene-background", label: "", accept: "image/*", value: backgroundImageUrl, onChange: (url) => {
|
|
435
|
-
setBackgroundImageUrl(url);
|
|
436
|
-
form.clearErrors();
|
|
437
|
-
}, onPresignedUrlChange: setBackgroundPreviewUrl, maxSize: 10, placeholder: "Upload h\u00ECnh \u1EA3nh n\u1EC1n h\u00ECnh \u1EA3nh n\u1EC1n", autoUpload: true, prefix: "questions/scenes" }), form.formState.errors.backgroundImageUrl && (_jsx("p", { className: "mt-1 text-sm text-red-500", children: form.formState.errors.backgroundImageUrl.message }))] }) }), backgroundPreviewUrl && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-gray-50 overflow-hidden isolate", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 px-4 py-2.5 bg-white", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-blue-600" }), _jsx("span", { className: "text-sm font-semibold text-gray-700", children: "H\u00ECnh \u1EA3nh n\u1EC1n" }), _jsx(SharedGroupBadge, { groupNumber: currentGroupNumber })] }), _jsx("div", { className: "text-xs text-gray-500 font-medium", children: "K\u00E9o th\u1EA3 v\u1ECB tr\u00ED \u0111\u1EC3 \u0111\u1ECBnh v\u1ECB tr\u00EAn \u1EA3nh n\u1EC1n" })] }), _jsx("div", { className: "p-3 w-full", children: _jsx(DropZonePreviewWithDrag, { backgroundUrl: backgroundPreviewUrl, dropZones: dropZones, onZoneDrag: handleZoneDrag, onZoneResize: handleZoneResize, activeZoneId: activeDropZoneId, onSelectZone: setActiveDropZoneId }) })] })), !backgroundPreviewUrl && (_jsx("div", { className: "rounded-xl border border-gray-200 bg-gray-50/50 p-6", children: _jsxs("div", { className: "flex flex-col items-center gap-2 text-center", children: [_jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-full bg-gray-100", children: _jsx(Target, { className: "h-5 w-5 text-gray-400" }) }), _jsx("p", { className: "text-sm text-gray-500", children: "Upload \u1EA3nh n\u1EC1n tr\u01B0\u1EDBc \u0111\u1EC3 thi\u1EBFt l\u1EADp v\u1ECB tr\u00ED tr\u00EAn h\u00ECnh \u1EA3nh n\u1EC1n." })] }) })), backgroundPreviewUrl && (_jsxs("div", { className: "pt-4 border-t border-gray-100", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-blue-100", children: _jsx(Target, { className: "h-3.5 w-3.5 text-blue-600" }) }), _jsxs("h3", { className: "text-base font-semibold text-gray-800", children: ["V\u1ECB tr\u00ED (", dropZones.length, " items)"] }), _jsx(SharedGroupBadge, { groupNumber: currentGroupNumber })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: addDropZone, className: "gap-2 border-blue-200 text-blue-600 hover:bg-blue-50 hover:text-blue-700", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm v\u1ECB tr\u00ED"] })] }), _jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: dropZones.length === 0 ? (_jsxs("div", { className: "col-span-full flex flex-col items-center gap-3 rounded-xl border-2 border-dashed border-gray-200 bg-gray-50/50 py-10", children: [_jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-gray-100", children: _jsx(Target, { className: "h-6 w-6 text-gray-400" }) }), _jsx("p", { className: "text-sm text-gray-500", children: "Ch\u01B0a c\u00F3 v\u1ECB tr\u00ED n\u00E0o. Nh\u1EA5n \u201CTh\u00EAm v\u1ECB tr\u00ED\u201D \u0111\u1EC3 b\u1EAFt \u0111\u1EA7u." })] })) : (dropZones.map((zone, index) => (_jsx(DropZoneEditor, { zone: zone, index: index, onUpdate: updateDropZone, onRemove: removeDropZone, isActive: activeDropZoneId === zone.id, onSelect: setActiveDropZoneId }, zone.id)))) }), form.formState.errors.dropZones && (_jsx("div", { className: "col-span-full mt-2 rounded-lg border border-red-200 bg-red-50 p-3", children: _jsx("p", { className: "text-sm text-red-500", children: form.formState.errors.dropZones.message || "Vui lòng nhập đầy đủ thông tin cho các vị trí." }) }))] })), _jsxs("div", { className: "pt-4 border-t border-gray-100", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-amber-100", children: _jsx(Move, { className: "h-3.5 w-3.5 text-amber-600" }) }), _jsxs("h3", { className: "text-base font-semibold text-gray-800", children: ["\u0110\u1ED1i t\u01B0\u1EE3ng (", objectNodes.length, " items)"] }), _jsx(SharedGroupBadge, { groupNumber: currentGroupNumber })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: addObjectNode, className: "gap-2 border-green-200 text-green-600 hover:bg-green-50 hover:text-green-700", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm \u0111\u1ED1i t\u01B0\u1EE3ng"] })] }), _jsxs("div", { className: "space-y-3", children: [objectNodes.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center gap-3 rounded-xl border-2 border-dashed border-gray-200 bg-gray-50/50 py-10", children: [_jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-gray-100", children: _jsx(ImageIcon, { className: "h-6 w-6 text-gray-400" }) }), _jsx("p", { className: "text-sm text-gray-500", children: "Ch\u01B0a c\u00F3 \u0111\u1ED1i t\u01B0\u1EE3ng n\u00E0o. Nh\u1EA5n \u201CTh\u00EAm \u0111\u1ED1i t\u01B0\u1EE3ng\u201D \u0111\u1EC3 t\u1EA1o c\u00E1c \u0111\u1ED1i t\u01B0\u1EE3ng \u1EDF sidebar." })] })) : (objectNodes.map((node, index) => (_jsx(DragObjectEditor, { node: node, index: index, onUpdate: updateObjectNode, onRemove: removeObjectNode }, node.id)))), form.formState.errors.objectNodes && (_jsx("p", { className: "mt-2 text-sm text-red-500", children: form.formState.errors.objectNodes.message || "Vui lòng nhập đầy đủ thông tin cho các đối tượng." }))] })] })] }))] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-green-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-green-400 to-emerald-400" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-green-100", children: _jsx(Volume2, { className: "h-4 w-4 text-green-600" }) }), _jsx(CardTitle, { className: "text-lg font-semibold text-gray-800", children: "Th\u00F4ng tin c\u00E2u h\u1ECFi" })] }) }), _jsxs(CardContent, { className: "space-y-6 pt-2", children: [_jsx("div", { className: "rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4", children: _jsxs("div", { className: "flex flex-col md:flex-row gap-6", children: [_jsx("div", { className: "flex-1", children: _jsx(PointsInput, { value: points, onChange: (e) => setPoints(parseFloat(e.target.value) || 1) }) }), _jsx("div", { className: "flex-1 flex flex-col justify-center pt-2" })] }) }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Volume2, { className: "h-4 w-4 text-indigo-500" }), "Audio c\u00E2u h\u1ECFi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(FileUpload, { id: "drag-scene-audio", label: "", accept: "audio/*", value: audioUrl, onChange: (url) => {
|
|
438
|
-
setAudioUrl(url);
|
|
439
|
-
form.clearErrors();
|
|
440
|
-
}, onPresignedUrlChange: setAudioPreviewUrl, maxSize: 10, placeholder: "Upload file audio c\u00E2u h\u1ECFi", autoUpload: true, prefix: "questions/audio", examId: examId || undefined, partId: partId || undefined }), form.formState.errors.audioUrl && (_jsx("p", { className: "mt-1 text-sm text-red-500", children: form.formState.errors.audioUrl.message })), audioPreviewUrl && (_jsx("audio", { controls: true, className: "mt-2 w-full", src: audioPreviewUrl, children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." }))] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-green-100", children: _jsx(Lightbulb, { className: "h-3.5 w-3.5 text-green-600" }) }), _jsxs(Label, { className: "text-base font-semibold text-gray-800", children: ["\u0110\u1ED1i t\u01B0\u1EE3ng \u0111\u00FAng ", _jsx("span", { className: "text-red-500", children: "*" })] })] }), objectNodes.length === 0 ? (_jsx("p", { className: "text-sm text-amber-600 italic", children: "Vui l\u00F2ng th\u00EAm \u0111\u1ED1i t\u01B0\u1EE3ng \u1EDF ph\u1EA7n d\u1EEF li\u1EC7u nh\u00F3m tr\u01B0\u1EDBc khi ch\u1ECDn \u0111\u00E1p \u00E1n." })) : (_jsx(RadioGroup, { value: correctObjectId, onValueChange: (val) => {
|
|
441
|
-
setCorrectObjectId(val);
|
|
442
|
-
form.clearErrors();
|
|
443
|
-
}, className: "grid gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4", children: objectNodes.map((node, index) => (_jsxs("label", { className: `flex cursor-pointer items-center gap-3 rounded-lg border-2 p-3 transition-all ${correctObjectId === node.id
|
|
444
|
-
? 'border-green-500 bg-green-50 ring-1 ring-green-500 shadow-sm'
|
|
445
|
-
: 'border-gray-200 hover:border-green-300 hover:bg-green-50/50'}`, children: [_jsx(RadioGroupItem, { value: node.id, id: `correct-obj-${node.id}`, className: "flex-shrink-0" }), _jsx("div", { className: "h-12 w-12 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 bg-white", children: node.imageUrl ? (_jsx(PresignedImage, { url: node.imageUrl, alt: node.label })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center", children: _jsx(ImageIcon, { className: "h-4 w-4 text-gray-300" }) })) }), _jsx("div", { className: "min-w-0 flex-1", children: _jsxs("p", { className: "font-medium text-gray-900 text-sm line-clamp-2 break-all", children: [index + 1, ". ", node.label || '(Chưa đặt tên)'] }) })] }, node.id))) })), form.formState.errors.correctObjectId && (_jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-3 mt-2", children: _jsxs("p", { className: "text-sm text-red-700", children: ["\u26A0\uFE0F ", form.formState.errors.correctObjectId.message] }) }))] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-blue-100", children: _jsx(Target, { className: "h-3.5 w-3.5 text-blue-600" }) }), _jsxs(Label, { className: "text-base font-semibold text-gray-800", children: ["V\u1ECB tr\u00ED \u0111\u00FAng ", _jsx("span", { className: "text-red-500", children: "*" })] })] }), dropZones.length === 0 ? (_jsx("p", { className: "text-sm text-amber-600 italic", children: "Vui l\u00F2ng th\u00EAm v\u1ECB tr\u00ED \u1EDF ph\u1EA7n d\u1EEF li\u1EC7u nh\u00F3m tr\u01B0\u1EDBc khi ch\u1ECDn \u0111\u00E1p \u00E1n." })) : (_jsx(RadioGroup, { value: correctDropZoneId, onValueChange: (val) => {
|
|
446
|
-
setCorrectDropZoneId(val);
|
|
447
|
-
form.clearErrors();
|
|
448
|
-
}, className: "grid gap-3 sm:grid-cols-2 md:grid-cols-3", children: dropZones.map((zone, index) => {
|
|
449
|
-
const color = getZoneColor(index);
|
|
450
|
-
return (_jsxs("label", { className: `flex cursor-pointer items-center gap-3 rounded-lg border-2 p-3 transition-all ${correctDropZoneId === zone.id
|
|
451
|
-
? 'border-blue-500 bg-blue-50 ring-1 ring-blue-500 shadow-sm'
|
|
452
|
-
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50/50'}`, children: [_jsx(RadioGroupItem, { value: zone.id, id: `correct-dz-${zone.id}`, className: "flex-shrink-0" }), _jsx("div", { className: `flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md ${color.dot} text-white shadow-sm`, children: _jsx(MapPin, { className: "h-4 w-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("p", { className: "font-medium text-gray-900 text-sm line-clamp-1 break-all", children: [index + 1, ". ", zone.label || '(Chưa đặt nhãn)'] }), _jsxs("p", { className: "text-[11px] text-gray-400 mt-0.5", children: ["V\u1ECB tr\u00ED: (", zone.x, "%, ", zone.y, "%) \u2014 ", zone.width, "\u00D7", zone.height, "%"] })] })] }, zone.id));
|
|
453
|
-
}) })), form.formState.errors.correctDropZoneId && (_jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-3 mt-2", children: _jsxs("p", { className: "text-sm text-red-700", children: ["\u26A0\uFE0F ", form.formState.errors.correctDropZoneId.message] }) }))] }), selectedObject && selectedDropZone && (_jsxs("div", { className: "rounded-xl border-2 border-green-200 bg-gradient-to-r from-green-50 to-emerald-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx("div", { className: "flex h-5 w-5 items-center justify-center rounded-full bg-green-500 text-white", children: _jsx(Lightbulb, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-sm font-semibold text-green-800", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" })] }), _jsxs("div", { className: "flex items-center gap-3 rounded-lg bg-white/80 p-3 border border-green-100", children: [_jsx("div", { className: "h-14 w-14 flex-shrink-0 overflow-hidden rounded-lg border-2 border-green-300 bg-white", children: selectedObject.imageUrl ? (_jsx(PresignedImage, { url: selectedObject.imageUrl, alt: selectedObject.label })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center", children: _jsx(ImageIcon, { className: "h-5 w-5 text-gray-300" }) })) }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-semibold text-gray-900 text-sm", children: selectedObject.label || '(Chưa đặt tên)' }), _jsx("p", { className: "text-xs text-gray-500", children: "\u0110\u1ED1i t\u01B0\u1EE3ng" })] }), _jsx(ArrowRight, { className: "h-5 w-5 text-green-500 flex-shrink-0 mx-1" }), _jsx("div", { className: `flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg ${selectedDropZoneIndex >= 0 ? getZoneColor(selectedDropZoneIndex).dot : 'bg-blue-500'} text-white shadow-sm`, children: _jsx(MapPin, { className: "h-5 w-5" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-semibold text-gray-900 text-sm", children: selectedDropZone.label || '(Chưa đặt nhãn)' }), _jsxs("p", { className: "text-xs text-gray-500", children: ["V\u1ECB tr\u00ED (", selectedDropZone.x, "%, ", selectedDropZone.y, "%)"] })] })] })] })), (!correctObjectId || !correctDropZoneId) && objectNodes.length > 0 && dropZones.length > 0 && (_jsxs("div", { className: "flex items-start gap-2.5 rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx(AlertTriangle, { className: "h-4 w-4 text-amber-500 flex-shrink-0 mt-0.5" }), _jsx("p", { className: "text-sm text-amber-700", children: "Vui l\u00F2ng ch\u1ECDn c\u1EA3 \u0111\u1ED1i t\u01B0\u1EE3ng \u0111\u00FAng v\u00E0 v\u1ECB tr\u00ED \u0111\u00FAng cho c\u00E2u h\u1ECFi n\u00E0y" })] }))] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-amber-400 to-orange-400" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 shadow-sm", children: _jsx(BookOpen, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-lg font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" }), _jsx("span", { className: "inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2.5 py-0.5 text-xs font-medium text-amber-600", children: "Kh\u00F4ng b\u1EAFt bu\u1ED9c" })] }), _jsx("p", { className: "text-sm text-gray-500 mt-0.5", children: "Gi\u1EA3i th\u00EDch cho \u0111\u00E1p \u00E1n \u0111\u00FAng \u2014 hi\u1EC3n th\u1ECB sau khi h\u1ECDc sinh ho\u00E0n th\u00E0nh c\u00E2u h\u1ECFi" })] })] }) }), _jsx(CardContent, { children: _jsx(Textarea, { value: explanation, onChange: (e) => setExplanation(e.target.value), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", rows: 3, className: "min-h-[80px] border-gray-200 bg-white transition-all focus:border-amber-300 focus:ring-amber-200 resize-none" }) })] })] }));
|
|
454
|
-
}
|
|
455
|
-
export function ListenAndDragObjectsIntoSceneCreator(props) {
|
|
456
|
-
return _jsx(ListenAndDragObjectsIntoSceneCreatorContent, { ...props });
|
|
457
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useState as t,useRef as a,useEffect as n,useCallback as l}from"react";import{Button as i}from"../../../../components/ui/button";import{Label as s}from"../../../../components/ui/label";import{Input as o}from"../../../../components/ui/input";import{Textarea as d}from"../../../../components/ui/textarea";import{Card as c,CardContent as m,CardHeader as h,CardTitle as g}from"../../../../components/ui/card";import{PointsInput as u}from"../../../../components/ui/points-input";import{RadioGroup as b,RadioGroupItem as p}from"../../../../components/ui/radio-group";import{Pencil as x,Eye as f,Plus as N,Trash2 as v,Image as w,Volume2 as y,Lightbulb as j,GripVertical as k,Target as I,Move as U,ChevronDown as C,ChevronUp as S,MapPin as Z,ArrowRight as D,AlertTriangle as z,ListChecks as V,BookOpen as E}from"lucide-react";import{FileUpload as O}from"../../../../components/ui/file-upload";import{usePresignedFileUrl as M,useDebouncedCallback as P}from"../../../../shared/lib/hooks";import{useReactHookForm as R}from"../../../../shared/lib/hooks/useReactHookForm";import{listenAndDragObjectsIntoSceneFormSchema as T}from"../../../../shared/lib/validation/listen-and-drag-objects-into-scene.validation";import{ListenAndDragObjectsIntoSceneClient as $}from"./ListenAndDragObjectsIntoSceneClient";import{useSceneQuestionGroup as B}from"../../_shared/hooks/useSceneQuestionGroup";import{generateObjectId as A}from"../../_shared/utils/id-generator";import{CreatorGuide as X}from"../../components/CreatorGuide";import{useExamQuestionStore as q}from"../../../../shared/lib/stores/examQuestionStore";const F=[{bg:"bg-blue-500",border:"border-blue-400",light:"bg-blue-100",text:"text-blue-700",dot:"bg-blue-500"},{bg:"bg-emerald-500",border:"border-emerald-400",light:"bg-emerald-100",text:"text-emerald-700",dot:"bg-emerald-500"},{bg:"bg-purple-500",border:"border-purple-400",light:"bg-purple-100",text:"text-purple-700",dot:"bg-purple-500"},{bg:"bg-rose-500",border:"border-rose-400",light:"bg-rose-100",text:"text-rose-700",dot:"bg-rose-500"},{bg:"bg-amber-500",border:"border-amber-400",light:"bg-amber-100",text:"text-amber-700",dot:"bg-amber-500"},{bg:"bg-cyan-500",border:"border-cyan-400",light:"bg-cyan-100",text:"text-cyan-700",dot:"bg-cyan-500"}];function G(e){return F[e%F.length]}function L({groupNumber:t}){return r("span",{className:"inline-flex items-center gap-1 rounded-full bg-orange-100 px-2.5 py-0.5 text-xs font-medium text-orange-700",children:[e("span",{children:"🔗"}),"Nhóm ",t," — Dùng chung"]})}function H({url:r,alt:t}){const{previewUrl:a}=M(r);return a?e("img",{src:a,alt:t,className:"h-full w-full object-contain pointer-events-none",draggable:!1}):e(w,{className:"h-4 w-4 text-gray-400"})}function Y({node:a,index:l,onUpdate:d,onRemove:c}){const[m,h]=t(""),[g,u]=t(!1),{previewUrl:b}=M(a.imageUrl||"");n(()=>{b&&h(b)},[b]);return e("div",{className:"rounded-lg border-2 border-gray-200 bg-white p-4 transition-all hover:border-gray-300 hover:shadow-sm",children:r("div",{className:"flex items-start gap-3",children:[r("div",{className:"flex flex-col items-center gap-1 pt-1",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-orange-500 to-amber-600 text-sm font-bold text-white shadow-sm",children:l+1}),e(k,{className:"h-4 w-4 text-gray-400"})]}),e("div",{className:"flex-shrink-0",children:e("div",m?{className:"h-20 w-20 overflow-hidden rounded-lg border-2 border-gray-200 bg-gray-50 transition-all hover:border-orange-300",children:e("img",{src:m,alt:a.label||"Object",className:"h-full w-full object-contain p-1"})}:{className:"flex h-20 w-20 items-center justify-center rounded-lg border-2 border-dashed border-gray-200 bg-gray-50",children:e(w,{className:"h-6 w-6 text-gray-300"})})}),r("div",{className:"flex-1 space-y-3",children:[r("div",{children:[e(s,{className:"text-xs font-medium text-gray-500",children:"Tên đối tượng"}),e(o,{value:a.label,onChange:e=>d(a.id,{label:e.target.value}),placeholder:"Ví dụ: lemon, kite, car...",className:"mt-1 border-gray-200"})]}),r("div",{children:[e(s,{className:"text-xs font-medium text-gray-500",children:"Hình ảnh đối tượng"}),e("div",{className:"mt-1",children:e(O,{id:`drag-object-image-${a.id}`,label:"",accept:"image/*",value:a.imageUrl||"",onChange:e=>d(a.id,{imageUrl:e}),onPresignedUrlChange:h,maxSize:5,placeholder:"Upload hình ảnh đối tượng",autoUpload:!0,prefix:"questions/objects"})})]})]}),r("div",{className:"flex flex-col items-center gap-1 pt-1",children:[e(i,{variant:"ghost",size:"sm",onClick:()=>{g?(c(a.id),u(!1)):(u(!0),setTimeout(()=>u(!1),3e3))},className:"transition-all "+(g?"bg-red-100 text-red-600 hover:bg-red-200 hover:text-red-700 ring-1 ring-red-300":"text-red-400 hover:bg-red-50 hover:text-red-600"),title:g?"Nhấn lần nữa để xác nhận xóa":"Xóa đối tượng",children:e(v,{className:"h-4 w-4"})}),g&&e("span",{className:"text-[10px] font-medium text-red-500 whitespace-nowrap",children:"Nhấn lại?"})]})]})})}function Q({zone:a,index:n,onUpdate:l,onRemove:d,isActive:c,onSelect:m}){const[h,g]=t(!1),u=G(n);return e("div",{className:"rounded-lg border-2 border-dashed p-3 transition-all cursor-pointer "+(c?"border-blue-400 bg-blue-50/50 shadow-sm ring-1 ring-blue-200":"border-gray-200 bg-white hover:border-blue-200 hover:bg-blue-50/20"),onClick:()=>m?.(a.id),children:r("div",{className:"flex items-start gap-2.5",children:[e("div",{className:"flex flex-col items-center gap-1 pt-1.5",children:e("div",{className:`h-6 w-6 rounded-md ${u.dot} flex items-center justify-center text-[10px] font-bold text-white shadow-sm`,children:n+1})}),r("div",{className:"flex-1 space-y-2",children:[e(o,{value:a.label,onChange:e=>l(a.id,{label:e.target.value}),placeholder:"Ví dụ: near the pond, on the tree...",className:"h-8 border-gray-200 text-sm",onClick:e=>e.stopPropagation()}),r("div",{className:"grid grid-cols-4 gap-1.5",children:[r("div",{children:[e(s,{className:"text-[10px] font-medium text-gray-400",children:"X%"}),e(o,{type:"number",min:0,max:100,value:a.x,onChange:e=>l(a.id,{x:parseFloat(e.target.value)||0}),className:"h-7 border-gray-200 text-xs px-1 text-center",onClick:e=>e.stopPropagation()})]}),r("div",{children:[e(s,{className:"text-[10px] font-medium text-gray-400",children:"Y%"}),e(o,{type:"number",min:0,max:100,value:a.y,onChange:e=>l(a.id,{y:parseFloat(e.target.value)||0}),className:"h-7 border-gray-200 text-xs px-1 text-center",onClick:e=>e.stopPropagation()})]}),r("div",{children:[e(s,{className:"text-[10px] font-medium text-gray-400",children:"W%"}),e(o,{type:"number",min:1,max:100,value:a.width,onChange:e=>l(a.id,{width:parseFloat(e.target.value)||10}),className:"h-7 border-gray-200 text-xs px-1 text-center",onClick:e=>e.stopPropagation()})]}),r("div",{children:[e(s,{className:"text-[10px] font-medium text-gray-400",children:"H%"}),e(o,{type:"number",min:1,max:100,value:a.height,onChange:e=>l(a.id,{height:parseFloat(e.target.value)||10}),className:"h-7 border-gray-200 text-xs px-1 text-center",onClick:e=>e.stopPropagation()})]})]})]}),r("div",{className:"flex flex-col items-center pt-1",children:[e(i,{variant:"ghost",size:"sm",onClick:e=>{e.stopPropagation(),h?(d(a.id),g(!1)):(g(!0),setTimeout(()=>g(!1),3e3))},className:"h-7 w-7 p-0 transition-all "+(h?"bg-red-100 text-red-600 hover:bg-red-200 ring-1 ring-red-300":"text-red-400 hover:bg-red-50 hover:text-red-600"),title:h?"Nhấn lần nữa để xác nhận xóa":"Xóa vị trí",children:e(v,{className:"h-3.5 w-3.5"})}),h&&e("span",{className:"text-[9px] font-medium text-red-500 mt-0.5",children:"Xóa?"})]})]})})}function _({backgroundUrl:i,dropZones:s,onZoneDrag:o,onZoneResize:d,activeZoneId:c,onSelectZone:m}){const h=a(null),[g,u]=t(null),[b,p]=t({x:0,y:0}),[x,f]=t(null),[N,v]=t(null),[w,y]=t({x:0,y:0}),[j,k]=t({width:0,height:0}),U=(e,r,t,a,n)=>{n.preventDefault(),n.stopPropagation(),f(e),v(r),y({x:n.clientX,y:n.clientY}),k({width:t,height:a})},C=l(e=>{if(h.current)if(g){const r=h.current.getBoundingClientRect(),t=(e.clientX-r.left)/r.width*100,a=(e.clientY-r.top)/r.height*100,n=Math.max(0,Math.min(100,t-b.x)),l=Math.max(0,Math.min(100,a-b.y));o(g,Math.round(10*n)/10,Math.round(10*l)/10)}else if(x&&N){const r=h.current.getBoundingClientRect(),t=(e.clientX-w.x)/r.width*100,a=(e.clientY-w.y)/r.height*100;let n=j.width,l=j.height;"e"!==N&&"se"!==N||(n=Math.max(1,j.width+t)),"s"!==N&&"se"!==N||(l=Math.max(1,j.height+a)),d(x,Math.round(10*n)/10,Math.round(10*l)/10)}},[g,x,N,w,j,b,o,d]),S=l(()=>{u(null),f(null),v(null)},[]);return n(()=>{if(g||x)return window.addEventListener("mousemove",C),window.addEventListener("mouseup",S),()=>{window.removeEventListener("mousemove",C),window.removeEventListener("mouseup",S)}},[g,x,C,S]),r("div",{ref:h,className:"relative w-full select-none",children:[e("img",{src:i,alt:"Scene background",className:"w-full rounded-md object-contain",draggable:!1}),s.map((t,a)=>{const n=g===t.id||x===t.id||c===t.id,l=G(a);return r("div",{className:"absolute cursor-move rounded-md border-2 border-dashed transition-colors group "+(n?`${l.border} bg-blue-200/30 shadow-lg z-20`:`${l.border} bg-blue-100/20 hover:bg-blue-200/30 z-10`),style:{left:`${t.x}%`,top:`${t.y}%`,width:`${t.width}%`,height:`${t.height}%`},onMouseDown:e=>((e,r,t,a)=>{if(a.preventDefault(),!a.target.dataset.resizeHandle&&(u(e),m?.(e),h.current)){const e=h.current.getBoundingClientRect(),n=(a.clientX-e.left)/e.width*100,l=(a.clientY-e.top)/e.height*100;p({x:n-r,y:l-t})}})(t.id,t.x,t.y,e),children:[r("div",{className:`absolute -top-5 left-0 rounded ${l.dot} px-1.5 py-0.5 text-[10px] font-medium text-white whitespace-nowrap shadow-sm`,children:[a+1,". ",t.label||`Zone ${a+1}`]}),e("div",{className:"flex h-full w-full items-center justify-center",children:e(I,{className:"h-4 w-4 text-blue-400/60"})}),e("div",{"data-resize-handle":"true",className:"absolute -right-1.5 top-1/2 h-3 w-3 -translate-y-1/2 cursor-e-resize rounded-full border border-blue-500 bg-white opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100",style:{opacity:n?1:void 0},onMouseDown:e=>U(t.id,"e",t.width,t.height,e)}),e("div",{"data-resize-handle":"true",className:"absolute -bottom-1.5 left-1/2 h-3 w-3 -translate-x-1/2 cursor-s-resize rounded-full border border-blue-500 bg-white opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100",style:{opacity:n?1:void 0},onMouseDown:e=>U(t.id,"s",t.width,t.height,e)}),e("div",{"data-resize-handle":"true",className:"absolute -bottom-1.5 -right-1.5 h-3 w-3 cursor-se-resize rounded-full border border-blue-500 bg-white opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100",style:{opacity:n?1:void 0},onMouseDown:e=>U(t.id,"se",t.width,t.height,e)})]},t.id)})]})}function J({initialData:o,onSave:v,onSaveToQuestionBank:k,onCancel:F,onChange:J,onUnsavedChangesChange:K,validationRef:W,externalErrors:ee,groups:re,questionIndexInPart:te=0,partId:ae=""}){console.log("[ListenAndDragObjectsIntoScene_Creator] RENDER",{initialData:o,questionIndexInPart:te,partId:ae,groups:re});const ne=q(e=>e.examId),le={backgroundImageUrl:o?.backgroundImageUrl||"",objectNodes:o?.objectNodes||[],dropZones:o?.dropZones||[]},{isGrouped:ie,currentGroupNumber:se,backgroundImageUrl:oe,setBackgroundImageUrl:de,objectNodes:ce,setObjectNodes:me,dropZones:he,setDropZones:ge}=B({partId:ae,groups:re,questionIndexInPart:te,initialData:le}),[ue,be]=t(o?.audioUrl||""),[pe,xe]=t(null!=o?.correctObjectId?String(o.correctObjectId):""),[fe,Ne]=t(null!=o?.correctDropZoneId?String(o.correctDropZoneId):""),[ve,we]=t(o?.points),[ye,je]=t(o?.explanation||""),[ke,Ie]=t(o?.isBackup||!1),[Ue,Ce]=t(!1),[Se,Ze]=t(""),[De,ze]=t(""),[Ve,Ee]=t(null),[Oe,Me]=t(!1),Pe=R({defaultValues:{audioUrl:ue,backgroundImageUrl:oe,objectNodes:ce,dropZones:he,correctObjectId:pe,correctDropZoneId:fe,isBackup:ke},zodSchema:T,mode:"onBlur"}),Re=a(o),Te=a(!0),$e=a(J),Be=a(!1),{previewUrl:Ae}=M(oe),{previewUrl:Xe}=M(ue);n(()=>{Ae&&Ze(Ae)},[Ae]),n(()=>{Xe&&ze(Xe)},[Xe]),n(()=>{$e.current=J},[J]);const qe=P(e=>{$e.current?.(e)},300);n(()=>{W&&(W.current={triggerValidation:async()=>Fe(),getFormData:()=>({audioUrl:ue,backgroundImageUrl:oe,objectNodes:ce,dropZones:he,correctObjectId:pe,correctDropZoneId:fe,points:ve,explanation:ye,isBackup:ke})})},[W,ue,oe,ce,he,pe,fe,ve,ye,ke]),n(()=>{if(!o)return void(Be.current||(Be.current=!0,Te.current=!1));if(!(JSON.stringify(Re.current)!==JSON.stringify(o))&&Be.current)return;Re.current=o;const e=o?.audioUrl||"",r=null!=o?.correctObjectId?String(o.correctObjectId):"",t=null!=o?.correctDropZoneId?String(o.correctDropZoneId):"",a=o?.points||1,n=o?.explanation||"",l=o?.isBackup||!1;Te.current=!0,Be.current=!0,be(e),xe(r),Ne(t),we(a),je(n),Ie(l),setTimeout(()=>{Te.current=!1},150)},[o]),n(()=>{if(Te.current||!Be.current)return;qe({audioUrl:ue,backgroundImageUrl:oe,objectNodes:ce,dropZones:he,correctObjectId:pe,correctDropZoneId:fe,points:ve,explanation:ye,isBackup:ke,groups:re,groupNumber:se})},[ue,oe,ce,he,pe,fe,ve,ye,ke,re,se,qe]);const Fe=async()=>{Pe.setValue("audioUrl",ue),Pe.setValue("backgroundImageUrl",oe),Pe.setValue("objectNodes",ce),Pe.setValue("dropZones",he),Pe.setValue("correctObjectId",String(pe)),Pe.setValue("correctDropZoneId",String(fe)),Pe.setValue("isBackup",ke);return await Pe.trigger()},Ge=(e,r)=>{me(ce.map(t=>t.id===e?{...t,...r}:t)),Pe.clearErrors()},Le=e=>{me(ce.filter(r=>r.id!==e)),pe===e&&xe(""),Pe.clearErrors()},He=(e,r)=>{ge(he.map(t=>t.id===e?{...t,...r}:t)),Pe.clearErrors()},Ye=e=>{ge(he.filter(r=>r.id!==e)),fe===e&&Ne(""),Ve===e&&Ee(null),Pe.clearErrors()},Qe=l((e,r,t)=>{ge(a=>a.map(a=>a.id===e?{...a,x:r,y:t}:a))},[]),_e=l((e,r,t)=>{ge(a=>a.map(a=>a.id===e?{...a,width:r,height:t}:a))},[]),Je=ce.find(e=>e.id===pe),Ke=he.find(e=>e.id===fe),We=he.findIndex(e=>e.id===fe);return r("div",Ue?{className:"space-y-6",children:[e(c,{children:e(h,{children:r("div",{className:"flex items-center justify-between",children:[r("div",{children:[e(g,{children:"Nghe và kéo thả đối tượng (Preview)"}),e("p",{className:"mt-1 text-sm text-gray-500",children:"Chế độ xem trước - Hiển thị đáp án đúng"})]}),r(i,{variant:"outline",size:"sm",onClick:()=>Ce(!1),children:[e(x,{className:"mr-2 h-4 w-4"}),"Quay lại chỉnh sửa"]})]})})}),e($,{questionData:{audioUrl:De||ue,backgroundImageUrl:Se||oe,objectNodes:ce,dropZones:he},isReviewMode:!0,userAnswer:pe&&fe?{objectId:pe,dropZoneId:fe}:void 0,correctAnswer:pe&&fe?{objectId:pe,dropZoneId:fe}:void 0,explanation:ye})]}:{className:"space-y-6",children:[e(X,{colorScheme:"rose",steps:[{text:"Upload hình nền (Scene) chung cho nhóm câu hỏi"},{text:"Thêm các đối tượng kéo-thả (draggable objects) với hình ảnh và tên"},{text:"Thiết lập vùng thả (drop zones) trên hình nền — vị trí mà học sinh cần kéo đối tượng vào"},{text:"Upload audio hướng dẫn cho từng câu"},{text:"Ghép đúng đối tượng với vùng thả tương ứng",highlight:!0}],tips:["Vùng thả nên đủ lớn và rõ ràng, tránh chồng chéo"]}),r(c,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-rose-100/50",children:[e("div",{className:"h-1.5 bg-gradient-to-r from-rose-500 via-pink-500 to-fuchsia-500"}),e(h,{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-rose-500 to-pink-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:"Nhóm đáp án"}),r("div",{className:"mt-1.5 inline-flex items-center gap-1.5 rounded-full bg-rose-100 px-3 py-1 text-sm font-medium text-rose-700",children:[e("span",{children:"🖼️"}),r("span",{children:["Nhóm ",se]}),e("span",{className:"text-xs font-normal opacity-75 ml-1",children:"(Dùng chung)"})]})]})]}),r(i,{variant:"outline",size:"sm",onClick:()=>Ce(!0),disabled:!oe||ce.length<2||he.length<1,className:"gap-2 border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700",children:[e(f,{className:"h-4 w-4"}),"Preview"]})]})})]}),r(c,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-rose-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-rose-400 to-pink-400"}),e(h,{className:"pb-3",children:r("div",{className:"flex items-center justify-between",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-rose-100",children:e(w,{className:"h-3.5 w-3.5 text-rose-600"})}),e(g,{className:"text-lg font-semibold text-gray-800",children:"Ảnh nền & Đối tượng kéo thả"})]}),r(i,{variant:"ghost",size:"sm",onClick:()=>Me(!Oe),className:"gap-1 text-gray-500 hover:text-gray-700",children:[e(Oe?C:S,{className:"h-4 w-4"}),Oe?"Mở rộng":"Thu gọn"]})]})}),!Oe&&r(m,{className:"space-y-6",children:[e("div",{className:"rounded-xl border-2 p-5 bg-gradient-to-r from-orange-50 to-amber-50 border-orange-200",children:r("div",{className:"space-y-2",children:[r(s,{className:"flex items-center gap-2 text-base font-semibold text-orange-800",children:[e(w,{className:"h-4 w-4 text-orange-600"}),"Ảnh nền (Scene) ",e("span",{className:"text-red-500",children:"*"})]}),e(O,{id:"drag-scene-background",label:"",accept:"image/*",value:oe,onChange:e=>{de(e),Pe.clearErrors()},onPresignedUrlChange:Ze,maxSize:10,placeholder:"Upload hình ảnh nền hình ảnh nền",autoUpload:!0,prefix:"questions/scenes"}),Pe.formState.errors.backgroundImageUrl&&e("p",{className:"mt-1 text-sm text-red-500",children:Pe.formState.errors.backgroundImageUrl.message})]})}),Se&&r("div",{className:"rounded-xl border border-gray-200 bg-gray-50 overflow-hidden isolate",children:[r("div",{className:"flex items-center justify-between border-b border-gray-100 px-4 py-2.5 bg-white",children:[r("div",{className:"flex items-center gap-2",children:[e(w,{className:"h-4 w-4 text-blue-600"}),e("span",{className:"text-sm font-semibold text-gray-700",children:"Hình ảnh nền"}),e(L,{groupNumber:se})]}),e("div",{className:"text-xs text-gray-500 font-medium",children:"Kéo thả vị trí để định vị trên ảnh nền"})]}),e("div",{className:"p-3 w-full",children:e(_,{backgroundUrl:Se,dropZones:he,onZoneDrag:Qe,onZoneResize:_e,activeZoneId:Ve,onSelectZone:Ee})})]}),!Se&&e("div",{className:"rounded-xl border border-gray-200 bg-gray-50/50 p-6",children:r("div",{className:"flex flex-col items-center gap-2 text-center",children:[e("div",{className:"flex h-10 w-10 items-center justify-center rounded-full bg-gray-100",children:e(I,{className:"h-5 w-5 text-gray-400"})}),e("p",{className:"text-sm text-gray-500",children:"Upload ảnh nền trước để thiết lập vị trí trên hình ảnh nền."})]})}),Se&&r("div",{className:"pt-4 border-t border-gray-100",children:[r("div",{className:"flex items-center justify-between mb-4",children:[r("div",{className:"flex items-center gap-2",children:[e("div",{className:"flex h-6 w-6 items-center justify-center rounded-md bg-blue-100",children:e(I,{className:"h-3.5 w-3.5 text-blue-600"})}),r("h3",{className:"text-base font-semibold text-gray-800",children:["Vị trí (",he.length," items)"]}),e(L,{groupNumber:se})]}),r(i,{variant:"outline",size:"sm",onClick:()=>{const e={id:A(),label:"",x:20+20*he.length%60,y:30+15*he.length%40,width:12,height:12};ge([...he,e]),Pe.clearErrors()},className:"gap-2 border-blue-200 text-blue-600 hover:bg-blue-50 hover:text-blue-700",children:[e(N,{className:"h-4 w-4"}),"Thêm vị trí"]})]}),e("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-3",children:0===he.length?r("div",{className:"col-span-full flex flex-col items-center gap-3 rounded-xl border-2 border-dashed border-gray-200 bg-gray-50/50 py-10",children:[e("div",{className:"flex h-12 w-12 items-center justify-center rounded-full bg-gray-100",children:e(I,{className:"h-6 w-6 text-gray-400"})}),e("p",{className:"text-sm text-gray-500",children:"Chưa có vị trí nào. Nhấn “Thêm vị trí” để bắt đầu."})]}):he.map((r,t)=>e(Q,{zone:r,index:t,onUpdate:He,onRemove:Ye,isActive:Ve===r.id,onSelect:Ee},r.id))}),Pe.formState.errors.dropZones&&e("div",{className:"col-span-full mt-2 rounded-lg border border-red-200 bg-red-50 p-3",children:e("p",{className:"text-sm text-red-500",children:Pe.formState.errors.dropZones.message||"Vui lòng nhập đầy đủ thông tin cho các vị trí."})})]}),r("div",{className:"pt-4 border-t border-gray-100",children:[r("div",{className:"flex items-center justify-between mb-4",children:[r("div",{className:"flex items-center gap-2",children:[e("div",{className:"flex h-6 w-6 items-center justify-center rounded-md bg-amber-100",children:e(U,{className:"h-3.5 w-3.5 text-amber-600"})}),r("h3",{className:"text-base font-semibold text-gray-800",children:["Đối tượng (",ce.length," items)"]}),e(L,{groupNumber:se})]}),r(i,{variant:"outline",size:"sm",onClick:()=>{const e={id:A(),imageUrl:"",label:""};me([...ce,e]),Pe.clearErrors()},className:"gap-2 border-green-200 text-green-600 hover:bg-green-50 hover:text-green-700",children:[e(N,{className:"h-4 w-4"}),"Thêm đối tượng"]})]}),r("div",{className:"space-y-3",children:[0===ce.length?r("div",{className:"flex flex-col items-center gap-3 rounded-xl border-2 border-dashed border-gray-200 bg-gray-50/50 py-10",children:[e("div",{className:"flex h-12 w-12 items-center justify-center rounded-full bg-gray-100",children:e(w,{className:"h-6 w-6 text-gray-400"})}),e("p",{className:"text-sm text-gray-500",children:"Chưa có đối tượng nào. Nhấn “Thêm đối tượng” để tạo các đối tượng ở sidebar."})]}):ce.map((r,t)=>e(Y,{node:r,index:t,onUpdate:Ge,onRemove:Le},r.id)),Pe.formState.errors.objectNodes&&e("p",{className:"mt-2 text-sm text-red-500",children:Pe.formState.errors.objectNodes.message||"Vui lòng nhập đầy đủ thông tin cho các đối tượng."})]})]})]})]}),r(c,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-green-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-green-400 to-emerald-400"}),e(h,{className:"pb-4",children:r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-green-100",children:e(y,{className:"h-4 w-4 text-green-600"})}),e(g,{className:"text-lg font-semibold text-gray-800",children:"Thông tin câu hỏi"})]})}),r(m,{className:"space-y-6 pt-2",children:[e("div",{className:"rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4",children:r("div",{className:"flex flex-col md:flex-row gap-6",children:[e("div",{className:"flex-1",children:e(u,{value:ve,onChange:e=>we(parseFloat(e.target.value)||1)})}),e("div",{className:"flex-1 flex flex-col justify-center pt-2"})]})}),r("div",{className:"space-y-2",children:[r(s,{className:"flex items-center gap-2 text-base font-semibold text-gray-800",children:[e(y,{className:"h-4 w-4 text-indigo-500"}),"Audio câu hỏi ",e("span",{className:"text-red-500",children:"*"})]}),e(O,{id:"drag-scene-audio",label:"",accept:"audio/*",value:ue,onChange:e=>{be(e),Pe.clearErrors()},onPresignedUrlChange:ze,maxSize:10,placeholder:"Upload file audio câu hỏi",autoUpload:!0,prefix:"questions/audio",examId:ne||void 0,partId:ae||void 0}),Pe.formState.errors.audioUrl&&e("p",{className:"mt-1 text-sm text-red-500",children:Pe.formState.errors.audioUrl.message}),De&&e("audio",{controls:!0,className:"mt-2 w-full",src:De,children:"Trình duyệt không hỗ trợ audio."})]}),r("div",{className:"space-y-3",children:[r("div",{className:"flex items-center gap-2",children:[e("div",{className:"flex h-6 w-6 items-center justify-center rounded-md bg-green-100",children:e(j,{className:"h-3.5 w-3.5 text-green-600"})}),r(s,{className:"text-base font-semibold text-gray-800",children:["Đối tượng đúng ",e("span",{className:"text-red-500",children:"*"})]})]}),0===ce.length?e("p",{className:"text-sm text-amber-600 italic",children:"Vui lòng thêm đối tượng ở phần dữ liệu nhóm trước khi chọn đáp án."}):e(b,{value:pe,onValueChange:e=>{xe(e),Pe.clearErrors()},className:"grid gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4",children:ce.map((t,a)=>r("label",{className:"flex cursor-pointer items-center gap-3 rounded-lg border-2 p-3 transition-all "+(pe===t.id?"border-green-500 bg-green-50 ring-1 ring-green-500 shadow-sm":"border-gray-200 hover:border-green-300 hover:bg-green-50/50"),children:[e(p,{value:t.id,id:`correct-obj-${t.id}`,className:"flex-shrink-0"}),e("div",{className:"h-12 w-12 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 bg-white",children:t.imageUrl?e(H,{url:t.imageUrl,alt:t.label}):e("div",{className:"flex h-full w-full items-center justify-center",children:e(w,{className:"h-4 w-4 text-gray-300"})})}),e("div",{className:"min-w-0 flex-1",children:r("p",{className:"font-medium text-gray-900 text-sm line-clamp-2 break-all",children:[a+1,". ",t.label||"(Chưa đặt tên)"]})})]},t.id))}),Pe.formState.errors.correctObjectId&&e("div",{className:"rounded-lg border border-red-200 bg-red-50 p-3 mt-2",children:r("p",{className:"text-sm text-red-700",children:["⚠️ ",Pe.formState.errors.correctObjectId.message]})})]}),r("div",{className:"space-y-3",children:[r("div",{className:"flex items-center gap-2",children:[e("div",{className:"flex h-6 w-6 items-center justify-center rounded-md bg-blue-100",children:e(I,{className:"h-3.5 w-3.5 text-blue-600"})}),r(s,{className:"text-base font-semibold text-gray-800",children:["Vị trí đúng ",e("span",{className:"text-red-500",children:"*"})]})]}),0===he.length?e("p",{className:"text-sm text-amber-600 italic",children:"Vui lòng thêm vị trí ở phần dữ liệu nhóm trước khi chọn đáp án."}):e(b,{value:fe,onValueChange:e=>{Ne(e),Pe.clearErrors()},className:"grid gap-3 sm:grid-cols-2 md:grid-cols-3",children:he.map((t,a)=>{const n=G(a);return r("label",{className:"flex cursor-pointer items-center gap-3 rounded-lg border-2 p-3 transition-all "+(fe===t.id?"border-blue-500 bg-blue-50 ring-1 ring-blue-500 shadow-sm":"border-gray-200 hover:border-blue-300 hover:bg-blue-50/50"),children:[e(p,{value:t.id,id:`correct-dz-${t.id}`,className:"flex-shrink-0"}),e("div",{className:`flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md ${n.dot} text-white shadow-sm`,children:e(Z,{className:"h-4 w-4"})}),r("div",{className:"min-w-0 flex-1",children:[r("p",{className:"font-medium text-gray-900 text-sm line-clamp-1 break-all",children:[a+1,". ",t.label||"(Chưa đặt nhãn)"]}),r("p",{className:"text-[11px] text-gray-400 mt-0.5",children:["Vị trí: (",t.x,"%, ",t.y,"%) — ",t.width,"×",t.height,"%"]})]})]},t.id)})}),Pe.formState.errors.correctDropZoneId&&e("div",{className:"rounded-lg border border-red-200 bg-red-50 p-3 mt-2",children:r("p",{className:"text-sm text-red-700",children:["⚠️ ",Pe.formState.errors.correctDropZoneId.message]})})]}),Je&&Ke&&r("div",{className:"rounded-xl border-2 border-green-200 bg-gradient-to-r from-green-50 to-emerald-50 p-4",children:[r("div",{className:"flex items-center gap-2 mb-3",children:[e("div",{className:"flex h-5 w-5 items-center justify-center rounded-full bg-green-500 text-white",children:e(j,{className:"h-3 w-3"})}),e("span",{className:"text-sm font-semibold text-green-800",children:"Đáp án đúng"})]}),r("div",{className:"flex items-center gap-3 rounded-lg bg-white/80 p-3 border border-green-100",children:[e("div",{className:"h-14 w-14 flex-shrink-0 overflow-hidden rounded-lg border-2 border-green-300 bg-white",children:Je.imageUrl?e(H,{url:Je.imageUrl,alt:Je.label}):e("div",{className:"flex h-full w-full items-center justify-center",children:e(w,{className:"h-5 w-5 text-gray-300"})})}),r("div",{className:"min-w-0",children:[e("p",{className:"font-semibold text-gray-900 text-sm",children:Je.label||"(Chưa đặt tên)"}),e("p",{className:"text-xs text-gray-500",children:"Đối tượng"})]}),e(D,{className:"h-5 w-5 text-green-500 flex-shrink-0 mx-1"}),e("div",{className:`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg ${We>=0?G(We).dot:"bg-blue-500"} text-white shadow-sm`,children:e(Z,{className:"h-5 w-5"})}),r("div",{className:"min-w-0",children:[e("p",{className:"font-semibold text-gray-900 text-sm",children:Ke.label||"(Chưa đặt nhãn)"}),r("p",{className:"text-xs text-gray-500",children:["Vị trí (",Ke.x,"%, ",Ke.y,"%)"]})]})]})]}),(!pe||!fe)&&ce.length>0&&he.length>0&&r("div",{className:"flex items-start gap-2.5 rounded-lg border border-amber-200 bg-amber-50 p-3",children:[e(z,{className:"h-4 w-4 text-amber-500 flex-shrink-0 mt-0.5"}),e("p",{className:"text-sm text-amber-700",children:"Vui lòng chọn cả đối tượng đúng và vị trí đúng cho câu hỏi này"})]})]})]}),r(c,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-amber-400 to-orange-400"}),e(h,{className:"pb-4",children:r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 shadow-sm",children:e(E,{className:"h-4 w-4 text-white"})}),r("div",{children:[r("div",{className:"flex items-center gap-2",children:[e(g,{className:"text-lg font-semibold text-gray-800",children:"Giải thích"}),e("span",{className:"inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2.5 py-0.5 text-xs font-medium text-amber-600",children:"Không bắt buộc"})]}),e("p",{className:"text-sm text-gray-500 mt-0.5",children:"Giải thích cho đáp án đúng — hiển thị sau khi học sinh hoàn thành câu hỏi"})]})]})}),e(m,{children:e(d,{value:ye,onChange:e=>je(e.target.value),placeholder:"VD: Câu trả lời đúng là... vì...",rows:3,className:"min-h-[80px] border-gray-200 bg-white transition-all focus:border-amber-300 focus:ring-amber-200 resize-none"})})]})]})}export function ListenAndDragObjectsIntoSceneCreator(r){return e(J,{...r})}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
export * from './ListenAndDragObjectsIntoSceneClient';
|
|
1
|
+
export*from"./ListenAndDragObjectsIntoSceneCreator";export*from"./ListenAndDragObjectsIntoSceneClient";
|