@tinyweb_dev/oe-exam-sdk 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/exam-create.js +1 -179
- package/dist/api/exam-create.types.js +1 -1
- package/dist/api/exam-questions.js +1 -98
- package/dist/api/exam-questions.types.js +1 -1
- package/dist/api/exam-taking.js +1 -73
- package/dist/api/exam-taking.types.js +1 -1
- package/dist/api/index.js +1 -6
- package/dist/api/result-review.js +1 -50
- package/dist/api/result-review.types.js +1 -1
- package/dist/components/CueCardEditor.js +1 -58
- package/dist/components/common/ResolvedImage.js +1 -15
- package/dist/components/exams/CreateExamInfoForm.js +1 -35
- package/dist/components/exams/CreateExamPageContainer.js +1 -103
- package/dist/components/exams/ExamCreator.js +1 -89
- package/dist/components/exams/ExamPartTabs.js +1 -40
- package/dist/components/exams/ExamQuestionGrid.js +1 -18
- package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
- package/dist/components/exams/PartAudioUploader.js +1 -9
- package/dist/components/exams/TemplateSelector.js +1 -66
- package/dist/components/exams/api.js +1 -3
- package/dist/components/exams/create-exam.validation.js +1 -18
- package/dist/components/exams/exam-template.utils.js +1 -18
- package/dist/components/exams/index.js +1 -10
- package/dist/components/exams/take/ExamTakingPageContainer.js +1 -153
- package/dist/components/exams/take/GenericQuestionRenderer.js +1 -14
- package/dist/components/exams/take/exam-taking.utils.js +1 -88
- package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -24
- package/dist/components/exams/take/index.js +1 -2
- package/dist/components/exams/take/types.js +1 -1
- package/dist/components/index.js +1 -42
- package/dist/components/questions/_shared/config/question-types.config.js +1 -333
- package/dist/components/questions/_shared/constants/color-palette.js +1 -21
- package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +1 -105
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +1 -222
- package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +1 -140
- package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +1 -123
- package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +1 -50
- package/dist/components/questions/_shared/hooks/useQuestionFormState.js +1 -54
- package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +1 -123
- package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +1 -121
- package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +1 -70
- package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +1 -194
- package/dist/components/questions/_shared/index.js +1 -7
- package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
- package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
- package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +1 -3
- package/dist/components/questions/_shared/types/color-region.config.js +1 -1
- package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
- package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
- package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
- package/dist/components/questions/_shared/types/index.js +1 -12
- package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +1 -3
- package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +1 -3
- package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
- package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
- package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
- package/dist/components/questions/_shared/types/question-group.type.js +1 -11
- package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
- package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
- package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
- package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
- package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
- package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
- package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
- package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +1 -3
- package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
- package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
- package/dist/components/questions/_shared/utils/answer-builder.js +1 -79
- package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +1 -39
- package/dist/components/questions/_shared/utils/id-generator.js +1 -3
- package/dist/components/questions/_shared/utils/question-validation.js +1 -21
- package/dist/components/questions/components/ColorNode.js +1 -85
- package/dist/components/questions/components/ColorRegionClient.js +1 -545
- package/dist/components/questions/components/ColorRegionCreator.js +1 -420
- package/dist/components/questions/components/ColorRegionNode.js +1 -59
- package/dist/components/questions/components/CreatorGuide.js +1 -103
- package/dist/components/questions/components/NameNode.js +1 -26
- package/dist/components/questions/components/QuestionBankOption.js +1 -46
- package/dist/components/questions/components/QuestionBasicInfoForm.js +1 -9
- package/dist/components/questions/components/QuestionCommonFields.js +1 -33
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -258
- package/dist/components/questions/components/QuestionTypeSelector.js +1 -32
- package/dist/components/questions/components/QuestionTypeSpecificForms.js +1 -6
- package/dist/components/questions/components/RegionNode.js +1 -94
- package/dist/components/questions/components/index.js +1 -14
- package/dist/components/questions/creator/QuestionCreator.js +1 -90
- package/dist/components/questions/creator/QuestionGroupCreator.js +1 -119
- package/dist/components/questions/creator/dedicated-component-router.js +1 -79
- package/dist/components/questions/creator/index.js +1 -3
- package/dist/components/questions/creator/question-type-registry.js +1 -80
- package/dist/components/questions/groups/ArticlesGroupCard.js +1 -121
- package/dist/components/questions/groups/BasicQuestionGroupCard.js +1 -50
- package/dist/components/questions/groups/DocumentGroupCard.js +1 -114
- package/dist/components/questions/groups/MatchAnswerGroupCard.js +1 -25
- package/dist/components/questions/index.js +1 -10
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +1 -37
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +1 -351
- package/dist/components/questions/types/answer-the-question/register.js +1 -67
- package/dist/components/questions/types/answer-the-question/transform.js +1 -36
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +1 -141
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +1 -118
- package/dist/components/questions/types/arrange-letters-into-words/index.js +1 -2
- package/dist/components/questions/types/arrange-letters-into-words/register.js +1 -4
- package/dist/components/questions/types/arrange-letters-into-words/transform.js +1 -24
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +1 -64
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +1 -169
- package/dist/components/questions/types/choose-correct-adjective/index.js +1 -2
- package/dist/components/questions/types/choose-correct-adjective/register.js +1 -55
- package/dist/components/questions/types/choose-correct-adjective/transform.js +1 -16
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -131
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -581
- package/dist/components/questions/types/choose-the-correct-answer/index.js +1 -2
- package/dist/components/questions/types/choose-the-correct-answer/register.js +1 -101
- package/dist/components/questions/types/choose-the-correct-answer/transform.js +1 -43
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -275
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +1 -448
- package/dist/components/questions/types/fill-in-blank/index.js +1 -2
- package/dist/components/questions/types/fill-in-blank/register.js +1 -138
- package/dist/components/questions/types/fill-in-blank/transform.js +1 -59
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +1 -117
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +1 -671
- package/dist/components/questions/types/fill-missing-words-in-grid/index.js +1 -2
- package/dist/components/questions/types/fill-missing-words-in-grid/register.js +1 -37
- package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +1 -15
- package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +1 -262
- package/dist/components/questions/types/gn-speaking-interview/register.js +1 -121
- package/dist/components/questions/types/gn-speaking-interview/transform.js +1 -48
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +1 -16
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +1 -27
- package/dist/components/questions/types/image-object-matching/index.js +1 -2
- package/dist/components/questions/types/image-object-matching/register.js +1 -3
- package/dist/components/questions/types/image-object-matching/transform.js +1 -2
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +1 -102
- package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +1 -344
- package/dist/components/questions/types/label-the-picture/index.js +1 -2
- package/dist/components/questions/types/label-the-picture/register.js +1 -54
- package/dist/components/questions/types/label-the-picture/transform.js +1 -28
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +1 -78
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +1 -232
- package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +1 -2
- package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +1 -82
- package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +1 -27
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +1 -70
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +1 -386
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +1 -2
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +1 -100
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +1 -33
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +1 -106
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +1 -457
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +1 -2
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +1 -110
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +1 -35
- package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +1 -8
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +1 -22
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +1 -291
- package/dist/components/questions/types/listen-and-speak-answer/index.js +1 -2
- package/dist/components/questions/types/listen-and-speak-answer/register.js +1 -84
- package/dist/components/questions/types/listen-and-speak-answer/transform.js +1 -27
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +1 -32
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +1 -295
- package/dist/components/questions/types/listen-and-speak-compare-images/register.js +1 -79
- package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +1 -27
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +1 -34
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +1 -285
- package/dist/components/questions/types/listen-and-speak-image-group/register.js +1 -81
- package/dist/components/questions/types/listen-and-speak-image-group/transform.js +1 -26
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +1 -38
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +1 -385
- package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +1 -87
- package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +1 -32
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +1 -247
- package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +1 -59
- package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +1 -18
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +1 -256
- package/dist/components/questions/types/listen-and-speak-question-list/register.js +1 -81
- package/dist/components/questions/types/listen-and-speak-question-list/transform.js +1 -26
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +1 -230
- package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +1 -56
- package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +1 -16
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +1 -72
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +1 -230
- package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +1 -66
- package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +1 -42
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +1 -96
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +1 -238
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +1 -2
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +1 -76
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +1 -19
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +1 -134
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +1 -617
- package/dist/components/questions/types/look-picture-fill-word-hint/index.js +1 -2
- package/dist/components/questions/types/look-picture-fill-word-hint/register.js +1 -82
- package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +1 -29
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +1 -38
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +1 -359
- package/dist/components/questions/types/match-by-writing-answer/index.js +1 -3
- package/dist/components/questions/types/match-by-writing-answer/register.js +1 -58
- package/dist/components/questions/types/match-by-writing-answer/transform.js +1 -61
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +1 -16
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +1 -23
- package/dist/components/questions/types/read-and-color-objects/index.js +1 -2
- package/dist/components/questions/types/read-and-color-objects/register.js +1 -3
- package/dist/components/questions/types/read-and-color-objects/transform.js +1 -36
- package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +1 -125
- package/dist/components/questions/types/read-displayed-content/register.js +1 -64
- package/dist/components/questions/types/read-displayed-content/transform.js +1 -12
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +1 -64
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +1 -511
- package/dist/components/questions/types/read-passage-and-answer-questions/index.js +1 -2
- package/dist/components/questions/types/read-passage-and-answer-questions/register.js +1 -58
- package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +1 -28
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +1 -373
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -41
- package/dist/components/questions/types/speaking-conversation/register.js +1 -110
- package/dist/components/questions/types/speaking-conversation/transform.js +1 -34
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -74
- package/dist/components/questions/types/speaking-cue-card/register.js +1 -52
- package/dist/components/questions/types/speaking-cue-card/transform.js +1 -12
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +1 -10
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +1 -161
- package/dist/components/questions/types/speaking-describe-image/index.js +1 -2
- package/dist/components/questions/types/speaking-describe-image/register.js +1 -69
- package/dist/components/questions/types/speaking-describe-image/transform.js +1 -16
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +1 -234
- package/dist/components/questions/types/spontaneous-qa/register.js +1 -93
- package/dist/components/questions/types/spontaneous-qa/transform.js +1 -34
- package/dist/components/questions/types/true-false/TrueFalseClient.js +1 -75
- package/dist/components/questions/types/true-false/TrueFalseCreator.js +1 -244
- package/dist/components/questions/types/true-false/index.js +1 -2
- package/dist/components/questions/types/true-false/register.js +1 -77
- package/dist/components/questions/types/true-false/transform.js +1 -32
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +1 -233
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +1 -631
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +1 -28
- package/dist/components/questions/types/word-fill-paragraph/index.js +1 -2
- package/dist/components/questions/types/word-fill-paragraph/register.js +1 -136
- package/dist/components/questions/types/word-fill-paragraph/transform.js +1 -56
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +1 -138
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +1 -261
- package/dist/components/questions/types/word-fill-structured-form/index.js +1 -2
- package/dist/components/questions/types/word-fill-structured-form/register.js +1 -67
- package/dist/components/questions/types/word-fill-structured-form/transform.js +1 -40
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +1 -84
- package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +1 -192
- package/dist/components/questions/types/word-ordering/index.js +1 -2
- package/dist/components/questions/types/word-ordering/register.js +1 -46
- package/dist/components/questions/types/word-ordering/transform.js +1 -24
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +1 -98
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +1 -320
- package/dist/components/questions/types/write-a-short-letter/index.js +1 -2
- package/dist/components/questions/types/write-a-short-letter/register.js +1 -68
- package/dist/components/questions/types/write-a-short-letter/transform.js +1 -25
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +1 -61
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +1 -196
- package/dist/components/questions/types/write-correct-verb-form/index.js +1 -2
- package/dist/components/questions/types/write-correct-verb-form/register.js +1 -62
- package/dist/components/questions/types/write-correct-verb-form/transform.js +1 -17
- package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +1 -22
- package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +1 -174
- package/dist/components/questions/types/write-sentences/index.js +1 -2
- package/dist/components/questions/types/write-sentences/register.js +1 -58
- package/dist/components/questions/types/write-sentences/transform.js +1 -28
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +1 -96
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +1 -161
- package/dist/components/questions/types/write-short-paragraph/index.js +1 -2
- package/dist/components/questions/types/write-short-paragraph/register.js +1 -4
- package/dist/components/questions/types/write-short-paragraph/transform.js +1 -21
- package/dist/components/questions/viewer/QuestionGroupViewer.js +1 -36
- package/dist/components/questions/viewer/QuestionViewer.js +1 -446
- package/dist/components/questions/viewer/index.js +1 -2
- package/dist/components/results/ResultReviewPageContainer.js +1 -98
- package/dist/components/results/ReviewQuestionRenderer.js +1 -72
- package/dist/components/results/index.js +1 -2
- package/dist/components/results/result-review.utils.js +1 -6
- package/dist/components/results/review-data.js +1 -150
- package/dist/components/results/review-renderer.utils.js +1 -97
- package/dist/components/results/review-types.js +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +1 -138
- package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +1 -12
- package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +1 -18
- package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +1 -11
- package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +1 -11
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +1 -9
- package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +1 -13
- package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +1 -8
- package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +1 -6
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +1 -5
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +1 -9
- package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +1 -41
- package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -25
- package/dist/components/themes/cambridge-yle/index.js +1 -13
- package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +1 -24
- package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +1 -88
- package/dist/components/themes/english-certification/EcWordFillParagraph.js +1 -55
- package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +1 -156
- package/dist/components/themes/english-certification/EcWritingTask.js +1 -34
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -114
- package/dist/components/themes/english-certification/EnglishCertificationBrand.js +1 -11
- package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +1 -21
- package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +1 -15
- package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +1 -19
- package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +1 -148
- package/dist/components/themes/english-certification/EnglishCertificationHeader.js +1 -31
- package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +1 -71
- package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +1 -33
- package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +1 -16
- package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +1 -22
- package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +1 -37
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -72
- package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +1 -90
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +1 -18
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +1 -29
- package/dist/components/themes/english-certification/english-certification-theme.js +1 -58
- package/dist/components/themes/english-certification/grouped-question-utils.js +1 -218
- package/dist/components/themes/english-certification/index.js +1 -21
- package/dist/components/themes/index.js +1 -6
- package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +1 -170
- package/dist/components/themes/summer-sky/SummerSkyLayout.js +1 -221
- package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +1 -18
- package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +1 -194
- package/dist/components/themes/types.js +1 -1
- package/dist/components/ui/alert-dialog.js +1 -26
- package/dist/components/ui/alert.js +1 -22
- package/dist/components/ui/avatar.js +1 -12
- package/dist/components/ui/badge.js +1 -38
- package/dist/components/ui/button.js +1 -35
- package/dist/components/ui/calendar.js +1 -76
- package/dist/components/ui/card.js +1 -16
- package/dist/components/ui/carousel.js +1 -91
- package/dist/components/ui/checkbox.js +1 -9
- package/dist/components/ui/collapsible.js +1 -13
- package/dist/components/ui/command.js +1 -34
- package/dist/components/ui/confirm-dialog.js +1 -49
- package/dist/components/ui/dialog.js +1 -23
- package/dist/components/ui/drawer.js +1 -35
- package/dist/components/ui/dropdown-menu.js +1 -33
- package/dist/components/ui/file-upload.js +1 -155
- package/dist/components/ui/form.js +1 -62
- package/dist/components/ui/hover-card.js +1 -10
- package/dist/components/ui/image-preview.js +1 -29
- package/dist/components/ui/input.js +1 -8
- package/dist/components/ui/label.js +1 -9
- package/dist/components/ui/language-switcher.js +1 -15
- package/dist/components/ui/multi-select.js +1 -90
- package/dist/components/ui/points-input.js +1 -33
- package/dist/components/ui/popover.js +1 -26
- package/dist/components/ui/progress.js +1 -16
- package/dist/components/ui/radio-group.js +1 -12
- package/dist/components/ui/scroll-area.js +1 -11
- package/dist/components/ui/select.js +1 -38
- package/dist/components/ui/separator.js +1 -8
- package/dist/components/ui/sheet.js +1 -40
- package/dist/components/ui/single-select.js +1 -76
- package/dist/components/ui/skeleton.js +1 -6
- package/dist/components/ui/switch.js +1 -8
- package/dist/components/ui/table.js +1 -20
- package/dist/components/ui/tabs.js +1 -17
- package/dist/components/ui/textarea.js +1 -8
- package/dist/components/ui/tooltip.js +1 -11
- package/dist/index.js +1 -19
- package/dist/mcp/config.js +1 -32
- package/dist/mcp/http-client.js +1 -48
- package/dist/mcp/index.js +1 -29
- package/dist/mcp/tools/_helpers.js +1 -25
- package/dist/mcp/tools/exams.js +1 -439
- package/dist/mcp/tools/index.js +1 -6
- package/dist/mcp/tools/results.js +1 -19
- package/dist/shared/constants/ApiConstant.js +1 -276
- package/dist/shared/constants/QueryKeyConstant.js +1 -112
- package/dist/shared/constants/exam-level.enum.js +1 -41
- package/dist/shared/constants/landings/placement-test.constants.js +1 -431
- package/dist/shared/constants/question-skills.js +1 -267
- package/dist/shared/constants/routes.js +1 -81
- package/dist/shared/index.js +1 -7
- package/dist/shared/lib/hooks/index.js +1 -10
- package/dist/shared/lib/hooks/useAlertDialog.js +1 -57
- package/dist/shared/lib/hooks/useAudioPlayCounter.js +1 -30
- package/dist/shared/lib/hooks/useAudioPreview.js +1 -81
- package/dist/shared/lib/hooks/useAudioUpload.js +1 -24
- package/dist/shared/lib/hooks/useConfirmDialog.js +1 -89
- package/dist/shared/lib/hooks/useDebouncedCallback.js +1 -32
- package/dist/shared/lib/hooks/useDelayedAutoPlay.js +1 -12
- package/dist/shared/lib/hooks/useFileUpload.js +1 -24
- package/dist/shared/lib/hooks/useImageUrl.js +1 -5
- package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -10
- package/dist/shared/lib/hooks/useReactHookForm.js +1 -31
- package/dist/shared/lib/hooks/useToast.js +1 -62
- package/dist/shared/lib/i18n/index.js +1 -54
- package/dist/shared/lib/i18n/messages/en/admin.js +1 -2104
- package/dist/shared/lib/i18n/messages/en/auth.js +1 -86
- package/dist/shared/lib/i18n/messages/en/common.js +1 -140
- package/dist/shared/lib/i18n/messages/en/file-upload.js +1 -18
- package/dist/shared/lib/i18n/messages/en/student.js +1 -133
- package/dist/shared/lib/i18n/messages/en/terms.js +1 -37
- package/dist/shared/lib/i18n/messages/en.js +1 -15
- package/dist/shared/lib/i18n/messages/vi/admin.js +1 -2104
- package/dist/shared/lib/i18n/messages/vi/auth.js +1 -86
- package/dist/shared/lib/i18n/messages/vi/common.js +1 -140
- package/dist/shared/lib/i18n/messages/vi/file-upload.js +1 -18
- package/dist/shared/lib/i18n/messages/vi/student.js +1 -133
- package/dist/shared/lib/i18n/messages/vi/terms.js +1 -37
- package/dist/shared/lib/i18n/messages/vi.js +1 -15
- package/dist/shared/lib/i18n/types.js +1 -1
- package/dist/shared/lib/stores/examQuestionStore.js +1 -212
- package/dist/shared/lib/stores/localeStore.js +1 -10
- package/dist/shared/lib/utils/public-page.js +1 -44
- package/dist/shared/lib/utils/question-transform-types.js +1 -1
- package/dist/shared/lib/utils/question-transform.js +1 -174
- package/dist/shared/lib/utils.js +1 -168
- package/dist/shared/lib/validation/choose-correct-answer.validation.js +1 -111
- package/dist/shared/lib/validation/label-the-picture.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +1 -46
- package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +1 -21
- package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +1 -34
- package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +1 -40
- package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +1 -45
- package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +1 -39
- package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +1 -32
- package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +1 -28
- package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +1 -12
- package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +1 -47
- package/dist/shared/lib/validation/read-and-color-objects.validation.js +1 -29
- package/dist/shared/lib/validation/read-displayed-content.validation.js +1 -28
- package/dist/shared/lib/validation/speaking-conversation.validation.js +1 -75
- package/dist/shared/lib/validation/speaking-describe-image.validation.js +1 -31
- package/dist/shared/lib/validation/spontaneous-qa.validation.js +1 -48
- package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +1 -38
- package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +1 -39
- package/dist/shared/lib/validation/word-fill-paragraph.validation.js +1 -43
- package/dist/shared/lib/validation/word-ordering.validation.js +1 -33
- package/dist/shared/types/branch.types.js +1 -6
- package/dist/shared/types/common.types.js +1 -50
- package/dist/shared/types/entities/exam-schedule.types.js +1 -5
- package/dist/shared/types/entities/exam.types.js +1 -1
- package/dist/shared/types/entities/question.types.js +1 -4
- package/dist/shared/types/entities/result.types.js +1 -4
- package/dist/shared/types/entities/user.types.js +1 -4
- package/dist/shared/types/exam-taking.types.js +1 -2
- package/dist/shared/types/history.types.js +1 -34
- package/dist/shared/types/index.js +1 -1
- package/dist/shared/types/questions/choose-correct-adjective.js +1 -4
- package/dist/shared/types/questions/choose-the-correct-answer.js +1 -9
- package/dist/shared/types/questions/fill-missing-words-in-grid.js +1 -11
- package/dist/shared/types/questions/index.js +1 -40
- package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-info-exchange.js +1 -3
- package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +1 -3
- package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-with-story-images.js +1 -3
- package/dist/shared/types/questions/look-picture-choose-correct-answer.js +1 -5
- package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +1 -9
- package/dist/shared/types/questions/look-picture-fill-word-hint.js +1 -7
- package/dist/shared/types/questions/read-displayed-content.js +1 -1
- package/dist/shared/types/questions/speaking-conversation.js +1 -7
- package/dist/shared/types/questions/speaking-describe-image.js +1 -1
- package/dist/shared/types/questions/spontaneous-qa.js +1 -1
- package/dist/shared/types/questions/write-correct-verb-form.js +1 -12
- package/dist/shared/types/role.types.js +1 -1
- package/dist/shared/types/structure-management.js +1 -28
- package/dist/shared/types/student.types.js +1 -54
- package/dist/shared/types/teacher-dashboard-rooms.js +1 -2
- package/package.json +4 -2
|
@@ -1,3 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { createColorRegionRegistration } from '../../_shared/utils/createColorRegionRegistration';
|
|
3
|
-
export const imageObjectMatchingRegistration = createColorRegionRegistration(ImageObjectMatchingCreator);
|
|
1
|
+
import{ImageObjectMatchingCreator as t}from"./ImageObjectMatchingCreator";import{createColorRegionRegistration as e}from"../../_shared/utils/createColorRegionRegistration";export const imageObjectMatchingRegistration=e(t);
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { transformReadAndColorObjects as transformImageObjectMatching } from '../read-and-color-objects/transform';
|
|
1
|
+
export{transformReadAndColorObjects as transformImageObjectMatching}from"../read-and-color-objects/transform";
|
|
@@ -1,102 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useEffect, useRef } from 'react';
|
|
4
|
-
import { cn } from '../../../../shared/lib/utils';
|
|
5
|
-
import { Input } from '../../../../components/ui/input';
|
|
6
|
-
import { Lightbulb } from 'lucide-react';
|
|
7
|
-
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
8
|
-
export function LabelThePictureClient({ questionData, userAnswer = {}, isReviewMode = false, explanation, onSubmit, }) {
|
|
9
|
-
const { imageUrl, wordBank, labels } = questionData;
|
|
10
|
-
// Initialize answers with pre-filled values
|
|
11
|
-
const initialAnswers = labels.reduce((acc, label) => {
|
|
12
|
-
if (label.isPreFilled && label.correctAnswer) {
|
|
13
|
-
acc[label.id] = label.correctAnswer;
|
|
14
|
-
}
|
|
15
|
-
return acc;
|
|
16
|
-
}, {});
|
|
17
|
-
const [answers, setAnswers] = useState(userAnswer || initialAnswers);
|
|
18
|
-
// Use refs to track previous values and prevent infinite loops
|
|
19
|
-
const previousUserAnswerRef = useRef('');
|
|
20
|
-
const previousLabelsRef = useRef('');
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
// Serialize current values for comparison
|
|
23
|
-
const currentUserAnswerStr = JSON.stringify(userAnswer || {});
|
|
24
|
-
const currentLabelsStr = JSON.stringify(labels);
|
|
25
|
-
// Only update if values actually changed
|
|
26
|
-
if (previousUserAnswerRef.current === currentUserAnswerStr &&
|
|
27
|
-
previousLabelsRef.current === currentLabelsStr) {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
// Update refs
|
|
31
|
-
previousUserAnswerRef.current = currentUserAnswerStr;
|
|
32
|
-
previousLabelsRef.current = currentLabelsStr;
|
|
33
|
-
// Re-calculate pre-filled answers
|
|
34
|
-
const preFilledAnswers = labels.reduce((acc, label) => {
|
|
35
|
-
if (label.isPreFilled && label.correctAnswer) {
|
|
36
|
-
acc[label.id] = label.correctAnswer;
|
|
37
|
-
}
|
|
38
|
-
return acc;
|
|
39
|
-
}, {});
|
|
40
|
-
if (userAnswer && Object.keys(userAnswer).length > 0) {
|
|
41
|
-
// Merge user answers with pre-filled values
|
|
42
|
-
const mergedAnswers = { ...preFilledAnswers, ...userAnswer };
|
|
43
|
-
setAnswers(mergedAnswers);
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
// Initialize with pre-filled values if no user answer
|
|
47
|
-
setAnswers(preFilledAnswers);
|
|
48
|
-
}
|
|
49
|
-
}, [userAnswer, labels]);
|
|
50
|
-
const handleAnswerChange = (labelId, value) => {
|
|
51
|
-
if (isReviewMode)
|
|
52
|
-
return;
|
|
53
|
-
const newAnswers = {
|
|
54
|
-
...answers,
|
|
55
|
-
[labelId]: value,
|
|
56
|
-
};
|
|
57
|
-
setAnswers(newAnswers);
|
|
58
|
-
onSubmit?.(newAnswers);
|
|
59
|
-
};
|
|
60
|
-
const { previewUrl, isLoading: isLoadingUrl, error: urlError } = usePresignedFileUrl(imageUrl);
|
|
61
|
-
// Debug logging
|
|
62
|
-
console.log('🖼️ LabelThePictureClient image debug:', {
|
|
63
|
-
inputImageUrl: imageUrl,
|
|
64
|
-
previewUrl,
|
|
65
|
-
isLoadingUrl,
|
|
66
|
-
urlError,
|
|
67
|
-
finalSrc: previewUrl || imageUrl,
|
|
68
|
-
});
|
|
69
|
-
// Sort labels by number
|
|
70
|
-
const sortedLabels = [...labels].sort((a, b) => a.number - b.number);
|
|
71
|
-
return (_jsxs("div", { className: "space-y-5", children: [_jsx("div", { className: "flex items-center justify-between", children: _jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold text-gray-900", children: "Label the Picture" }), _jsx("p", { className: "text-sm text-gray-500", children: "Nh\u00ECn tranh v\u00E0 \u0111i\u1EC1n t\u1EEB v\u00E0o c\u00E1c v\u1ECB tr\u00ED t\u01B0\u01A1ng \u1EE9ng" })] }) }), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg", children: [_jsx("div", { className: "border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-red-400" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-green-400" }), _jsx("span", { className: "ml-3 text-sm font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex justify-center bg-gradient-to-br from-slate-50 to-gray-100 p-4", children: _jsxs("div", { className: "relative max-h-[400px] w-full max-w-2xl overflow-hidden rounded-lg", children: [isLoadingUrl && (_jsx("div", { className: "flex h-64 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })), !isLoadingUrl && (previewUrl || imageUrl) ? (
|
|
72
|
-
/* eslint-disable-next-line @next/next/no-img-element */
|
|
73
|
-
_jsx("img", { src: previewUrl || imageUrl, alt: "Label the picture", className: "h-full w-full object-contain", onError: (e) => {
|
|
74
|
-
console.error('Failed to load image:', imageUrl, 'previewUrl:', previewUrl);
|
|
75
|
-
// Replace with default placeholder
|
|
76
|
-
const target = e.target;
|
|
77
|
-
target.onerror = null; // Prevent infinite loop
|
|
78
|
-
target.src = '/images/placeholder-image.svg';
|
|
79
|
-
} })) : (_jsxs("div", { className: "flex h-64 flex-col items-center justify-center gap-3 bg-gray-50 rounded-lg border-2 border-dashed border-gray-200", children: [_jsx("svg", { className: "h-16 w-16 text-gray-300", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" }) }), _jsx("p", { className: "text-gray-400 font-medium", children: "Ch\u01B0a c\u00F3 h\u00ECnh \u1EA3nh" })] }))] }) })] }), wordBank.length > 0 && (_jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-amber-50 to-orange-50 px-4 py-2.5", children: [_jsx("svg", { className: "h-5 w-5 text-amber-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" }) }), _jsx("h3", { className: "text-sm font-semibold text-gray-800", children: "Word Bank" })] }), _jsx("div", { className: "p-4", children: _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => {
|
|
80
|
-
const isUsedAsPreFilled = labels.some((label) => label.isPreFilled && label.correctAnswer === word);
|
|
81
|
-
const isUsedByUser = Object.values(answers).includes(word);
|
|
82
|
-
return (_jsxs("div", { className: cn('flex items-center gap-1.5 rounded-lg border-2 px-3 py-1.5 text-sm font-semibold transition-all', isUsedAsPreFilled
|
|
83
|
-
? 'border-blue-300 bg-blue-50 text-blue-700'
|
|
84
|
-
: isUsedByUser
|
|
85
|
-
? 'border-green-300 bg-green-50 text-green-700'
|
|
86
|
-
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:shadow-sm'), children: [word, isUsedAsPreFilled && (_jsx("span", { className: "rounded bg-blue-200 px-1 py-0.5 text-xs", children: "s\u1EB5n" })), isUsedByUser && !isUsedAsPreFilled && (_jsx("span", { className: "rounded bg-green-200 px-1 py-0.5 text-xs", children: "\u2713" }))] }, index));
|
|
87
|
-
}) }) })] })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("svg", { className: "h-5 w-5 text-blue-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" }) }), _jsx("h3", { className: "text-sm font-semibold text-gray-800", children: "\u0110i\u1EC1n t\u1EEB v\u00E0o c\u00E1c v\u1ECB tr\u00ED" })] }), _jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: sortedLabels.map((label) => {
|
|
88
|
-
const userAnswerValue = answers[label.id] || '';
|
|
89
|
-
const correctAnswer = label.correctAnswer.trim().toLowerCase();
|
|
90
|
-
const userAnswerLower = userAnswerValue.trim().toLowerCase();
|
|
91
|
-
const isCorrect = isReviewMode && userAnswerLower === correctAnswer && userAnswerValue.trim() !== '';
|
|
92
|
-
const isWrong = isReviewMode && userAnswerValue.trim() !== '' && userAnswerLower !== correctAnswer;
|
|
93
|
-
const isPreFilled = label.isPreFilled || false;
|
|
94
|
-
return (_jsxs("div", { className: cn('group overflow-hidden rounded-xl border bg-white shadow-sm transition-all hover:shadow-md', isPreFilled && 'border-blue-300 bg-blue-50/30', isReviewMode && isCorrect && !isPreFilled && 'border-green-400 bg-green-50/50', isReviewMode && isWrong && !isPreFilled && 'border-red-400 bg-red-50/50', !isReviewMode && !isPreFilled && 'border-gray-200'), children: [_jsxs("div", { className: cn('flex items-center justify-between px-4 py-2', isPreFilled ? 'bg-blue-100/50' : 'bg-gray-50'), children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: cn('flex h-7 w-7 items-center justify-center rounded-lg text-xs font-bold text-white shadow-sm', isPreFilled
|
|
95
|
-
? 'bg-blue-500'
|
|
96
|
-
: isCorrect
|
|
97
|
-
? 'bg-green-500'
|
|
98
|
-
: isWrong
|
|
99
|
-
? 'bg-red-500'
|
|
100
|
-
: 'bg-gradient-to-br from-blue-500 to-indigo-600'), children: label.number }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["V\u1ECB tr\u00ED ", label.number] })] }), isPreFilled && (_jsx("span", { className: "rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-600", children: "\u0110i\u1EC1n s\u1EB5n" })), isReviewMode && isCorrect && !isPreFilled && (_jsx("span", { className: "rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-600", children: "\u2713 \u0110\u00FAng" })), isReviewMode && isWrong && !isPreFilled && (_jsx("span", { className: "rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600", children: "\u2717 Sai" }))] }), _jsxs("div", { className: "p-4", children: [_jsx(Input, { value: userAnswerValue, onChange: (e) => handleAnswerChange(label.id, e.target.value), readOnly: isReviewMode || isPreFilled, placeholder: isPreFilled ? '' : 'Nhập từ...', className: cn('w-full text-center font-semibold', isPreFilled && 'bg-blue-50 border-blue-200 cursor-not-allowed', isCorrect && !isPreFilled && 'bg-green-50 border-green-300', isWrong && !isPreFilled && 'bg-red-50 border-red-300') }), isReviewMode && isWrong && !isPreFilled && (_jsxs("div", { className: "mt-2 rounded-lg bg-green-50 p-2 flex items-center justify-center gap-1.5 border border-green-200", children: [_jsx("span", { className: "text-xs text-green-600", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "text-sm font-bold text-green-700", children: label.correctAnswer })] }))] })] }, label.id));
|
|
101
|
-
}) })] }), isReviewMode && explanation && (_jsx("div", { className: "rounded-xl bg-gradient-to-r from-blue-50 to-indigo-50 p-4 border border-blue-200", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("div", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-white", children: _jsx(Lightbulb, { className: "h-3.5 w-3.5" }) }), _jsxs("div", { className: "flex-1", children: [_jsx("span", { className: "text-xs font-semibold text-blue-700 uppercase tracking-wider", children: "Gi\u1EA3i th\u00EDch" }), _jsx("p", { className: "mt-1 text-sm text-gray-700 leading-relaxed", children: explanation })] })] }) }))] }));
|
|
102
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useState as t,useEffect as l,useRef as s}from"react";import{cn as n}from"../../../../shared/lib/utils";import{Input as a}from"../../../../components/ui/input";import{Lightbulb as d}from"lucide-react";import{usePresignedFileUrl as i}from"../../../../shared/lib/hooks";export function LabelThePictureClient({questionData:o,userAnswer:c={},isReviewMode:m=!1,explanation:g,onSubmit:h}){const{imageUrl:b,wordBank:u,labels:x}=o,p=x.reduce((e,r)=>(r.isPreFilled&&r.correctAnswer&&(e[r.id]=r.correctAnswer),e),{}),[f,v]=t(c||p),N=s(""),w=s("");l(()=>{const e=JSON.stringify(c||{}),r=JSON.stringify(x);if(N.current===e&&w.current===r)return;N.current=e,w.current=r;const t=x.reduce((e,r)=>(r.isPreFilled&&r.correctAnswer&&(e[r.id]=r.correctAnswer),e),{});if(c&&Object.keys(c).length>0){const e={...t,...c};v(e)}else v(t)},[c,x]);const{previewUrl:y,isLoading:k,error:j}=i(b);console.log("🖼️ LabelThePictureClient image debug:",{inputImageUrl:b,previewUrl:y,isLoadingUrl:k,urlError:j,finalSrc:y||b});const L=[...x].sort((e,r)=>e.number-r.number);return r("div",{className:"space-y-5",children:[e("div",{className:"flex items-center justify-between",children:r("div",{children:[e("h2",{className:"text-lg font-semibold text-gray-900",children:"Label the Picture"}),e("p",{className:"text-sm text-gray-500",children:"Nhìn tranh và điền từ vào các vị trí tương ứng"})]})}),r("div",{className:"overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg",children:[e("div",{className:"border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3",children:r("div",{className:"flex items-center gap-2",children:[e("div",{className:"h-2.5 w-2.5 rounded-full bg-red-400"}),e("div",{className:"h-2.5 w-2.5 rounded-full bg-yellow-400"}),e("div",{className:"h-2.5 w-2.5 rounded-full bg-green-400"}),e("span",{className:"ml-3 text-sm font-medium text-gray-500",children:"Hình ảnh"})]})}),e("div",{className:"flex justify-center bg-gradient-to-br from-slate-50 to-gray-100 p-4",children:r("div",{className:"relative max-h-[400px] w-full max-w-2xl overflow-hidden rounded-lg",children:[k&&e("div",{className:"flex h-64 items-center justify-center",children:e("div",{className:"h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500"})}),k||!y&&!b?r("div",{className:"flex h-64 flex-col items-center justify-center gap-3 bg-gray-50 rounded-lg border-2 border-dashed border-gray-200",children:[e("svg",{className:"h-16 w-16 text-gray-300",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:e("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:1.5,d:"M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"})}),e("p",{className:"text-gray-400 font-medium",children:"Chưa có hình ảnh"})]}):e("img",{src:y||b,alt:"Label the picture",className:"h-full w-full object-contain",onError:e=>{console.error("Failed to load image:",b,"previewUrl:",y);const r=e.target;r.onerror=null,r.src="/images/placeholder-image.svg"}})]})})]}),u.length>0&&r("div",{className:"overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm",children:[r("div",{className:"flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-amber-50 to-orange-50 px-4 py-2.5",children:[e("svg",{className:"h-5 w-5 text-amber-600",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:e("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"})}),e("h3",{className:"text-sm font-semibold text-gray-800",children:"Word Bank"})]}),e("div",{className:"p-4",children:e("div",{className:"flex flex-wrap gap-2",children:u.map((t,l)=>{const s=x.some(e=>e.isPreFilled&&e.correctAnswer===t),a=Object.values(f).includes(t);return r("div",{className:n("flex items-center gap-1.5 rounded-lg border-2 px-3 py-1.5 text-sm font-semibold transition-all",s?"border-blue-300 bg-blue-50 text-blue-700":a?"border-green-300 bg-green-50 text-green-700":"border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:shadow-sm"),children:[t,s&&e("span",{className:"rounded bg-blue-200 px-1 py-0.5 text-xs",children:"sẵn"}),a&&!s&&e("span",{className:"rounded bg-green-200 px-1 py-0.5 text-xs",children:"✓"})]},l)})})})]}),r("div",{className:"space-y-4",children:[r("div",{className:"flex items-center gap-2",children:[e("svg",{className:"h-5 w-5 text-blue-600",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:e("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"})}),e("h3",{className:"text-sm font-semibold text-gray-800",children:"Điền từ vào các vị trí"})]}),e("div",{className:"grid gap-3 sm:grid-cols-2 lg:grid-cols-3",children:L.map(t=>{const l=f[t.id]||"",s=t.correctAnswer.trim().toLowerCase(),d=l.trim().toLowerCase(),i=m&&d===s&&""!==l.trim(),o=m&&""!==l.trim()&&d!==s,c=t.isPreFilled||!1;return r("div",{className:n("group overflow-hidden rounded-xl border bg-white shadow-sm transition-all hover:shadow-md",c&&"border-blue-300 bg-blue-50/30",m&&i&&!c&&"border-green-400 bg-green-50/50",m&&o&&!c&&"border-red-400 bg-red-50/50",!m&&!c&&"border-gray-200"),children:[r("div",{className:n("flex items-center justify-between px-4 py-2",c?"bg-blue-100/50":"bg-gray-50"),children:[r("div",{className:"flex items-center gap-2",children:[e("div",{className:n("flex h-7 w-7 items-center justify-center rounded-lg text-xs font-bold text-white shadow-sm",c?"bg-blue-500":i?"bg-green-500":o?"bg-red-500":"bg-gradient-to-br from-blue-500 to-indigo-600"),children:t.number}),r("span",{className:"text-sm font-medium text-gray-700",children:["Vị trí ",t.number]})]}),c&&e("span",{className:"rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-600",children:"Điền sẵn"}),m&&i&&!c&&e("span",{className:"rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-600",children:"✓ Đúng"}),m&&o&&!c&&e("span",{className:"rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600",children:"✗ Sai"})]}),r("div",{className:"p-4",children:[e(a,{value:l,onChange:e=>((e,r)=>{if(m)return;const t={...f,[e]:r};v(t),h?.(t)})(t.id,e.target.value),readOnly:m||c,placeholder:c?"":"Nhập từ...",className:n("w-full text-center font-semibold",c&&"bg-blue-50 border-blue-200 cursor-not-allowed",i&&!c&&"bg-green-50 border-green-300",o&&!c&&"bg-red-50 border-red-300")}),m&&o&&!c&&r("div",{className:"mt-2 rounded-lg bg-green-50 p-2 flex items-center justify-center gap-1.5 border border-green-200",children:[e("span",{className:"text-xs text-green-600",children:"Đáp án:"}),e("span",{className:"text-sm font-bold text-green-700",children:t.correctAnswer})]})]})]},t.id)})})]}),m&&g&&e("div",{className:"rounded-xl bg-gradient-to-r from-blue-50 to-indigo-50 p-4 border border-blue-200",children:r("div",{className:"flex items-start gap-3",children:[e("div",{className:"flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-white",children:e(d,{className:"h-3.5 w-3.5"})}),r("div",{className:"flex-1",children:[e("span",{className:"text-xs font-semibold text-blue-700 uppercase tracking-wider",children:"Giải thích"}),e("p",{className:"mt-1 text-sm text-gray-700 leading-relaxed",children:g})]})]})})]})}
|
|
@@ -1,344 +1 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useRef, useEffect } 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 { Pencil, Eye, Plus, X, Image, Lightbulb, Tags, BookOpen } from 'lucide-react';
|
|
11
|
-
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
12
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../../../components/ui/select';
|
|
13
|
-
import { LabelThePictureClient } from './LabelThePictureClient';
|
|
14
|
-
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
15
|
-
import { useReactHookForm } from '../../../../shared/lib/hooks/useReactHookForm';
|
|
16
|
-
import { labelThePictureFormSchema, } from '../../../../shared/lib/validation/label-the-picture.validation';
|
|
17
|
-
function LabelThePictureCreatorContent({ initialData, onSave, onCancel, onChange, onUnsavedChangesChange, validationRef, }) {
|
|
18
|
-
// ============ HELPER FUNCTIONS FOR NEW API FORMAT ============
|
|
19
|
-
// Support NEW API format (content.imageUrl, content.wordBank, content.labels) and legacy formats
|
|
20
|
-
const getInitialImageUrl = () => {
|
|
21
|
-
// NEW API format: content.imageUrl
|
|
22
|
-
if (initialData?.content?.imageUrl)
|
|
23
|
-
return initialData.content.imageUrl;
|
|
24
|
-
// Legacy format: imageUrl at root level
|
|
25
|
-
return initialData?.imageUrl || '';
|
|
26
|
-
};
|
|
27
|
-
const getInitialWordBank = () => {
|
|
28
|
-
// NEW API format: content.wordBank
|
|
29
|
-
if (initialData?.content?.wordBank && Array.isArray(initialData.content.wordBank)) {
|
|
30
|
-
return initialData.content.wordBank;
|
|
31
|
-
}
|
|
32
|
-
// Legacy format: wordBank at root level
|
|
33
|
-
return initialData?.wordBank || [];
|
|
34
|
-
};
|
|
35
|
-
const getInitialLabels = () => {
|
|
36
|
-
// NEW API format: content.labels with correctAnswer in each label
|
|
37
|
-
if (initialData?.content?.labels && Array.isArray(initialData.content.labels)) {
|
|
38
|
-
return initialData.content.labels;
|
|
39
|
-
}
|
|
40
|
-
// Legacy format: labels at root level
|
|
41
|
-
return initialData?.labels || [];
|
|
42
|
-
};
|
|
43
|
-
const getInitialPoints = () => {
|
|
44
|
-
// Points at root level
|
|
45
|
-
return initialData?.points || 1;
|
|
46
|
-
};
|
|
47
|
-
const getInitialExplanation = () => {
|
|
48
|
-
return initialData?.explanation || '';
|
|
49
|
-
};
|
|
50
|
-
// State
|
|
51
|
-
const [imageUrl, setImageUrl] = useState(getInitialImageUrl());
|
|
52
|
-
const [wordBank, setWordBank] = useState(getInitialWordBank());
|
|
53
|
-
const [labels, setLabels] = useState(getInitialLabels());
|
|
54
|
-
const [points, setPoints] = useState(getInitialPoints());
|
|
55
|
-
const [explanation, setExplanation] = useState(getInitialExplanation());
|
|
56
|
-
// Use react-hook-form with Zod validation
|
|
57
|
-
const form = useReactHookForm({
|
|
58
|
-
defaultValues: {
|
|
59
|
-
imageUrl: imageUrl,
|
|
60
|
-
wordBank: wordBank,
|
|
61
|
-
labels: labels,
|
|
62
|
-
},
|
|
63
|
-
zodSchema: labelThePictureFormSchema,
|
|
64
|
-
mode: 'onBlur',
|
|
65
|
-
});
|
|
66
|
-
// Track previous initialData to detect changes
|
|
67
|
-
const previousInitialDataRef = useRef(initialData);
|
|
68
|
-
const isInitializingRef = useRef(true); // Start as true to prevent onChange on first render
|
|
69
|
-
const onChangeRef = useRef(onChange);
|
|
70
|
-
const hasInitializedRef = useRef(false); // Track if we've initialized from initialData
|
|
71
|
-
// NOTE: Status tracking is now handled entirely by ExamCreator via localStorage (exam_question_status_{id})
|
|
72
|
-
// The onChange callback triggers handleQuestionDraftChange in ExamCreator which updates localStorage
|
|
73
|
-
// Update onChange ref when it changes
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
onChangeRef.current = onChange;
|
|
76
|
-
}, [onChange]);
|
|
77
|
-
// Expose validation function via ref
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
if (validationRef) {
|
|
80
|
-
validationRef.current = {
|
|
81
|
-
triggerValidation: async () => {
|
|
82
|
-
return validateForm();
|
|
83
|
-
},
|
|
84
|
-
// Return current form data immediately (not debounced)
|
|
85
|
-
getFormData: () => ({
|
|
86
|
-
imageUrl,
|
|
87
|
-
wordBank,
|
|
88
|
-
labels,
|
|
89
|
-
points,
|
|
90
|
-
explanation,
|
|
91
|
-
}),
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
}, [validationRef, imageUrl, wordBank, labels, points, explanation]);
|
|
95
|
-
// Store presigned URLs for images
|
|
96
|
-
const [imagePreviewUrl, setImagePreviewUrl] = useState('');
|
|
97
|
-
// Use custom hooks for presigned URLs
|
|
98
|
-
const { previewUrl: presignedPreviewUrl } = usePresignedFileUrl(imageUrl);
|
|
99
|
-
// Update image preview URL - only set when we have a valid presigned URL
|
|
100
|
-
// Don't fallback to raw imageUrl as it's a storage path that won't load
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
if (presignedPreviewUrl) {
|
|
103
|
-
setImagePreviewUrl(presignedPreviewUrl);
|
|
104
|
-
}
|
|
105
|
-
// Don't fallback to imageUrl directly - it's a storage path, not a valid URL
|
|
106
|
-
}, [presignedPreviewUrl]);
|
|
107
|
-
// Sync state from initialData when it changes
|
|
108
|
-
// IMPORTANT: Only sync when initialData comes from API (has ID) or when explicitly loading
|
|
109
|
-
// Don't sync when parent just echoes back our own onChange data
|
|
110
|
-
useEffect(() => {
|
|
111
|
-
// Skip if initialData is undefined/null
|
|
112
|
-
if (!initialData) {
|
|
113
|
-
// Only initialize once to empty state
|
|
114
|
-
if (!hasInitializedRef.current) {
|
|
115
|
-
hasInitializedRef.current = true;
|
|
116
|
-
isInitializingRef.current = false;
|
|
117
|
-
}
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
const initialDataChanged = JSON.stringify(previousInitialDataRef.current) !== JSON.stringify(initialData);
|
|
121
|
-
console.log('🏷️ LabelThePictureCreator useEffect:', {
|
|
122
|
-
initialDataChanged,
|
|
123
|
-
hasInitialized: hasInitializedRef.current,
|
|
124
|
-
initialData,
|
|
125
|
-
previousData: previousInitialDataRef.current,
|
|
126
|
-
});
|
|
127
|
-
// Only sync from initialData if:
|
|
128
|
-
// 1. First time initialization (hasInitializedRef.current is false)
|
|
129
|
-
// 2. OR initialData has actually changed AND we're not the source of the change
|
|
130
|
-
// (detecting by checking if previousInitialDataRef is undefined or significantly different)
|
|
131
|
-
const isFirstInit = !hasInitializedRef.current;
|
|
132
|
-
const isApiDataLoad = initialDataChanged && previousInitialDataRef.current === undefined;
|
|
133
|
-
if (isFirstInit || isApiDataLoad) {
|
|
134
|
-
isInitializingRef.current = true;
|
|
135
|
-
previousInitialDataRef.current = initialData;
|
|
136
|
-
hasInitializedRef.current = true;
|
|
137
|
-
console.log('🏷️ LabelThePictureCreator - Setting state from initialData:', {
|
|
138
|
-
imageUrl: getInitialImageUrl(),
|
|
139
|
-
wordBank: getInitialWordBank(),
|
|
140
|
-
labels: getInitialLabels(),
|
|
141
|
-
});
|
|
142
|
-
setImageUrl(getInitialImageUrl());
|
|
143
|
-
setWordBank(getInitialWordBank());
|
|
144
|
-
setLabels(getInitialLabels());
|
|
145
|
-
setPoints(getInitialPoints());
|
|
146
|
-
setExplanation(getInitialExplanation());
|
|
147
|
-
// Wait for state updates to complete before allowing onChange
|
|
148
|
-
setTimeout(() => {
|
|
149
|
-
isInitializingRef.current = false;
|
|
150
|
-
}, 150);
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
// Just update the ref to track current value, but don't reset state
|
|
154
|
-
previousInitialDataRef.current = initialData;
|
|
155
|
-
}
|
|
156
|
-
}, [initialData]);
|
|
157
|
-
// Convert to payload format and call onChange
|
|
158
|
-
// IMPORTANT: Only call onChange after initialization is complete AND state has values
|
|
159
|
-
useEffect(() => {
|
|
160
|
-
// Don't call onChange during initialization
|
|
161
|
-
if (isInitializingRef.current || !hasInitializedRef.current) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
// Don't call onChange if we have no meaningful data yet
|
|
165
|
-
// This prevents overwriting API data with empty values
|
|
166
|
-
if (!imageUrl && wordBank.length === 0 && labels.length === 0) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const payload = {
|
|
170
|
-
imageUrl,
|
|
171
|
-
wordBank,
|
|
172
|
-
labels,
|
|
173
|
-
};
|
|
174
|
-
payload.points = points;
|
|
175
|
-
payload.explanation = explanation;
|
|
176
|
-
requestAnimationFrame(() => {
|
|
177
|
-
onChangeRef.current?.(payload);
|
|
178
|
-
});
|
|
179
|
-
}, [imageUrl, wordBank, labels, points, explanation]);
|
|
180
|
-
// UI State
|
|
181
|
-
const [isClientMode, setIsClientMode] = useState(false);
|
|
182
|
-
// Add word to word bank
|
|
183
|
-
const addWordToBank = () => {
|
|
184
|
-
setWordBank([...wordBank, '']);
|
|
185
|
-
form.clearErrors();
|
|
186
|
-
};
|
|
187
|
-
// Update word in word bank
|
|
188
|
-
const updateWordInBank = (index, value) => {
|
|
189
|
-
const newWordBank = [...wordBank];
|
|
190
|
-
newWordBank[index] = value;
|
|
191
|
-
setWordBank(newWordBank);
|
|
192
|
-
form.clearErrors();
|
|
193
|
-
};
|
|
194
|
-
// Remove word from word bank
|
|
195
|
-
const removeWordFromBank = (index) => {
|
|
196
|
-
setWordBank(wordBank.filter((_, i) => i !== index));
|
|
197
|
-
form.clearErrors();
|
|
198
|
-
};
|
|
199
|
-
// Add label position
|
|
200
|
-
const addLabel = () => {
|
|
201
|
-
const maxNumber = labels.length > 0 ? Math.max(...labels.map((l) => l.number)) : 0;
|
|
202
|
-
const newLabel = {
|
|
203
|
-
id: `label-${Date.now()}-${Math.random()}`,
|
|
204
|
-
number: maxNumber + 1,
|
|
205
|
-
correctAnswer: '',
|
|
206
|
-
};
|
|
207
|
-
setLabels([...labels, newLabel]);
|
|
208
|
-
form.clearErrors();
|
|
209
|
-
};
|
|
210
|
-
// Update label
|
|
211
|
-
const updateLabel = (id, field, value) => {
|
|
212
|
-
setLabels(labels.map((label) => (label.id === id ? { ...label, [field]: value } : label)));
|
|
213
|
-
form.clearErrors();
|
|
214
|
-
};
|
|
215
|
-
// Remove label
|
|
216
|
-
const removeLabel = (id) => {
|
|
217
|
-
setLabels(labels.filter((label) => label.id !== id));
|
|
218
|
-
// Re-number remaining labels
|
|
219
|
-
const remainingLabels = labels.filter((label) => label.id !== id);
|
|
220
|
-
const sortedLabels = remainingLabels.sort((a, b) => a.number - b.number);
|
|
221
|
-
setLabels(sortedLabels.map((label, index) => ({
|
|
222
|
-
...label,
|
|
223
|
-
number: index + 1,
|
|
224
|
-
})));
|
|
225
|
-
form.clearErrors();
|
|
226
|
-
};
|
|
227
|
-
// Get validation errors
|
|
228
|
-
const getErrors = () => {
|
|
229
|
-
const errors = {};
|
|
230
|
-
// Image URL error
|
|
231
|
-
const imageError = form.formState.errors.imageUrl;
|
|
232
|
-
if (imageError) {
|
|
233
|
-
errors.imageUrl = imageError.message;
|
|
234
|
-
}
|
|
235
|
-
// Word bank error
|
|
236
|
-
const wordBankError = form.formState.errors.wordBank;
|
|
237
|
-
if (wordBankError && typeof wordBankError.message === 'string') {
|
|
238
|
-
errors.wordBank = wordBankError.message;
|
|
239
|
-
}
|
|
240
|
-
// Word bank items errors
|
|
241
|
-
errors.wordBankItems = {};
|
|
242
|
-
wordBank.forEach((_, index) => {
|
|
243
|
-
const itemError = form.formState.errors.wordBank?.[index];
|
|
244
|
-
if (itemError) {
|
|
245
|
-
errors.wordBankItems[index] = itemError.message;
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
// Labels error
|
|
249
|
-
const labelsError = form.formState.errors.labels;
|
|
250
|
-
if (labelsError && typeof labelsError.message === 'string') {
|
|
251
|
-
errors.labels = labelsError.message;
|
|
252
|
-
}
|
|
253
|
-
// Label items errors
|
|
254
|
-
errors.labelItems = {};
|
|
255
|
-
labels.forEach((label, index) => {
|
|
256
|
-
const itemError = form.formState.errors.labels?.[index]?.correctAnswer;
|
|
257
|
-
if (itemError) {
|
|
258
|
-
errors.labelItems[label.id] = itemError.message;
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
return errors;
|
|
262
|
-
};
|
|
263
|
-
// Validate form data
|
|
264
|
-
const validateForm = () => {
|
|
265
|
-
// Update form values before validation
|
|
266
|
-
form.setValue('imageUrl', imageUrl);
|
|
267
|
-
form.setValue('wordBank', wordBank);
|
|
268
|
-
form.setValue('labels', labels);
|
|
269
|
-
// Trigger validation
|
|
270
|
-
const result = labelThePictureFormSchema.safeParse({ imageUrl, wordBank, labels });
|
|
271
|
-
if (!result.success) {
|
|
272
|
-
// Set errors from Zod validation
|
|
273
|
-
result.error.issues.forEach((issue) => {
|
|
274
|
-
const path = issue.path.join('.');
|
|
275
|
-
form.setError(path, { message: issue.message });
|
|
276
|
-
});
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
return true;
|
|
280
|
-
};
|
|
281
|
-
// Validate before switching to client mode
|
|
282
|
-
const handlePreview = () => {
|
|
283
|
-
if (!validateForm()) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
setIsClientMode(true);
|
|
287
|
-
};
|
|
288
|
-
// Handle save with validation
|
|
289
|
-
const handleSave = () => {
|
|
290
|
-
if (!validateForm()) {
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
// NOTE: Save status is now managed by ExamCreator via localStorage
|
|
294
|
-
const saveData = {
|
|
295
|
-
imageUrl,
|
|
296
|
-
wordBank,
|
|
297
|
-
labels,
|
|
298
|
-
};
|
|
299
|
-
saveData.points = points;
|
|
300
|
-
onSave?.(saveData);
|
|
301
|
-
};
|
|
302
|
-
const errors = getErrors();
|
|
303
|
-
// If in client mode, show preview
|
|
304
|
-
if (isClientMode) {
|
|
305
|
-
// Pass raw imageUrl (relative path) - Client will fetch presigned URL itself via usePresignedFileUrl hook
|
|
306
|
-
const questionData = {
|
|
307
|
-
imageUrl: imageUrl, // Use raw path, not presignedPreviewUrl
|
|
308
|
-
wordBank,
|
|
309
|
-
labels,
|
|
310
|
-
};
|
|
311
|
-
// Auto-fill all positions with correct answers for preview
|
|
312
|
-
const correctAnswers = labels.reduce((acc, label) => {
|
|
313
|
-
acc[label.id] = label.correctAnswer;
|
|
314
|
-
return acc;
|
|
315
|
-
}, {});
|
|
316
|
-
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: "Label the Picture (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 Edit Mode"] })] }) }) }), _jsx(LabelThePictureClient, { questionData: questionData, userAnswer: correctAnswers, isReviewMode: true, explanation: explanation })] }));
|
|
317
|
-
}
|
|
318
|
-
return (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50", children: [_jsx("div", { className: "h-1.5 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-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-blue-500 to-indigo-600 shadow-md", children: _jsx(Image, { className: "h-5 w-5 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-xl font-bold text-gray-900", children: "G\u1EAFn nh\u00E3n h\u00ECnh \u1EA3nh" }), _jsx("p", { className: "text-sm text-gray-500", children: "T\u1EA1o c\u00E2u h\u1ECFi v\u1EDBi h\u00ECnh \u1EA3nh, word bank v\u00E0 c\u00E1c v\u1ECB tr\u00ED c\u1EA7n \u0111i\u1EC1n" })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: handlePreview, className: "gap-2 border-indigo-200 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(Eye, { className: "h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] })] }) }), _jsxs(CardContent, { className: "space-y-6", children: [_jsx("div", { className: "rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4", children: _jsx(PointsInput, { defaultValue: points, onChange: (e) => setPoints(Number(e.target.value) || 1) }) }), _jsx("div", { className: "rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-5 shadow-sm", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100", children: _jsx(Lightbulb, { className: "h-4 w-4 text-blue-600" }) }), _jsxs("div", { className: "text-sm text-blue-900", children: [_jsx("p", { className: "font-semibold text-blue-800", children: "H\u01B0\u1EDBng d\u1EABn t\u1EA1o c\u00E2u h\u1ECFi" }), _jsxs("ul", { className: "mt-2 space-y-1.5 text-blue-700", children: [_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "Upload h\u00ECnh \u1EA3nh c\u1EA7n g\u1EAFn nh\u00E3n"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "Th\u00EAm c\u00E1c t\u1EEB v\u00E0o Word Bank (danh s\u00E1ch t\u1EEB g\u1EE3i \u00FD)"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "T\u1EA1o c\u00E1c v\u1ECB tr\u00ED c\u1EA7n \u0111i\u1EC1n v\u00E0 ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng t\u1EEB Word Bank"] })] })] })] }) }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Image, { className: "h-4 w-4 text-indigo-500" }), "H\u00ECnh \u1EA3nh ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(FileUpload, { id: "label-picture-image", label: "H\u00ECnh \u1EA3nh", accept: "image/*", value: imageUrl, onChange: (url) => {
|
|
319
|
-
setImageUrl(url);
|
|
320
|
-
form.clearErrors();
|
|
321
|
-
}, onPresignedUrlChange: (presignedUrl) => {
|
|
322
|
-
setImagePreviewUrl(presignedUrl);
|
|
323
|
-
}, maxSize: 5, placeholder: "Upload \u1EA3nh ho\u1EB7c paste URL", required: true, autoUpload: true, prefix: "questions" }), errors.imageUrl && (_jsx("p", { className: "mt-1 text-sm text-red-600", children: errors.imageUrl })), imagePreviewUrl && (_jsx("div", { className: "relative mt-2 h-64 w-full max-w-2xl overflow-hidden rounded border", children: _jsx("img", { src: imagePreviewUrl, alt: "Preview for question", className: "h-full w-full object-contain", onError: () => {
|
|
324
|
-
console.error('Failed to load image:', imageUrl);
|
|
325
|
-
} }) }))] })] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-indigo-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-indigo-400 to-purple-400" }), _jsx(CardHeader, { className: "pb-4", 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-indigo-100", children: _jsx(Tags, { className: "h-3.5 w-3.5 text-indigo-600" }) }), _jsxs(CardTitle, { className: "text-lg font-semibold text-gray-800", children: ["Word Bank ", _jsxs("span", { className: "text-sm font-normal text-gray-500", children: ["(", wordBank.filter(w => w.trim()).length, " t\u1EEB)"] })] })] }), _jsxs(Button, { type: "button", size: "sm", onClick: addWordToBank, className: "gap-2 border-green-200 text-green-600 hover:bg-green-50 hover:text-green-700", variant: "outline", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm t\u1EEB"] })] }) }), _jsxs(CardContent, { className: "space-y-4", children: [errors.wordBank && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4", children: [_jsx("span", { className: "text-lg", children: "\u274C" }), _jsx("p", { className: "text-sm text-red-600", children: errors.wordBank })] })), _jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: wordBank.map((word, index) => (_jsxs("div", { className: `group relative rounded-xl border-2 p-3 transition-all duration-200 ${word.trim()
|
|
326
|
-
? 'border-green-200 bg-gradient-to-r from-green-50 to-emerald-50'
|
|
327
|
-
: 'border-gray-200 bg-white hover:border-gray-300'}`, children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: `flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg font-bold transition-all ${word.trim()
|
|
328
|
-
? 'bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-md'
|
|
329
|
-
: 'bg-gray-100 text-gray-600'}`, children: index + 1 }), _jsx(Input, { value: word, onChange: (e) => updateWordInBank(index, e.target.value), placeholder: `Từ ${index + 1}`, className: `flex-1 border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${errors.wordBankItems?.[index] ? 'border-red-400' : ''}` }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeWordFromBank(index), className: "text-gray-400 hover:bg-red-50 hover:text-red-500", children: _jsx(X, { className: "h-4 w-4" }) })] }), errors.wordBankItems?.[index] && (_jsx("p", { className: "mt-2 ml-11 text-sm text-red-600", children: errors.wordBankItems[index] }))] }, index))) }), wordBank.length === 0 && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-4", children: [_jsx("span", { className: "text-lg", children: "\u26A0\uFE0F" }), _jsx("p", { className: "text-sm text-amber-800", children: "Ch\u01B0a c\u00F3 t\u1EEB n\u00E0o. Nh\u1EA5n \"Th\u00EAm t\u1EEB\" \u0111\u1EC3 b\u1EAFt \u0111\u1EA7u." })] }))] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-purple-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-purple-400 to-pink-400" }), _jsx(CardHeader, { className: "pb-4", 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-purple-100", children: _jsx(Tags, { className: "h-3.5 w-3.5 text-purple-600" }) }), _jsxs(CardTitle, { className: "text-lg font-semibold text-gray-800", children: ["V\u1ECB tr\u00ED c\u1EA7n \u0111i\u1EC1n ", _jsxs("span", { className: "text-sm font-normal text-gray-500", children: ["(", labels.length, " v\u1ECB tr\u00ED)"] })] })] }), _jsxs(Button, { type: "button", size: "sm", onClick: addLabel, className: "gap-2 border-purple-200 text-purple-600 hover:bg-purple-50 hover:text-purple-700", variant: "outline", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm v\u1ECB tr\u00ED"] })] }) }), _jsxs(CardContent, { className: "space-y-4", children: [errors.labels && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4", children: [_jsx("span", { className: "text-lg", children: "\u274C" }), _jsx("p", { className: "text-sm text-red-600", children: errors.labels })] })), _jsx("div", { className: "space-y-4", children: labels
|
|
330
|
-
.sort((a, b) => a.number - b.number)
|
|
331
|
-
.map((label) => {
|
|
332
|
-
const hasAnswer = label.correctAnswer && label.correctAnswer.trim();
|
|
333
|
-
return (_jsx("div", { className: `group relative rounded-xl border-2 p-4 transition-all duration-200 ${hasAnswer
|
|
334
|
-
? 'border-green-300 bg-gradient-to-r from-green-50 to-emerald-50 shadow-md shadow-green-100'
|
|
335
|
-
: 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm'}`, children: _jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: `flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl font-bold transition-all ${hasAnswer
|
|
336
|
-
? 'bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-md'
|
|
337
|
-
: 'bg-purple-100 text-purple-700'}`, children: label.number }), _jsxs("div", { className: "flex-1 space-y-3", children: [_jsxs("div", { children: [_jsxs(Label, { className: "text-sm font-semibold text-gray-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx("div", { className: "mt-1.5", children: _jsxs(Select, { value: label.correctAnswer || '', onValueChange: (value) => updateLabel(label.id, 'correctAnswer', value), children: [_jsx(SelectTrigger, { className: `w-full border-gray-200 transition-all focus:border-indigo-300 focus:ring-indigo-200 ${errors.labelItems?.[label.id] ? 'border-red-400' : ''}`, children: _jsx(SelectValue, { placeholder: "Ch\u1ECDn t\u1EEB trong word bank" }) }), _jsx(SelectContent, { children: wordBank
|
|
338
|
-
.filter((word) => word.trim() !== '')
|
|
339
|
-
.map((word, index) => (_jsx(SelectItem, { value: word, children: word }, index))) })] }) }), errors.labelItems?.[label.id] && (_jsx("p", { className: "mt-1 text-sm text-red-600", children: errors.labelItems[label.id] }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "checkbox", id: `prefilled-${label.id}`, checked: label.isPreFilled || false, onChange: (e) => updateLabel(label.id, 'isPreFilled', e.target.checked), className: "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" }), _jsx(Label, { htmlFor: `prefilled-${label.id}`, className: "text-sm font-normal text-gray-600 cursor-pointer", children: "\u0110\u00E1nh d\u1EA5u l\u00E0 t\u1EEB g\u1EE3i \u00FD (\u0111\u00E3 \u0111i\u1EC1n s\u1EB5n)" })] })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeLabel(label.id), className: "text-gray-400 hover:bg-red-50 hover:text-red-500", children: _jsx(X, { className: "h-4 w-4" }) })] }) }, label.id));
|
|
340
|
-
}) }), labels.length === 0 && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-4", children: [_jsx("span", { className: "text-lg", children: "\u26A0\uFE0F" }), _jsx("p", { className: "text-sm text-amber-800", children: "Ch\u01B0a c\u00F3 v\u1ECB tr\u00ED n\u00E0o. Nh\u1EA5n \"Th\u00EAm v\u1ECB tr\u00ED\" \u0111\u1EC3 b\u1EAFt \u0111\u1EA7u." })] }))] })] }), _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, { id: "explanation", 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" }) })] })] }));
|
|
341
|
-
}
|
|
342
|
-
export function LabelThePictureCreator(props) {
|
|
343
|
-
return _jsx(LabelThePictureCreatorContent, { ...props });
|
|
344
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useState as t,useRef as a,useEffect as n}from"react";import{Button as l}from"../../../../components/ui/button";import{Label as s}from"../../../../components/ui/label";import{Input as i}from"../../../../components/ui/input";import{Textarea as o}from"../../../../components/ui/textarea";import{Card as d,CardContent as c,CardHeader as m,CardTitle as h}from"../../../../components/ui/card";import{PointsInput as g}from"../../../../components/ui/points-input";import{Pencil as u,Eye as b,Plus as p,X as f,Image as x,Lightbulb as N,Tags as v,BookOpen as w}from"lucide-react";import{FileUpload as y}from"../../../../components/ui/file-upload";import{Select as k,SelectContent as C,SelectItem as B,SelectTrigger as U,SelectValue as j}from"../../../../components/ui/select";import{LabelThePictureClient as E}from"./LabelThePictureClient";import{usePresignedFileUrl as A}from"../../../../shared/lib/hooks";import{useReactHookForm as T}from"../../../../shared/lib/hooks/useReactHookForm";import{labelThePictureFormSchema as I}from"../../../../shared/lib/validation/label-the-picture.validation";function P({initialData:P,onSave:S,onCancel:z,onChange:D,onUnsavedChangesChange:V,validationRef:F}){const L=()=>P?.content?.imageUrl?P.content.imageUrl:P?.imageUrl||"",q=()=>P?.content?.wordBank&&Array.isArray(P.content.wordBank)?P.content.wordBank:P?.wordBank||[],H=()=>P?.content?.labels&&Array.isArray(P.content.labels)?P.content.labels:P?.labels||[],$=()=>P?.points||1,M=()=>P?.explanation||"",[R,G]=t(L()),[W,J]=t(q()),[O,K]=t(H()),[Q,X]=t($()),[Y,Z]=t(M()),_=T({defaultValues:{imageUrl:R,wordBank:W,labels:O},zodSchema:I,mode:"onBlur"}),ee=a(P),re=a(!0),te=a(D),ae=a(!1);n(()=>{te.current=D},[D]),n(()=>{F&&(F.current={triggerValidation:async()=>ce(),getFormData:()=>({imageUrl:R,wordBank:W,labels:O,points:Q,explanation:Y})})},[F,R,W,O,Q,Y]);const[ne,le]=t(""),{previewUrl:se}=A(R);n(()=>{se&&le(se)},[se]),n(()=>{if(!P)return void(ae.current||(ae.current=!0,re.current=!1));const e=JSON.stringify(ee.current)!==JSON.stringify(P);console.log("🏷️ LabelThePictureCreator useEffect:",{initialDataChanged:e,hasInitialized:ae.current,initialData:P,previousData:ee.current});const r=!ae.current,t=e&&void 0===ee.current;r||t?(re.current=!0,ee.current=P,ae.current=!0,console.log("🏷️ LabelThePictureCreator - Setting state from initialData:",{imageUrl:L(),wordBank:q(),labels:H()}),G(L()),J(q()),K(H()),X($()),Z(M()),setTimeout(()=>{re.current=!1},150)):ee.current=P},[P]),n(()=>{if(re.current||!ae.current)return;if(!R&&0===W.length&&0===O.length)return;const e={imageUrl:R,wordBank:W,labels:O};e.points=Q,e.explanation=Y,requestAnimationFrame(()=>{te.current?.(e)})},[R,W,O,Q,Y]);const[ie,oe]=t(!1),de=(e,r,t)=>{K(O.map(a=>a.id===e?{...a,[r]:t}:a)),_.clearErrors()},ce=()=>{_.setValue("imageUrl",R),_.setValue("wordBank",W),_.setValue("labels",O);const e=I.safeParse({imageUrl:R,wordBank:W,labels:O});return!!e.success||(e.error.issues.forEach(e=>{const r=e.path.join(".");_.setError(r,{message:e.message})}),!1)},me=(()=>{const e={},r=_.formState.errors.imageUrl;r&&(e.imageUrl=r.message);const t=_.formState.errors.wordBank;t&&"string"==typeof t.message&&(e.wordBank=t.message),e.wordBankItems={},W.forEach((r,t)=>{const a=_.formState.errors.wordBank?.[t];a&&(e.wordBankItems[t]=a.message)});const a=_.formState.errors.labels;return a&&"string"==typeof a.message&&(e.labels=a.message),e.labelItems={},O.forEach((r,t)=>{const a=_.formState.errors.labels?.[t]?.correctAnswer;a&&(e.labelItems[r.id]=a.message)}),e})();if(ie){const t={imageUrl:R,wordBank:W,labels:O},a=O.reduce((e,r)=>(e[r.id]=r.correctAnswer,e),{});return r("div",{className:"space-y-6",children:[e(d,{children:e(m,{children:r("div",{className:"flex items-center justify-between",children:[r("div",{children:[e(h,{children:"Label the Picture (Preview)"}),e("p",{className:"mt-1 text-sm text-gray-500",children:"Chế độ xem trước - Hiển thị đáp án đúng"})]}),r(l,{variant:"outline",size:"sm",onClick:()=>oe(!1),children:[e(u,{className:"mr-2 h-4 w-4"}),"Quay lại Edit Mode"]})]})})}),e(E,{questionData:t,userAnswer:a,isReviewMode:!0,explanation:Y})]})}return r("div",{className:"space-y-6",children:[r(d,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50",children:[e("div",{className:"h-1.5 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500"}),e(m,{className:"pb-4",children:r("div",{className:"flex items-center justify-between",children:[r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 shadow-md",children:e(x,{className:"h-5 w-5 text-white"})}),r("div",{children:[e(h,{className:"text-xl font-bold text-gray-900",children:"Gắn nhãn hình ảnh"}),e("p",{className:"text-sm text-gray-500",children:"Tạo câu hỏi với hình ảnh, word bank và các vị trí cần điền"})]})]}),r(l,{variant:"outline",size:"sm",onClick:()=>{ce()&&oe(!0)},className:"gap-2 border-indigo-200 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700",children:[e(b,{className:"h-4 w-4"}),"Xem trước"]})]})}),r(c,{className:"space-y-6",children:[e("div",{className:"rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4",children:e(g,{defaultValue:Q,onChange:e=>X(Number(e.target.value)||1)})}),e("div",{className:"rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-5 shadow-sm",children:r("div",{className:"flex items-start gap-3",children:[e("div",{className:"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100",children:e(N,{className:"h-4 w-4 text-blue-600"})}),r("div",{className:"text-sm text-blue-900",children:[e("p",{className:"font-semibold text-blue-800",children:"Hướng dẫn tạo câu hỏi"}),r("ul",{className:"mt-2 space-y-1.5 text-blue-700",children:[r("li",{className:"flex items-center gap-2",children:[e("span",{className:"h-1.5 w-1.5 rounded-full bg-blue-400"}),"Upload hình ảnh cần gắn nhãn"]}),r("li",{className:"flex items-center gap-2",children:[e("span",{className:"h-1.5 w-1.5 rounded-full bg-blue-400"}),"Thêm các từ vào Word Bank (danh sách từ gợi ý)"]}),r("li",{className:"flex items-center gap-2",children:[e("span",{className:"h-1.5 w-1.5 rounded-full bg-blue-400"}),"Tạo các vị trí cần điền và chọn đáp án đúng từ Word Bank"]})]})]})]})}),r("div",{className:"space-y-2",children:[r(s,{className:"flex items-center gap-2 text-base font-semibold text-gray-800",children:[e(x,{className:"h-4 w-4 text-indigo-500"}),"Hình ảnh ",e("span",{className:"text-red-500",children:"*"})]}),e(y,{id:"label-picture-image",label:"Hình ảnh",accept:"image/*",value:R,onChange:e=>{G(e),_.clearErrors()},onPresignedUrlChange:e=>{le(e)},maxSize:5,placeholder:"Upload ảnh hoặc paste URL",required:!0,autoUpload:!0,prefix:"questions"}),me.imageUrl&&e("p",{className:"mt-1 text-sm text-red-600",children:me.imageUrl}),ne&&e("div",{className:"relative mt-2 h-64 w-full max-w-2xl overflow-hidden rounded border",children:e("img",{src:ne,alt:"Preview for question",className:"h-full w-full object-contain",onError:()=>{console.error("Failed to load image:",R)}})})]})]})]}),r(d,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-indigo-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-indigo-400 to-purple-400"}),e(m,{className:"pb-4",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-indigo-100",children:e(v,{className:"h-3.5 w-3.5 text-indigo-600"})}),r(h,{className:"text-lg font-semibold text-gray-800",children:["Word Bank ",r("span",{className:"text-sm font-normal text-gray-500",children:["(",W.filter(e=>e.trim()).length," từ)"]})]})]}),r(l,{type:"button",size:"sm",onClick:()=>{J([...W,""]),_.clearErrors()},className:"gap-2 border-green-200 text-green-600 hover:bg-green-50 hover:text-green-700",variant:"outline",children:[e(p,{className:"h-4 w-4"}),"Thêm từ"]})]})}),r(c,{className:"space-y-4",children:[me.wordBank&&r("div",{className:"flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4",children:[e("span",{className:"text-lg",children:"❌"}),e("p",{className:"text-sm text-red-600",children:me.wordBank})]}),e("div",{className:"grid gap-3 sm:grid-cols-2",children:W.map((t,a)=>r("div",{className:"group relative rounded-xl border-2 p-3 transition-all duration-200 "+(t.trim()?"border-green-200 bg-gradient-to-r from-green-50 to-emerald-50":"border-gray-200 bg-white hover:border-gray-300"),children:[r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg font-bold transition-all "+(t.trim()?"bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-md":"bg-gray-100 text-gray-600"),children:a+1}),e(i,{value:t,onChange:e=>((e,r)=>{const t=[...W];t[e]=r,J(t),_.clearErrors()})(a,e.target.value),placeholder:`Từ ${a+1}`,className:"flex-1 border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 "+(me.wordBankItems?.[a]?"border-red-400":"")}),e(l,{type:"button",variant:"ghost",size:"icon",onClick:()=>(e=>{J(W.filter((r,t)=>t!==e)),_.clearErrors()})(a),className:"text-gray-400 hover:bg-red-50 hover:text-red-500",children:e(f,{className:"h-4 w-4"})})]}),me.wordBankItems?.[a]&&e("p",{className:"mt-2 ml-11 text-sm text-red-600",children:me.wordBankItems[a]})]},a))}),0===W.length&&r("div",{className:"flex items-start gap-3 rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-4",children:[e("span",{className:"text-lg",children:"⚠️"}),e("p",{className:"text-sm text-amber-800",children:'Chưa có từ nào. Nhấn "Thêm từ" để bắt đầu.'})]})]})]}),r(d,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-purple-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-purple-400 to-pink-400"}),e(m,{className:"pb-4",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-purple-100",children:e(v,{className:"h-3.5 w-3.5 text-purple-600"})}),r(h,{className:"text-lg font-semibold text-gray-800",children:["Vị trí cần điền ",r("span",{className:"text-sm font-normal text-gray-500",children:["(",O.length," vị trí)"]})]})]}),r(l,{type:"button",size:"sm",onClick:()=>{const e=O.length>0?Math.max(...O.map(e=>e.number)):0,r={id:`label-${Date.now()}-${Math.random()}`,number:e+1,correctAnswer:""};K([...O,r]),_.clearErrors()},className:"gap-2 border-purple-200 text-purple-600 hover:bg-purple-50 hover:text-purple-700",variant:"outline",children:[e(p,{className:"h-4 w-4"}),"Thêm vị trí"]})]})}),r(c,{className:"space-y-4",children:[me.labels&&r("div",{className:"flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4",children:[e("span",{className:"text-lg",children:"❌"}),e("p",{className:"text-sm text-red-600",children:me.labels})]}),e("div",{className:"space-y-4",children:O.sort((e,r)=>e.number-r.number).map(t=>{const a=t.correctAnswer&&t.correctAnswer.trim();return e("div",{className:"group relative rounded-xl border-2 p-4 transition-all duration-200 "+(a?"border-green-300 bg-gradient-to-r from-green-50 to-emerald-50 shadow-md shadow-green-100":"border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm"),children:r("div",{className:"flex items-start gap-4",children:[e("div",{className:"flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl font-bold transition-all "+(a?"bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-md":"bg-purple-100 text-purple-700"),children:t.number}),r("div",{className:"flex-1 space-y-3",children:[r("div",{children:[r(s,{className:"text-sm font-semibold text-gray-700",children:["Đáp án đúng ",e("span",{className:"text-red-500",children:"*"})]}),e("div",{className:"mt-1.5",children:r(k,{value:t.correctAnswer||"",onValueChange:e=>de(t.id,"correctAnswer",e),children:[e(U,{className:"w-full border-gray-200 transition-all focus:border-indigo-300 focus:ring-indigo-200 "+(me.labelItems?.[t.id]?"border-red-400":""),children:e(j,{placeholder:"Chọn từ trong word bank"})}),e(C,{children:W.filter(e=>""!==e.trim()).map((r,t)=>e(B,{value:r,children:r},t))})]})}),me.labelItems?.[t.id]&&e("p",{className:"mt-1 text-sm text-red-600",children:me.labelItems[t.id]})]}),r("div",{className:"flex items-center gap-2",children:[e("input",{type:"checkbox",id:`prefilled-${t.id}`,checked:t.isPreFilled||!1,onChange:e=>de(t.id,"isPreFilled",e.target.checked),className:"h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"}),e(s,{htmlFor:`prefilled-${t.id}`,className:"text-sm font-normal text-gray-600 cursor-pointer",children:"Đánh dấu là từ gợi ý (đã điền sẵn)"})]})]}),e(l,{type:"button",variant:"ghost",size:"icon",onClick:()=>(e=>{K(O.filter(r=>r.id!==e));const r=O.filter(r=>r.id!==e).sort((e,r)=>e.number-r.number);K(r.map((e,r)=>({...e,number:r+1}))),_.clearErrors()})(t.id),className:"text-gray-400 hover:bg-red-50 hover:text-red-500",children:e(f,{className:"h-4 w-4"})})]})},t.id)})}),0===O.length&&r("div",{className:"flex items-start gap-3 rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-4",children:[e("span",{className:"text-lg",children:"⚠️"}),e("p",{className:"text-sm text-amber-800",children:'Chưa có vị trí nào. Nhấn "Thêm vị trí" để bắt đầu.'})]})]})]}),r(d,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-amber-400 to-orange-400"}),e(m,{className:"pb-4",children:r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 shadow-sm",children:e(w,{className:"h-4 w-4 text-white"})}),r("div",{children:[r("div",{className:"flex items-center gap-2",children:[e(h,{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(c,{children:e(o,{id:"explanation",value:Y,onChange:e=>Z(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 LabelThePictureCreator(r){return e(P,{...r})}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
export * from './LabelThePictureClient';
|
|
1
|
+
export*from"./LabelThePictureCreator";export*from"./LabelThePictureClient";
|
|
@@ -1,54 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
export const labelThePictureRegistration = {
|
|
3
|
-
component: LabelThePictureCreator,
|
|
4
|
-
transformInitialData: (initialData) => {
|
|
5
|
-
const content = initialData?.content;
|
|
6
|
-
const answer = initialData?.answer;
|
|
7
|
-
let creatorData = answer;
|
|
8
|
-
if (content && typeof content === 'object' && content.labels && Array.isArray(content.labels)) {
|
|
9
|
-
const mergedLabels = content.labels.map((label) => ({
|
|
10
|
-
...label,
|
|
11
|
-
correctAnswer: answer?.answers?.[label.id] || label.correctAnswer || '',
|
|
12
|
-
}));
|
|
13
|
-
const apiPoints = initialData?.total_points
|
|
14
|
-
? parseFloat(initialData.total_points)
|
|
15
|
-
: (initialData?.points || 1);
|
|
16
|
-
creatorData = {
|
|
17
|
-
...content,
|
|
18
|
-
labels: mergedLabels,
|
|
19
|
-
points: apiPoints,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
const apiPoints = initialData?.total_points
|
|
24
|
-
? parseFloat(initialData.total_points)
|
|
25
|
-
: (initialData?.points || answer?.points || 1);
|
|
26
|
-
if (creatorData) {
|
|
27
|
-
creatorData = { ...creatorData, points: apiPoints };
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return creatorData;
|
|
31
|
-
},
|
|
32
|
-
getExtraProps: (ctx) => ({
|
|
33
|
-
onUnsavedChangesChange: ctx.onUnsavedChangesChange,
|
|
34
|
-
validationRef: ctx.validationRef,
|
|
35
|
-
}),
|
|
36
|
-
wrapOnSave: (data, ctx) => ({
|
|
37
|
-
type: ctx.questionType,
|
|
38
|
-
content: ctx.state.content,
|
|
39
|
-
points: data?.points ?? ctx.state.points ?? 1,
|
|
40
|
-
level: ctx.state.level,
|
|
41
|
-
difficulty: ctx.state.difficulty,
|
|
42
|
-
skill: ctx.state.skill,
|
|
43
|
-
answer: data,
|
|
44
|
-
}),
|
|
45
|
-
wrapOnChange: (data, ctx) => ({
|
|
46
|
-
type: ctx.questionType,
|
|
47
|
-
content: ctx.state.content,
|
|
48
|
-
points: data?.points ?? ctx.state.points ?? 1,
|
|
49
|
-
level: ctx.state.level,
|
|
50
|
-
difficulty: ctx.state.difficulty,
|
|
51
|
-
skill: ctx.state.skill,
|
|
52
|
-
answer: data,
|
|
53
|
-
}),
|
|
54
|
-
};
|
|
1
|
+
import{LabelThePictureCreator as t}from"./LabelThePictureCreator";export const labelThePictureRegistration={component:t,transformInitialData:t=>{const e=t?.content,s=t?.answer;let n=s;if(e&&"object"==typeof e&&e.labels&&Array.isArray(e.labels)){const a=e.labels.map(t=>({...t,correctAnswer:s?.answers?.[t.id]||t.correctAnswer||""})),o=t?.total_points?parseFloat(t.total_points):t?.points||1;n={...e,labels:a,points:o}}else{const e=t?.total_points?parseFloat(t.total_points):t?.points||s?.points||1;n&&(n={...n,points:e})}return n},getExtraProps:t=>({onUnsavedChangesChange:t.onUnsavedChangesChange,validationRef:t.validationRef}),wrapOnSave:(t,e)=>({type:e.questionType,content:e.state.content,points:t?.points??e.state.points??1,level:e.state.level,difficulty:e.state.difficulty,skill:e.state.skill,answer:t}),wrapOnChange:(t,e)=>({type:e.questionType,content:e.state.content,points:t?.points??e.state.points??1,level:e.state.level,difficulty:e.state.difficulty,skill:e.state.skill,answer:t})};
|