@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,617 +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, Lightbulb, ImageIcon, Type, ToggleLeft, BookOpen } from 'lucide-react';
|
|
11
|
-
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
12
|
-
import { ImagePreview } from '../../../../components/ui/image-preview';
|
|
13
|
-
import { LookPictureFillWordHintClient } from './LookPictureFillWordHintClient';
|
|
14
|
-
import { Switch } from '../../../../components/ui/switch';
|
|
15
|
-
import { useDebouncedCallback, usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
16
|
-
import { useExamQuestionStore } from '../../../../shared/lib/stores/examQuestionStore';
|
|
17
|
-
// Helper: Calculate group number for a question index based on groups config
|
|
18
|
-
function getGroupNumberForIndex(questionIndex, groups) {
|
|
19
|
-
if (!groups || groups.length === 0) {
|
|
20
|
-
return 1; // Default to group 1 if no groups config
|
|
21
|
-
}
|
|
22
|
-
let currentIndex = 0;
|
|
23
|
-
for (const group of groups) {
|
|
24
|
-
if (questionIndex < currentIndex + group.questionCount) {
|
|
25
|
-
return group.number;
|
|
26
|
-
}
|
|
27
|
-
currentIndex += group.questionCount;
|
|
28
|
-
}
|
|
29
|
-
return groups[groups.length - 1]?.number || 1;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* LookPictureFillWordHintCreator - Single Question Mode
|
|
33
|
-
*
|
|
34
|
-
* This component handles ONE question at a time. Navigation between questions
|
|
35
|
-
* is handled by the parent ExamCreator component.
|
|
36
|
-
*
|
|
37
|
-
* API Format (from questionGroup.payload.hintLetters):
|
|
38
|
-
* - hintLetters: ["a", "e", "i", "o", "u"]
|
|
39
|
-
*/
|
|
40
|
-
function LookPictureFillWordHintContent({ initialData, onSave, onSaveToQuestionBank, onCancel, onChange, onUnsavedChangesChange, externalErrors, groups, questionIndexInPart = 0, partId = '', validationRef, }) {
|
|
41
|
-
// Access exam question store to find and update shared hintLetters for questions in same group
|
|
42
|
-
const questionsState = useExamQuestionStore((state) => state.questions);
|
|
43
|
-
const currentQuestionIndex = useExamQuestionStore((state) => state.currentQuestionIndex);
|
|
44
|
-
const upsertQuestionData = useExamQuestionStore((state) => state.upsertQuestionData);
|
|
45
|
-
const getQuestionData = useExamQuestionStore((state) => state.getQuestionData);
|
|
46
|
-
// New: Use shared hintLetters by partId + groupNumber
|
|
47
|
-
const setSharedHintLetters = useExamQuestionStore((state) => state.setSharedHintLetters);
|
|
48
|
-
const getSharedHintLetters = useExamQuestionStore((state) => state.getSharedHintLetters);
|
|
49
|
-
// Get current question data from store (if exists) - this has priority over initialData
|
|
50
|
-
// because store contains the latest user input even if not saved yet
|
|
51
|
-
const getStoredQuestionData = () => {
|
|
52
|
-
const storedData = getQuestionData(currentQuestionIndex);
|
|
53
|
-
if (storedData?.answer) {
|
|
54
|
-
return storedData.answer;
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
};
|
|
58
|
-
// Find hintLetters from other questions in the same group (within same part)
|
|
59
|
-
// This allows sharing hintLetters across questions with same groupNumber
|
|
60
|
-
const findSharedHintLetters = (targetGroupNumber) => {
|
|
61
|
-
// Get the part info to find questions in same part
|
|
62
|
-
// Questions in same part share the same groups config
|
|
63
|
-
const allQuestions = Object.values(questionsState);
|
|
64
|
-
for (const entry of allQuestions) {
|
|
65
|
-
if (!entry?.data?.answer)
|
|
66
|
-
continue;
|
|
67
|
-
const answerData = entry.data.answer;
|
|
68
|
-
// Check if this question has hintLetters and same type
|
|
69
|
-
if (entry.data.type === 'LOOK_PICTURE_FILL_WORD_HINT') {
|
|
70
|
-
// Check hintLetters in answer data
|
|
71
|
-
if (answerData?.hintLetters && Array.isArray(answerData.hintLetters) && answerData.hintLetters.length > 0) {
|
|
72
|
-
// Check if same groupNumber
|
|
73
|
-
const subQuestion = answerData?.subQuestions?.[0];
|
|
74
|
-
if (subQuestion?.groupNumber === targetGroupNumber) {
|
|
75
|
-
return answerData.hintLetters;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return [];
|
|
81
|
-
};
|
|
82
|
-
// Create a default sub-question
|
|
83
|
-
const createDefaultSubQuestion = () => {
|
|
84
|
-
// Get current question's group number based on position in part
|
|
85
|
-
const groupNumber = getGroupNumberForIndex(questionIndexInPart, groups);
|
|
86
|
-
return {
|
|
87
|
-
id: `sq-${Date.now()}-${Math.random()}`,
|
|
88
|
-
imageUrl: '',
|
|
89
|
-
word: '',
|
|
90
|
-
correctAnswer: '',
|
|
91
|
-
points: 1,
|
|
92
|
-
groupNumber,
|
|
93
|
-
};
|
|
94
|
-
};
|
|
95
|
-
// Initialize from initialData or create new
|
|
96
|
-
// Supports both:
|
|
97
|
-
// 1. New API format: content.imageUrl, content.word, answer.correctAnswer
|
|
98
|
-
// 2. Legacy format: subQuestions array (for backward compatibility)
|
|
99
|
-
const initializeData = () => {
|
|
100
|
-
// Check for new API format first (content has imageUrl and word directly)
|
|
101
|
-
// This is the format from question-reverse-transform.ts
|
|
102
|
-
const content = initialData?.content;
|
|
103
|
-
const answer = initialData?.answer;
|
|
104
|
-
if (content?.imageUrl || content?.word || answer?.correctAnswer) {
|
|
105
|
-
const pointsFromInitialData = initialData?.points || 1;
|
|
106
|
-
return {
|
|
107
|
-
id: `sq-${Date.now()}-imported`,
|
|
108
|
-
imageUrl: content?.imageUrl || '',
|
|
109
|
-
word: content?.word || '',
|
|
110
|
-
correctAnswer: answer?.correctAnswer || '',
|
|
111
|
-
points: pointsFromInitialData,
|
|
112
|
-
groupNumber: getGroupNumberForIndex(questionIndexInPart, groups),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
// Legacy format: subQuestions array
|
|
116
|
-
if (initialData?.subQuestions && initialData.subQuestions.length > 0) {
|
|
117
|
-
console.log('⚠️ Using legacy format (subQuestions)');
|
|
118
|
-
const subQuestion = initialData.subQuestions[0];
|
|
119
|
-
const pointsFromInitialData = initialData?.points || subQuestion.points || 1;
|
|
120
|
-
return {
|
|
121
|
-
...subQuestion,
|
|
122
|
-
points: pointsFromInitialData,
|
|
123
|
-
groupNumber: getGroupNumberForIndex(questionIndexInPart, groups),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
return createDefaultSubQuestion();
|
|
127
|
-
};
|
|
128
|
-
// State for current question
|
|
129
|
-
const [currentQuestion, setCurrentQuestion] = useState(initializeData);
|
|
130
|
-
// Get current group number for initialization
|
|
131
|
-
const initialGroupNum = getGroupNumberForIndex(questionIndexInPart, groups);
|
|
132
|
-
// hintLetters: Simple string input for hint letters (comma separated)
|
|
133
|
-
// API format: hintLetters is an array like ["a", "e", "i", "o", "u"]
|
|
134
|
-
const [hintLettersInput, setHintLettersInput] = useState(() => {
|
|
135
|
-
console.log('🔍 [useState init] Initializing hintLettersInput', {
|
|
136
|
-
partId,
|
|
137
|
-
initialGroupNum,
|
|
138
|
-
questionIndexInPart,
|
|
139
|
-
});
|
|
140
|
-
// Priority 1: Check shared hintLetters from store (by partId + groupNumber)
|
|
141
|
-
// This is the most reliable source as it's updated when any question in the group is saved
|
|
142
|
-
if (partId) {
|
|
143
|
-
const storedSharedHintLetters = getSharedHintLetters(partId, initialGroupNum);
|
|
144
|
-
console.log('🔍 [useState init] Store hintLetters:', {
|
|
145
|
-
key: `${partId}_${initialGroupNum}`,
|
|
146
|
-
value: storedSharedHintLetters,
|
|
147
|
-
});
|
|
148
|
-
if (storedSharedHintLetters && storedSharedHintLetters.length > 0) {
|
|
149
|
-
console.log('✅ [useState init] Using store hintLetters:', storedSharedHintLetters);
|
|
150
|
-
return storedSharedHintLetters.join(', ');
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// Priority 2: New format - hintLetters in answer from question-reverse-transform.ts
|
|
154
|
-
const answerHintLetters = initialData?.answer?.hintLetters;
|
|
155
|
-
if (answerHintLetters && Array.isArray(answerHintLetters) && answerHintLetters.length > 0) {
|
|
156
|
-
console.log('✅ [useState init] Using answer.hintLetters:', answerHintLetters);
|
|
157
|
-
return answerHintLetters.join(', ');
|
|
158
|
-
}
|
|
159
|
-
// Priority 3: Handle hintLetters array directly (from API transform via dedicated-component-router)
|
|
160
|
-
if (initialData?.hintLetters && Array.isArray(initialData.hintLetters) && initialData.hintLetters.length > 0) {
|
|
161
|
-
console.log('✅ [useState init] Using initialData.hintLetters:', initialData.hintLetters);
|
|
162
|
-
return initialData.hintLetters.join(', ');
|
|
163
|
-
}
|
|
164
|
-
// Priority 4: Legacy - Handle hintLettersByGroup if present
|
|
165
|
-
if (initialData?.hintLettersByGroup) {
|
|
166
|
-
const letters = initialData.hintLettersByGroup[initialGroupNum];
|
|
167
|
-
if (letters && Array.isArray(letters)) {
|
|
168
|
-
console.log('✅ [useState init] Using hintLettersByGroup:', letters);
|
|
169
|
-
return letters.join(', ');
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
// Priority 5: Try to find shared hintLetters from other questions in same group (legacy fallback)
|
|
173
|
-
const sharedHintLetters = findSharedHintLetters(initialGroupNum);
|
|
174
|
-
if (sharedHintLetters.length > 0) {
|
|
175
|
-
console.log('✅ [useState init] Using findSharedHintLetters:', sharedHintLetters);
|
|
176
|
-
return sharedHintLetters.join(', ');
|
|
177
|
-
}
|
|
178
|
-
console.log('⚠️ [useState init] No hintLetters found, returning empty');
|
|
179
|
-
return '';
|
|
180
|
-
});
|
|
181
|
-
const [caseSensitive, setCaseSensitive] = useState(initialData?.caseSensitive || false);
|
|
182
|
-
const [explanation, setExplanation] = useState(initialData?.answer?.explanation || initialData?.explanation || '');
|
|
183
|
-
const [isClientMode, setIsClientMode] = useState(false);
|
|
184
|
-
const [addToQuestionBank, setAddToQuestionBank] = useState(false);
|
|
185
|
-
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
186
|
-
// Inline validation errors
|
|
187
|
-
const [validationErrors, setValidationErrors] = useState({});
|
|
188
|
-
// Store the last saved state for comparison
|
|
189
|
-
const savedStateRef = useRef('');
|
|
190
|
-
// Track presigned URLs for images
|
|
191
|
-
const [imagePreviewUrl, setImagePreviewUrl] = useState('');
|
|
192
|
-
// Refs for change tracking
|
|
193
|
-
const previousInitialDataRef = useRef(initialData);
|
|
194
|
-
const previousQuestionIndexRef = useRef(questionIndexInPart); // Track question index instead of ID
|
|
195
|
-
const isInitializingRef = useRef(false);
|
|
196
|
-
const hasInitializedRef = useRef(false); // Track if component has been initialized
|
|
197
|
-
const onChangeRef = useRef(onChange);
|
|
198
|
-
const previousPayloadRef = useRef('');
|
|
199
|
-
// Current group number
|
|
200
|
-
const currentGroupNumber = currentQuestion.groupNumber || 1;
|
|
201
|
-
// Ref for onUnsavedChangesChange callback
|
|
202
|
-
const onUnsavedChangesChangeRef = useRef(onUnsavedChangesChange);
|
|
203
|
-
useEffect(() => {
|
|
204
|
-
onUnsavedChangesChangeRef.current = onUnsavedChangesChange;
|
|
205
|
-
}, [onUnsavedChangesChange]);
|
|
206
|
-
// Update onChange ref
|
|
207
|
-
useEffect(() => {
|
|
208
|
-
onChangeRef.current = onChange;
|
|
209
|
-
}, [onChange]);
|
|
210
|
-
// Helper function to create current state string for comparison
|
|
211
|
-
const getCurrentStateString = () => {
|
|
212
|
-
return JSON.stringify({
|
|
213
|
-
imageUrl: currentQuestion.imageUrl,
|
|
214
|
-
word: currentQuestion.word,
|
|
215
|
-
correctAnswer: currentQuestion.correctAnswer,
|
|
216
|
-
points: currentQuestion.points,
|
|
217
|
-
hintLetters: getHintLettersArray(),
|
|
218
|
-
caseSensitive,
|
|
219
|
-
explanation,
|
|
220
|
-
});
|
|
221
|
-
};
|
|
222
|
-
// Initialize saved state when initialData changes (navigation to new question)
|
|
223
|
-
useEffect(() => {
|
|
224
|
-
if (isInitializingRef.current)
|
|
225
|
-
return;
|
|
226
|
-
// Set initial saved state after a short delay to allow state to settle
|
|
227
|
-
const timer = setTimeout(() => {
|
|
228
|
-
savedStateRef.current = getCurrentStateString();
|
|
229
|
-
setHasUnsavedChanges(false);
|
|
230
|
-
}, 150);
|
|
231
|
-
return () => clearTimeout(timer);
|
|
232
|
-
}, [questionIndexInPart]); // Reset when navigating to different question
|
|
233
|
-
// Detect unsaved changes by comparing current state with saved state
|
|
234
|
-
useEffect(() => {
|
|
235
|
-
if (isInitializingRef.current)
|
|
236
|
-
return;
|
|
237
|
-
if (!savedStateRef.current)
|
|
238
|
-
return; // Skip if no saved state yet
|
|
239
|
-
const currentState = getCurrentStateString();
|
|
240
|
-
const hasChanges = currentState !== savedStateRef.current;
|
|
241
|
-
if (hasChanges !== hasUnsavedChanges) {
|
|
242
|
-
setHasUnsavedChanges(hasChanges);
|
|
243
|
-
onUnsavedChangesChangeRef.current?.(hasChanges);
|
|
244
|
-
}
|
|
245
|
-
}, [currentQuestion, hintLettersInput, caseSensitive, explanation, hasUnsavedChanges]);
|
|
246
|
-
// Debounced onChange callback
|
|
247
|
-
const debouncedOnChange = useDebouncedCallback((data) => {
|
|
248
|
-
onChangeRef.current?.(data);
|
|
249
|
-
}, 300);
|
|
250
|
-
// Sync from initialData when:
|
|
251
|
-
// 1. First initialization with valid data (hasInitializedRef is false and initialData has valid content), OR
|
|
252
|
-
// 2. User navigated to a different question (questionIndexInPart changed)
|
|
253
|
-
// This prevents resetting state when parent updates initialData due to onChange callback
|
|
254
|
-
useEffect(() => {
|
|
255
|
-
const questionIndexChanged = questionIndexInPart !== previousQuestionIndexRef.current;
|
|
256
|
-
// Check for valid data in both new format (content/answer) and legacy format (subQuestions)
|
|
257
|
-
const content = initialData?.content;
|
|
258
|
-
const answer = initialData?.answer;
|
|
259
|
-
const hasNewFormatData = !!(content?.imageUrl || content?.word || answer?.correctAnswer);
|
|
260
|
-
const hasLegacyFormatData = !!(initialData?.subQuestions && initialData.subQuestions.length > 0);
|
|
261
|
-
const hasValidInitialData = hasNewFormatData || hasLegacyFormatData;
|
|
262
|
-
// Only sync if:
|
|
263
|
-
// 1. First initialization with valid data (and not already initialized), OR
|
|
264
|
-
// 2. User navigated to a different question (questionIndexInPart changed)
|
|
265
|
-
// IMPORTANT: Do NOT sync when parent updates initialData due to save/onChange callback
|
|
266
|
-
// This prevents resetting user input after save
|
|
267
|
-
const shouldSyncForFirstInit = !hasInitializedRef.current && hasValidInitialData;
|
|
268
|
-
const shouldSyncForNavigation = questionIndexChanged;
|
|
269
|
-
// Skip sync if we're already initialized and index didn't change
|
|
270
|
-
// This prevents the bug where "Đáp án đúng" input is cleared after save
|
|
271
|
-
if (!shouldSyncForFirstInit && !shouldSyncForNavigation) {
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
if (shouldSyncForFirstInit || shouldSyncForNavigation) {
|
|
275
|
-
isInitializingRef.current = true;
|
|
276
|
-
previousInitialDataRef.current = initialData;
|
|
277
|
-
previousQuestionIndexRef.current = questionIndexInPart;
|
|
278
|
-
// Mark as initialized - either with valid data or as empty new question
|
|
279
|
-
// This prevents re-syncing when parent updates initialData after save
|
|
280
|
-
hasInitializedRef.current = true;
|
|
281
|
-
if (initialData) {
|
|
282
|
-
// Get the group number for current question
|
|
283
|
-
const groupNum = getGroupNumberForIndex(questionIndexInPart, groups);
|
|
284
|
-
// Handle NEW API format: content.imageUrl, content.word, answer.correctAnswer
|
|
285
|
-
if (hasNewFormatData) {
|
|
286
|
-
const pointsFromInitialData = initialData?.points || 1;
|
|
287
|
-
setCurrentQuestion({
|
|
288
|
-
id: `sq-${Date.now()}-synced`,
|
|
289
|
-
imageUrl: content?.imageUrl || '',
|
|
290
|
-
word: content?.word || '',
|
|
291
|
-
correctAnswer: answer?.correctAnswer || '',
|
|
292
|
-
points: pointsFromInitialData,
|
|
293
|
-
groupNumber: groupNum,
|
|
294
|
-
});
|
|
295
|
-
// Handle hintLetters from answer (new format)
|
|
296
|
-
const answerHintLetters = answer?.hintLetters;
|
|
297
|
-
if (answerHintLetters && Array.isArray(answerHintLetters) && answerHintLetters.length > 0) {
|
|
298
|
-
setHintLettersInput(answerHintLetters.join(', '));
|
|
299
|
-
}
|
|
300
|
-
// Handle caseSensitive from answer
|
|
301
|
-
if (answer?.caseSensitive !== undefined) {
|
|
302
|
-
setCaseSensitive(answer.caseSensitive);
|
|
303
|
-
}
|
|
304
|
-
// Handle explanation from answer (new format)
|
|
305
|
-
if (answer?.explanation !== undefined) {
|
|
306
|
-
setExplanation(answer.explanation);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
// Handle LEGACY format: subQuestions array
|
|
310
|
-
else if (hasLegacyFormatData) {
|
|
311
|
-
console.log('⚠️ [sync effect] Using legacy format (subQuestions)');
|
|
312
|
-
const pointsFromInitialData = initialData?.points || initialData.subQuestions[0].points || 1;
|
|
313
|
-
setCurrentQuestion({
|
|
314
|
-
...initialData.subQuestions[0],
|
|
315
|
-
points: pointsFromInitialData,
|
|
316
|
-
groupNumber: groupNum,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
// Handle hintLetters from various sources (for both formats)
|
|
320
|
-
// Priority 1: Check shared hintLetters from store (by partId + groupNumber)
|
|
321
|
-
// This is the most reliable source as it's updated when any question in the group is saved
|
|
322
|
-
let hintLettersSet = false;
|
|
323
|
-
if (partId) {
|
|
324
|
-
const storedSharedHintLetters = getSharedHintLetters(partId, groupNum);
|
|
325
|
-
if (storedSharedHintLetters && storedSharedHintLetters.length > 0) {
|
|
326
|
-
setHintLettersInput(storedSharedHintLetters.join(', '));
|
|
327
|
-
hintLettersSet = true;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
// Priority 2: From initialData.hintLetters
|
|
331
|
-
if (!hintLettersSet && initialData.hintLetters && Array.isArray(initialData.hintLetters) && initialData.hintLetters.length > 0) {
|
|
332
|
-
setHintLettersInput(initialData.hintLetters.join(', '));
|
|
333
|
-
hintLettersSet = true;
|
|
334
|
-
}
|
|
335
|
-
// Priority 3: Legacy - hintLettersByGroup
|
|
336
|
-
if (!hintLettersSet && initialData.hintLettersByGroup) {
|
|
337
|
-
const letters = initialData.hintLettersByGroup[groupNum];
|
|
338
|
-
if (letters && Array.isArray(letters)) {
|
|
339
|
-
setHintLettersInput(letters.join(', '));
|
|
340
|
-
hintLettersSet = true;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
// Priority 4: Legacy fallback - find shared hintLetters from other questions
|
|
344
|
-
if (!hintLettersSet && !hasNewFormatData) {
|
|
345
|
-
const sharedHintLetters = findSharedHintLetters(groupNum);
|
|
346
|
-
if (sharedHintLetters.length > 0) {
|
|
347
|
-
setHintLettersInput(sharedHintLetters.join(', '));
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
if (initialData.caseSensitive !== undefined) {
|
|
351
|
-
setCaseSensitive(initialData.caseSensitive);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
setTimeout(() => {
|
|
355
|
-
isInitializingRef.current = false;
|
|
356
|
-
}, 100);
|
|
357
|
-
}
|
|
358
|
-
}, [initialData, groups, questionIndexInPart]);
|
|
359
|
-
const { previewUrl: resolvedImagePreviewUrl } = usePresignedFileUrl(currentQuestion.imageUrl);
|
|
360
|
-
useEffect(() => {
|
|
361
|
-
setImagePreviewUrl(resolvedImagePreviewUrl || currentQuestion.imageUrl || '');
|
|
362
|
-
}, [currentQuestion.imageUrl, resolvedImagePreviewUrl]);
|
|
363
|
-
// Convert hintLettersInput string to array
|
|
364
|
-
const getHintLettersArray = () => {
|
|
365
|
-
return hintLettersInput
|
|
366
|
-
.split(',')
|
|
367
|
-
.map((l) => l.trim())
|
|
368
|
-
.filter((l) => l.length > 0);
|
|
369
|
-
};
|
|
370
|
-
// Call onChange when data changes
|
|
371
|
-
useEffect(() => {
|
|
372
|
-
if (isInitializingRef.current)
|
|
373
|
-
return;
|
|
374
|
-
const hintLettersArray = getHintLettersArray();
|
|
375
|
-
const payload = {
|
|
376
|
-
subQuestions: [currentQuestion],
|
|
377
|
-
hintLetters: hintLettersArray,
|
|
378
|
-
caseSensitive,
|
|
379
|
-
groups,
|
|
380
|
-
};
|
|
381
|
-
// Calculate total points
|
|
382
|
-
payload.points = currentQuestion.points || 1;
|
|
383
|
-
// Include explanation in payload
|
|
384
|
-
payload.explanation = explanation;
|
|
385
|
-
const payloadString = JSON.stringify(payload);
|
|
386
|
-
if (previousPayloadRef.current !== payloadString) {
|
|
387
|
-
previousPayloadRef.current = payloadString;
|
|
388
|
-
// IMPORTANT: Save hintLetters to shared store whenever they change
|
|
389
|
-
// This ensures hintLetters are available for other questions in the same group
|
|
390
|
-
// even before user clicks "Save" button
|
|
391
|
-
if (partId && hintLettersArray.length > 0) {
|
|
392
|
-
console.log('📤 [onChange effect] Saving hintLetters to store:', {
|
|
393
|
-
partId,
|
|
394
|
-
groupNumber: currentGroupNumber,
|
|
395
|
-
hintLettersArray,
|
|
396
|
-
});
|
|
397
|
-
setSharedHintLetters(partId, currentGroupNumber, hintLettersArray);
|
|
398
|
-
}
|
|
399
|
-
debouncedOnChange(payload);
|
|
400
|
-
}
|
|
401
|
-
}, [currentQuestion, hintLettersInput, caseSensitive, explanation, groups, debouncedOnChange, partId, currentGroupNumber, setSharedHintLetters]);
|
|
402
|
-
// Update current question field
|
|
403
|
-
const updateCurrentQuestion = (field, value) => {
|
|
404
|
-
setCurrentQuestion((prev) => ({ ...prev, [field]: value }));
|
|
405
|
-
};
|
|
406
|
-
// Format hint letters input on blur
|
|
407
|
-
const formatHintLettersInput = () => {
|
|
408
|
-
const letters = getHintLettersArray();
|
|
409
|
-
if (letters.length > 0) {
|
|
410
|
-
setHintLettersInput(letters.join(', '));
|
|
411
|
-
}
|
|
412
|
-
};
|
|
413
|
-
// Preview mode
|
|
414
|
-
const handlePreview = () => {
|
|
415
|
-
setIsClientMode(true);
|
|
416
|
-
};
|
|
417
|
-
// Sync hintLetters to all questions in the same group
|
|
418
|
-
// This ensures when navigating back/next, all questions in same group show the latest hintLetters
|
|
419
|
-
const syncHintLettersToSameGroup = (newHintLetters) => {
|
|
420
|
-
const targetGroupNumber = currentGroupNumber;
|
|
421
|
-
// Find all questions in same group and update their hintLetters
|
|
422
|
-
Object.entries(questionsState).forEach(([indexStr, entry]) => {
|
|
423
|
-
const questionIndex = Number(indexStr);
|
|
424
|
-
// Skip current question (it will be saved by parent)
|
|
425
|
-
if (questionIndex === currentQuestionIndex)
|
|
426
|
-
return;
|
|
427
|
-
// Check if this question is same type and same group
|
|
428
|
-
if (!entry?.data)
|
|
429
|
-
return;
|
|
430
|
-
if (entry.data.type !== 'LOOK_PICTURE_FILL_WORD_HINT')
|
|
431
|
-
return;
|
|
432
|
-
const answerData = entry.data.answer;
|
|
433
|
-
const subQuestion = answerData?.subQuestions?.[0];
|
|
434
|
-
// Check if same group number
|
|
435
|
-
if (subQuestion?.groupNumber !== targetGroupNumber)
|
|
436
|
-
return;
|
|
437
|
-
// Update this question's hintLetters in store
|
|
438
|
-
const updatedAnswer = {
|
|
439
|
-
...answerData,
|
|
440
|
-
hintLetters: newHintLetters,
|
|
441
|
-
};
|
|
442
|
-
upsertQuestionData(questionIndex, {
|
|
443
|
-
...entry.data,
|
|
444
|
-
answer: updatedAnswer,
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
};
|
|
448
|
-
// Validate form data - returns errors object and sets inline errors
|
|
449
|
-
const validateForm = () => {
|
|
450
|
-
const hintLettersArray = getHintLettersArray();
|
|
451
|
-
const errors = {};
|
|
452
|
-
if (hintLettersArray.length === 0) {
|
|
453
|
-
errors.hintLetters = 'Vui lòng nhập chữ cái gợi ý!';
|
|
454
|
-
}
|
|
455
|
-
if (!currentQuestion.imageUrl) {
|
|
456
|
-
errors.imageUrl = 'Vui lòng upload hình ảnh!';
|
|
457
|
-
}
|
|
458
|
-
if (!currentQuestion.word) {
|
|
459
|
-
errors.word = 'Vui lòng nhập từ có chỗ trống!';
|
|
460
|
-
}
|
|
461
|
-
if (!currentQuestion.correctAnswer) {
|
|
462
|
-
errors.correctAnswer = 'Vui lòng nhập đáp án đúng!';
|
|
463
|
-
}
|
|
464
|
-
// Set validation errors for inline display
|
|
465
|
-
setValidationErrors(errors);
|
|
466
|
-
if (Object.keys(errors).length > 0) {
|
|
467
|
-
return { isValid: false };
|
|
468
|
-
}
|
|
469
|
-
const saveData = {
|
|
470
|
-
subQuestions: [currentQuestion],
|
|
471
|
-
hintLetters: hintLettersArray,
|
|
472
|
-
caseSensitive,
|
|
473
|
-
groups,
|
|
474
|
-
};
|
|
475
|
-
saveData.points = currentQuestion.points || 1;
|
|
476
|
-
saveData.explanation = explanation;
|
|
477
|
-
return { isValid: true, saveData };
|
|
478
|
-
};
|
|
479
|
-
// Expose validation function to parent via ref
|
|
480
|
-
useEffect(() => {
|
|
481
|
-
if (validationRef) {
|
|
482
|
-
validationRef.current = {
|
|
483
|
-
triggerValidation: async () => {
|
|
484
|
-
const { isValid } = validateForm();
|
|
485
|
-
return isValid;
|
|
486
|
-
},
|
|
487
|
-
// Return current form data immediately (not debounced)
|
|
488
|
-
getFormData: () => {
|
|
489
|
-
const hintLettersArray = getHintLettersArray();
|
|
490
|
-
return {
|
|
491
|
-
subQuestions: [currentQuestion],
|
|
492
|
-
hintLetters: hintLettersArray,
|
|
493
|
-
caseSensitive,
|
|
494
|
-
groups,
|
|
495
|
-
points: currentQuestion.points || 1,
|
|
496
|
-
explanation,
|
|
497
|
-
};
|
|
498
|
-
},
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
}, [validationRef, currentQuestion, hintLettersInput, caseSensitive, explanation, groups]);
|
|
502
|
-
// Save question only
|
|
503
|
-
const handleSave = () => {
|
|
504
|
-
const { isValid, saveData } = validateForm();
|
|
505
|
-
if (!isValid || !saveData)
|
|
506
|
-
return;
|
|
507
|
-
const hintLettersArray = getHintLettersArray();
|
|
508
|
-
console.log('💾 [handleSave] Saving hintLetters to store:', {
|
|
509
|
-
partId,
|
|
510
|
-
currentGroupNumber,
|
|
511
|
-
hintLettersArray,
|
|
512
|
-
storeKey: `${partId}_${currentGroupNumber}`,
|
|
513
|
-
});
|
|
514
|
-
// Save shared hintLetters to store by partId + groupNumber
|
|
515
|
-
// This is the primary sync mechanism - simple and reliable
|
|
516
|
-
if (partId) {
|
|
517
|
-
setSharedHintLetters(partId, currentGroupNumber, hintLettersArray);
|
|
518
|
-
console.log('✅ [handleSave] hintLetters saved to store');
|
|
519
|
-
}
|
|
520
|
-
else {
|
|
521
|
-
console.log('⚠️ [handleSave] partId is empty, skipping store save');
|
|
522
|
-
}
|
|
523
|
-
// Legacy: Also sync to other questions in same group (for backward compatibility)
|
|
524
|
-
syncHintLettersToSameGroup(hintLettersArray);
|
|
525
|
-
// Update saved state to current state (mark as saved)
|
|
526
|
-
savedStateRef.current = getCurrentStateString();
|
|
527
|
-
setHasUnsavedChanges(false);
|
|
528
|
-
onUnsavedChangesChangeRef.current?.(false);
|
|
529
|
-
onSave?.(saveData);
|
|
530
|
-
};
|
|
531
|
-
// Save and add to question bank
|
|
532
|
-
const handleSaveToQuestionBank = () => {
|
|
533
|
-
const { isValid, saveData } = validateForm();
|
|
534
|
-
if (!isValid || !saveData)
|
|
535
|
-
return;
|
|
536
|
-
const hintLettersArray = getHintLettersArray();
|
|
537
|
-
// Save shared hintLetters to store by partId + groupNumber
|
|
538
|
-
// This is the primary sync mechanism - simple and reliable
|
|
539
|
-
if (partId) {
|
|
540
|
-
setSharedHintLetters(partId, currentGroupNumber, hintLettersArray);
|
|
541
|
-
}
|
|
542
|
-
// Legacy: Also sync to other questions in same group (for backward compatibility)
|
|
543
|
-
syncHintLettersToSameGroup(hintLettersArray);
|
|
544
|
-
// Update saved state to current state (mark as saved)
|
|
545
|
-
savedStateRef.current = getCurrentStateString();
|
|
546
|
-
setHasUnsavedChanges(false);
|
|
547
|
-
onUnsavedChangesChangeRef.current?.(false);
|
|
548
|
-
onSaveToQuestionBank?.(saveData);
|
|
549
|
-
};
|
|
550
|
-
// Client mode preview
|
|
551
|
-
if (isClientMode) {
|
|
552
|
-
const questionData = {
|
|
553
|
-
subQuestions: [{
|
|
554
|
-
...currentQuestion,
|
|
555
|
-
imageUrl: imagePreviewUrl || currentQuestion.imageUrl,
|
|
556
|
-
}],
|
|
557
|
-
hintLetters: getHintLettersArray(),
|
|
558
|
-
caseSensitive,
|
|
559
|
-
groups,
|
|
560
|
-
};
|
|
561
|
-
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: "Nh\u00ECn tranh \u0111i\u1EC1n t\u1EEB g\u1EE3i \u00FD (Preview)" }), _jsx("p", { className: "mt-1 text-sm text-gray-500", children: "Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc" })] }), _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(LookPictureFillWordHintClient, { questionData: questionData, autoFillCorrectAnswers: true, isReviewMode: true, explanation: explanation, onSubmit: () => { } })] }));
|
|
562
|
-
}
|
|
563
|
-
// Get display URL for current question image
|
|
564
|
-
const rawImageUrl = currentQuestion.imageUrl || '';
|
|
565
|
-
const isHttpUrl = rawImageUrl.startsWith('http');
|
|
566
|
-
const displayUrl = imagePreviewUrl || (isHttpUrl ? rawImageUrl : '');
|
|
567
|
-
const isLoadingImage = rawImageUrl && !isHttpUrl && !imagePreviewUrl;
|
|
568
|
-
// Preview word with answer
|
|
569
|
-
const renderWordPreview = () => {
|
|
570
|
-
if (!currentQuestion.word || !currentQuestion.correctAnswer)
|
|
571
|
-
return null;
|
|
572
|
-
const word = currentQuestion.word;
|
|
573
|
-
const answerStr = currentQuestion.correctAnswer;
|
|
574
|
-
const answers = answerStr
|
|
575
|
-
.split(',')
|
|
576
|
-
.map((a) => a.trim())
|
|
577
|
-
.filter((a) => a.length > 0);
|
|
578
|
-
const placeholderRegex = /\{(\d+)\}/g;
|
|
579
|
-
const matches = word.match(placeholderRegex);
|
|
580
|
-
const blankCount = matches ? matches.length : 0;
|
|
581
|
-
let result = word;
|
|
582
|
-
for (let i = 0; i < Math.min(answers.length, blankCount); i++) {
|
|
583
|
-
result = result.replace(`{${i}}`, `[${answers[i]}]`);
|
|
584
|
-
}
|
|
585
|
-
return (_jsxs("div", { className: "mt-3 rounded-lg border-2 border-blue-200 bg-blue-50 p-2", children: [_jsx("p", { className: "text-blue-800", children: result }), answers.length !== blankCount && (_jsxs("p", { className: "mt-1 text-xs text-amber-600", children: ["\u26A0\uFE0F S\u1ED1 \u0111\u00E1p \u00E1n (", answers.length, ") kh\u00F4ng kh\u1EDBp v\u1EDBi s\u1ED1 ch\u1ED7 tr\u1ED1ng (", blankCount, ")"] }))] }));
|
|
586
|
-
};
|
|
587
|
-
return (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-purple-100/50", children: [_jsx("div", { className: "h-1.5 bg-gradient-to-r from-purple-500 via-indigo-500 to-blue-500" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-indigo-600 shadow-md", children: _jsx(ImageIcon, { className: "h-5 w-5 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-xl font-bold text-gray-900", children: "Nh\u00ECn tranh \u0111i\u1EC1n t\u1EEB g\u1EE3i \u00FD" }), _jsx("div", { className: "mt-1 inline-flex items-center gap-1.5 rounded-md bg-purple-100 px-2.5 py-1 text-sm font-medium text-purple-700 ring-1 ring-inset ring-purple-700/10", children: _jsxs("span", { children: ["\uD83D\uDCDA Nh\u00F3m ", currentGroupNumber] }) })] })] }), _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, { id: "points-header", value: currentQuestion.points || 1, onChange: (e) => {
|
|
588
|
-
updateCurrentQuestion('points', Number(e.target.value) || 1);
|
|
589
|
-
} }) }), _jsx("div", { className: "rounded-xl border border-purple-200 bg-gradient-to-r from-purple-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-purple-100", children: _jsx(Lightbulb, { className: "h-4 w-4 text-purple-600" }) }), _jsxs("div", { className: "text-sm text-purple-900", children: [_jsx("p", { className: "font-semibold text-purple-800", children: "H\u01B0\u1EDBng d\u1EABn t\u1EA1o c\u00E2u h\u1ECFi" }), _jsxs("ul", { className: "mt-2 space-y-1.5 text-purple-700", children: [_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-purple-400" }), "Nh\u1EADp c\u00E1c ch\u1EEF c\u00E1i g\u1EE3i \u00FD (ph\u00E2n c\u00E1ch b\u1EB1ng d\u1EA5u ph\u1EA9y)"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-purple-400" }), "S\u1EED d\u1EE5ng ", _jsx("code", { className: "mx-1 rounded bg-purple-100 px-1.5 py-0.5 font-mono text-purple-800", children: '{0}' }), ", ", _jsx("code", { className: "mx-1 rounded bg-purple-100 px-1.5 py-0.5 font-mono text-purple-800", children: '{1}' }), "... \u0111\u1EC3 \u0111\u00E1nh d\u1EA5u ch\u1ED7 tr\u1ED1ng"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-purple-400" }), "V\u00ED d\u1EE5 t\u1EEB: ", _jsxs("code", { className: "ml-1 rounded bg-purple-100 px-1.5 py-0.5 font-mono text-purple-800 select-all cursor-pointer", title: "Click \u0111\u1EC3 copy", children: ["c", '{0}', '{1}'] }), " v\u1EDBi \u0111\u00E1p \u00E1n ", _jsx("code", { className: "rounded bg-purple-100 px-1.5 py-0.5 font-mono text-purple-800", children: "a, r" })] })] })] })] }) }), _jsxs("div", { className: `rounded-lg border-2 p-4 ${validationErrors.hintLetters ? 'border-red-300 bg-red-50' : 'border-amber-200 bg-amber-50'}`, children: [_jsxs(Label, { htmlFor: "hint-letters", className: validationErrors.hintLetters ? 'text-red-800' : 'text-amber-800', children: ["Ch\u1EEF c\u00E1i g\u1EE3i \u00FD ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "hint-letters", type: "text", value: hintLettersInput, onBlur: formatHintLettersInput, onChange: (e) => {
|
|
590
|
-
setHintLettersInput(e.target.value);
|
|
591
|
-
if (validationErrors.hintLetters) {
|
|
592
|
-
setValidationErrors(prev => ({ ...prev, hintLetters: undefined }));
|
|
593
|
-
}
|
|
594
|
-
}, placeholder: "V\u00ED d\u1EE5: a, e, i, o, u", className: `mt-1 bg-white ${validationErrors.hintLetters ? 'border-red-500 focus:ring-red-500' : ''}` }), validationErrors.hintLetters ? (_jsx("p", { className: "mt-1 text-sm text-red-600", children: validationErrors.hintLetters })) : (_jsx("p", { className: "mt-1 text-xs text-amber-700", children: "Nh\u1EADp c\u00E1c ch\u1EEF c\u00E1i g\u1EE3i \u00FD, ph\u00E2n c\u00E1ch b\u1EB1ng d\u1EA5u ph\u1EA9y" }))] }), _jsxs("div", { className: "flex items-center justify-between rounded-xl border-2 border-gray-200 bg-white p-4 transition-all hover:border-gray-300", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100", children: _jsx(ToggleLeft, { className: "h-4 w-4 text-gray-600" }) }), _jsxs("div", { className: "space-y-0.5", children: [_jsx(Label, { htmlFor: "case-sensitive", className: "font-semibold text-gray-800", children: "Ph\u00E2n bi\u1EC7t hoa/th\u01B0\u1EDDng" }), _jsx("p", { className: "text-sm text-gray-500", children: "B\u1EADt n\u1EBFu \u0111\u00E1p \u00E1n c\u1EA7n ph\u00E2n bi\u1EC7t ch\u1EEF hoa/th\u01B0\u1EDDng" })] })] }), _jsx(Switch, { id: "case-sensitive", checked: caseSensitive, onCheckedChange: setCaseSensitive })] }), externalErrors && externalErrors.length > 0 && (_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("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-red-100", children: _jsx("span", { className: "text-lg", children: "\u274C" }) }), _jsxs("div", { className: "text-sm", children: [_jsx("p", { className: "font-semibold text-red-800", children: "L\u1ED7i:" }), _jsx("ul", { className: "mt-1 list-inside list-disc text-red-700", children: externalErrors.map((error, index) => (_jsx("li", { children: error }, index))) })] })] }))] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-blue-400 to-indigo-400" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-blue-100", children: _jsx(Type, { className: "h-3.5 w-3.5 text-blue-600" }) }), _jsx(CardTitle, { className: "text-lg font-semibold text-gray-800", children: "N\u1ED9i dung c\u00E2u h\u1ECFi" }), _jsxs("span", { className: "inline-flex items-center gap-1 rounded-md bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700 ring-1 ring-inset ring-purple-700/10", children: ["\uD83D\uDCDA Nh\u00F3m ", currentGroupNumber] })] }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-indigo-500" }), "H\u00ECnh \u1EA3nh ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(FileUpload, { id: "image", label: "H\u00ECnh \u1EA3nh", accept: "image/*", value: currentQuestion.imageUrl || '', onChange: (url) => {
|
|
595
|
-
updateCurrentQuestion('imageUrl', url);
|
|
596
|
-
if (validationErrors.imageUrl) {
|
|
597
|
-
setValidationErrors(prev => ({ ...prev, imageUrl: undefined }));
|
|
598
|
-
}
|
|
599
|
-
}, onPresignedUrlChange: (presignedUrl) => {
|
|
600
|
-
setImagePreviewUrl(presignedUrl);
|
|
601
|
-
}, maxSize: 5, placeholder: "Upload \u1EA3nh ho\u1EB7c paste URL", required: true, autoUpload: true, prefix: "questions" }), validationErrors.imageUrl && (_jsx("p", { className: "mt-1 text-sm text-red-600", children: validationErrors.imageUrl })), isLoadingImage && (_jsx(ImagePreview, { src: displayUrl, alt: "Preview for question", className: "mt-2 h-48 w-full max-w-md rounded-lg border border-gray-200" }))] }), _jsxs("div", { children: [_jsxs(Label, { htmlFor: "word", children: ["T\u1EEB c\u00F3 ch\u1ED7 tr\u1ED1ng ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "word", value: currentQuestion.word || '', onChange: (e) => {
|
|
602
|
-
updateCurrentQuestion('word', e.target.value);
|
|
603
|
-
if (validationErrors.word) {
|
|
604
|
-
setValidationErrors(prev => ({ ...prev, word: undefined }));
|
|
605
|
-
}
|
|
606
|
-
}, placeholder: "V\u00ED d\u1EE5: c{0}r, B{0}{1}k, f{0}o{1}", className: `mt-1 ${validationErrors.word ? 'border-red-500 focus:ring-red-500' : ''}`, maxLength: 50 }), validationErrors.word ? (_jsx("p", { className: "mt-1 text-sm text-red-600", children: validationErrors.word })) : (_jsxs("div", { className: "mt-1 space-y-1", children: [_jsxs("p", { className: "text-xs text-gray-500", children: ["S\u1EED d\u1EE5ng ", '{0}', ", ", '{1}', ", ", '{2}', ",... \u0111\u1EC3 \u0111\u00E1nh d\u1EA5u ch\u1ED7 tr\u1ED1ng c\u1EA7n \u0111i\u1EC1n"] }), _jsxs("p", { className: "text-xs text-gray-400", children: ["V\u00ED d\u1EE5: ", _jsxs("code", { className: "bg-gray-100 px-1 rounded select-all cursor-pointer", title: "Click \u0111\u1EC3 copy", children: ["c", '{0}', '{1}'] })] })] }))] }), _jsxs("div", { children: [_jsxs(Label, { htmlFor: "answer", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "answer", value: currentQuestion.correctAnswer || '', onChange: (e) => {
|
|
607
|
-
updateCurrentQuestion('correctAnswer', e.target.value);
|
|
608
|
-
if (validationErrors.correctAnswer) {
|
|
609
|
-
setValidationErrors(prev => ({ ...prev, correctAnswer: undefined }));
|
|
610
|
-
}
|
|
611
|
-
}, placeholder: "V\u00ED d\u1EE5: a ho\u1EB7c o,o (c\u00E1ch nhau b\u1EB1ng d\u1EA5u ph\u1EA9y)", className: `mt-1 ${validationErrors.correctAnswer ? 'border-red-500 focus:ring-red-500' : ''}` }), validationErrors.correctAnswer ? (_jsx("p", { className: "mt-1 text-sm text-red-600", children: validationErrors.correctAnswer })) : (_jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Nh\u1EADp c\u00E1c \u0111\u00E1p \u00E1n c\u00E1ch nhau b\u1EB1ng d\u1EA5u ph\u1EA9y. S\u1ED1 \u0111\u00E1p \u00E1n ph\u1EA3i kh\u1EDBp v\u1EDBi s\u1ED1 placeholder" })), renderWordPreview()] })] })] }), _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" }) })] })] }));
|
|
612
|
-
}
|
|
613
|
-
export function LookPictureFillWordHintCreator(props) {
|
|
614
|
-
return _jsx(LookPictureFillWordHintContent, { ...props });
|
|
615
|
-
}
|
|
616
|
-
// Legacy export for backward compatibility
|
|
617
|
-
export const LookPictureFillWordHint = LookPictureFillWordHintCreator;
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useState as r,useRef as n,useEffect as i}from"react";import{Button as s}from"../../../../components/ui/button";import{Label as o}from"../../../../components/ui/label";import{Input as a}from"../../../../components/ui/input";import{Textarea as l}from"../../../../components/ui/textarea";import{Card as c,CardContent as d,CardHeader as h,CardTitle as m}from"../../../../components/ui/card";import{PointsInput as u}from"../../../../components/ui/points-input";import{Pencil as p,Eye as g,Lightbulb as x,ImageIcon as b,Type as f,ToggleLeft as N,BookOpen as w}from"lucide-react";import{FileUpload as v}from"../../../../components/ui/file-upload";import{ImagePreview as y}from"../../../../components/ui/image-preview";import{LookPictureFillWordHintClient as L}from"./LookPictureFillWordHintClient";import{Switch as U}from"../../../../components/ui/switch";import{useDebouncedCallback as A,usePresignedFileUrl as S}from"../../../../shared/lib/hooks";import{useExamQuestionStore as C}from"../../../../shared/lib/stores/examQuestionStore";function j(e,t){if(!t||0===t.length)return 1;let r=0;for(const n of t){if(e<r+n.questionCount)return n.number;r+=n.questionCount}return t[t.length-1]?.number||1}function k({initialData:k,onSave:Q,onSaveToQuestionBank:V,onCancel:I,onChange:q,onUnsavedChangesChange:D,externalErrors:F,groups:H,questionIndexInPart:P=0,partId:B="",validationRef:G}){const $=C(e=>e.questions),O=(C(e=>e.currentQuestionIndex),C(e=>e.upsertQuestionData),C(e=>e.getQuestionData),C(e=>e.setSharedHintLetters)),T=C(e=>e.getSharedHintLetters),W=e=>{const t=Object.values($);for(const r of t){if(!r?.data?.answer)continue;const t=r.data.answer;if("LOOK_PICTURE_FILL_WORD_HINT"===r.data.type&&t?.hintLetters&&Array.isArray(t.hintLetters)&&t.hintLetters.length>0){const r=t?.subQuestions?.[0];if(r?.groupNumber===e)return t.hintLetters}}return[]},[z,R]=r(()=>{const e=k?.content,t=k?.answer;if(e?.imageUrl||e?.word||t?.correctAnswer){const r=k?.points||1;return{id:`sq-${Date.now()}-imported`,imageUrl:e?.imageUrl||"",word:e?.word||"",correctAnswer:t?.correctAnswer||"",points:r,groupNumber:j(P,H)}}if(k?.subQuestions&&k.subQuestions.length>0){console.log("⚠️ Using legacy format (subQuestions)");const e=k.subQuestions[0],t=k?.points||e.points||1;return{...e,points:t,groupNumber:j(P,H)}}return(()=>{const e=j(P,H);return{id:`sq-${Date.now()}-${Math.random()}`,imageUrl:"",word:"",correctAnswer:"",points:1,groupNumber:e}})()}),_=j(P,H),[M,E]=r(()=>{if(console.log("🔍 [useState init] Initializing hintLettersInput",{partId:B,initialGroupNum:_,questionIndexInPart:P}),B){const e=T(B,_);if(console.log("🔍 [useState init] Store hintLetters:",{key:`${B}_${_}`,value:e}),e&&e.length>0)return console.log("✅ [useState init] Using store hintLetters:",e),e.join(", ")}const e=k?.answer?.hintLetters;if(e&&Array.isArray(e)&&e.length>0)return console.log("✅ [useState init] Using answer.hintLetters:",e),e.join(", ");if(k?.hintLetters&&Array.isArray(k.hintLetters)&&k.hintLetters.length>0)return console.log("✅ [useState init] Using initialData.hintLetters:",k.hintLetters),k.hintLetters.join(", ");if(k?.hintLettersByGroup){const e=k.hintLettersByGroup[_];if(e&&Array.isArray(e))return console.log("✅ [useState init] Using hintLettersByGroup:",e),e.join(", ")}const t=W(_);return t.length>0?(console.log("✅ [useState init] Using findSharedHintLetters:",t),t.join(", ")):(console.log("⚠️ [useState init] No hintLetters found, returning empty"),"")}),[J,K]=r(k?.caseSensitive||!1),[X,Y]=r(k?.answer?.explanation||k?.explanation||""),[Z,ee]=r(!1),[te,re]=r(!1),[ne,ie]=r(!1),[se,oe]=r({}),ae=n(""),[le,ce]=r(""),de=n(k),he=n(P),me=n(!1),ue=n(!1),pe=n(q),ge=n(""),xe=z.groupNumber||1,be=n(D);i(()=>{be.current=D},[D]),i(()=>{pe.current=q},[q]);const fe=()=>JSON.stringify({imageUrl:z.imageUrl,word:z.word,correctAnswer:z.correctAnswer,points:z.points,hintLetters:ve(),caseSensitive:J,explanation:X});i(()=>{if(me.current)return;const e=setTimeout(()=>{ae.current=fe(),ie(!1)},150);return()=>clearTimeout(e)},[P]),i(()=>{if(me.current)return;if(!ae.current)return;const e=fe()!==ae.current;e!==ne&&(ie(e),be.current?.(e))},[z,M,J,X,ne]);const Ne=A(e=>{pe.current?.(e)},300);i(()=>{const e=P!==he.current,t=k?.content,r=k?.answer,n=!!(t?.imageUrl||t?.word||r?.correctAnswer),i=!!(k?.subQuestions&&k.subQuestions.length>0),s=n||i,o=!ue.current&&s;if((o||e)&&(o||e)){if(me.current=!0,de.current=k,he.current=P,ue.current=!0,k){const e=j(P,H);if(n){const n=k?.points||1;R({id:`sq-${Date.now()}-synced`,imageUrl:t?.imageUrl||"",word:t?.word||"",correctAnswer:r?.correctAnswer||"",points:n,groupNumber:e});const i=r?.hintLetters;i&&Array.isArray(i)&&i.length>0&&E(i.join(", ")),void 0!==r?.caseSensitive&&K(r.caseSensitive),void 0!==r?.explanation&&Y(r.explanation)}else if(i){console.log("⚠️ [sync effect] Using legacy format (subQuestions)");const t=k?.points||k.subQuestions[0].points||1;R({...k.subQuestions[0],points:t,groupNumber:e})}let s=!1;if(B){const t=T(B,e);t&&t.length>0&&(E(t.join(", ")),s=!0)}if(!s&&k.hintLetters&&Array.isArray(k.hintLetters)&&k.hintLetters.length>0&&(E(k.hintLetters.join(", ")),s=!0),!s&&k.hintLettersByGroup){const t=k.hintLettersByGroup[e];t&&Array.isArray(t)&&(E(t.join(", ")),s=!0)}if(!s&&!n){const t=W(e);t.length>0&&E(t.join(", "))}void 0!==k.caseSensitive&&K(k.caseSensitive)}setTimeout(()=>{me.current=!1},100)}},[k,H,P]);const{previewUrl:we}=S(z.imageUrl);i(()=>{ce(we||z.imageUrl||"")},[z.imageUrl,we]);const ve=()=>M.split(",").map(e=>e.trim()).filter(e=>e.length>0);i(()=>{if(me.current)return;const e=ve(),t={subQuestions:[z],hintLetters:e,caseSensitive:J,groups:H};t.points=z.points||1,t.explanation=X;const r=JSON.stringify(t);ge.current!==r&&(ge.current=r,B&&e.length>0&&(console.log("📤 [onChange effect] Saving hintLetters to store:",{partId:B,groupNumber:xe,hintLettersArray:e}),O(B,xe,e)),Ne(t))},[z,M,J,X,H,Ne,B,xe,O]);const ye=(e,t)=>{R(r=>({...r,[e]:t}))},Le=()=>{const e=ve(),t={};if(0===e.length&&(t.hintLetters="Vui lòng nhập chữ cái gợi ý!"),z.imageUrl||(t.imageUrl="Vui lòng upload hình ảnh!"),z.word||(t.word="Vui lòng nhập từ có chỗ trống!"),z.correctAnswer||(t.correctAnswer="Vui lòng nhập đáp án đúng!"),oe(t),Object.keys(t).length>0)return{isValid:!1};const r={subQuestions:[z],hintLetters:e,caseSensitive:J,groups:H};return r.points=z.points||1,r.explanation=X,{isValid:!0,saveData:r}};i(()=>{G&&(G.current={triggerValidation:async()=>{const{isValid:e}=Le();return e},getFormData:()=>{const e=ve();return{subQuestions:[z],hintLetters:e,caseSensitive:J,groups:H,points:z.points||1,explanation:X}}})},[G,z,M,J,X,H]);if(Z){const r={subQuestions:[{...z,imageUrl:le||z.imageUrl}],hintLetters:ve(),caseSensitive:J,groups:H};return t("div",{className:"space-y-6",children:[e(c,{children:e(h,{children:t("div",{className:"flex items-center justify-between",children:[t("div",{children:[e(m,{children:"Nhìn tranh điền từ gợi ý (Preview)"}),e("p",{className:"mt-1 text-sm text-gray-500",children:"Chế độ xem trước"})]}),t(s,{variant:"outline",size:"sm",onClick:()=>ee(!1),children:[e(p,{className:"mr-2 h-4 w-4"}),"Quay lại Edit Mode"]})]})})}),e(L,{questionData:r,autoFillCorrectAnswers:!0,isReviewMode:!0,explanation:X,onSubmit:()=>{}})]})}const Ue=z.imageUrl||"",Ae=Ue.startsWith("http"),Se=le||(Ae?Ue:""),Ce=Ue&&!Ae&&!le;return t("div",{className:"space-y-6",children:[t(c,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-purple-100/50",children:[e("div",{className:"h-1.5 bg-gradient-to-r from-purple-500 via-indigo-500 to-blue-500"}),e(h,{className:"pb-4",children:t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-indigo-600 shadow-md",children:e(b,{className:"h-5 w-5 text-white"})}),t("div",{children:[e(m,{className:"text-xl font-bold text-gray-900",children:"Nhìn tranh điền từ gợi ý"}),e("div",{className:"mt-1 inline-flex items-center gap-1.5 rounded-md bg-purple-100 px-2.5 py-1 text-sm font-medium text-purple-700 ring-1 ring-inset ring-purple-700/10",children:t("span",{children:["📚 Nhóm ",xe]})})]})]}),t(s,{variant:"outline",size:"sm",onClick:()=>{ee(!0)},className:"gap-2 border-indigo-200 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700",children:[e(g,{className:"h-4 w-4"}),"Xem trước"]})]})}),t(d,{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(u,{id:"points-header",value:z.points||1,onChange:e=>{ye("points",Number(e.target.value)||1)}})}),e("div",{className:"rounded-xl border border-purple-200 bg-gradient-to-r from-purple-50 to-indigo-50 p-5 shadow-sm",children:t("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-purple-100",children:e(x,{className:"h-4 w-4 text-purple-600"})}),t("div",{className:"text-sm text-purple-900",children:[e("p",{className:"font-semibold text-purple-800",children:"Hướng dẫn tạo câu hỏi"}),t("ul",{className:"mt-2 space-y-1.5 text-purple-700",children:[t("li",{className:"flex items-center gap-2",children:[e("span",{className:"h-1.5 w-1.5 rounded-full bg-purple-400"}),"Nhập các chữ cái gợi ý (phân cách bằng dấu phẩy)"]}),t("li",{className:"flex items-center gap-2",children:[e("span",{className:"h-1.5 w-1.5 rounded-full bg-purple-400"}),"Sử dụng ",e("code",{className:"mx-1 rounded bg-purple-100 px-1.5 py-0.5 font-mono text-purple-800",children:"{0}"}),", ",e("code",{className:"mx-1 rounded bg-purple-100 px-1.5 py-0.5 font-mono text-purple-800",children:"{1}"}),"... để đánh dấu chỗ trống"]}),t("li",{className:"flex items-center gap-2",children:[e("span",{className:"h-1.5 w-1.5 rounded-full bg-purple-400"}),"Ví dụ từ: ",t("code",{className:"ml-1 rounded bg-purple-100 px-1.5 py-0.5 font-mono text-purple-800 select-all cursor-pointer",title:"Click để copy",children:["c","{0}","{1}"]})," với đáp án ",e("code",{className:"rounded bg-purple-100 px-1.5 py-0.5 font-mono text-purple-800",children:"a, r"})]})]})]})]})}),t("div",{className:"rounded-lg border-2 p-4 "+(se.hintLetters?"border-red-300 bg-red-50":"border-amber-200 bg-amber-50"),children:[t(o,{htmlFor:"hint-letters",className:se.hintLetters?"text-red-800":"text-amber-800",children:["Chữ cái gợi ý ",e("span",{className:"text-red-500",children:"*"})]}),e(a,{id:"hint-letters",type:"text",value:M,onBlur:()=>{const e=ve();e.length>0&&E(e.join(", "))},onChange:e=>{E(e.target.value),se.hintLetters&&oe(e=>({...e,hintLetters:void 0}))},placeholder:"Ví dụ: a, e, i, o, u",className:"mt-1 bg-white "+(se.hintLetters?"border-red-500 focus:ring-red-500":"")}),se.hintLetters?e("p",{className:"mt-1 text-sm text-red-600",children:se.hintLetters}):e("p",{className:"mt-1 text-xs text-amber-700",children:"Nhập các chữ cái gợi ý, phân cách bằng dấu phẩy"})]}),t("div",{className:"flex items-center justify-between rounded-xl border-2 border-gray-200 bg-white p-4 transition-all hover:border-gray-300",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100",children:e(N,{className:"h-4 w-4 text-gray-600"})}),t("div",{className:"space-y-0.5",children:[e(o,{htmlFor:"case-sensitive",className:"font-semibold text-gray-800",children:"Phân biệt hoa/thường"}),e("p",{className:"text-sm text-gray-500",children:"Bật nếu đáp án cần phân biệt chữ hoa/thường"})]})]}),e(U,{id:"case-sensitive",checked:J,onCheckedChange:K})]}),F&&F.length>0&&t("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("div",{className:"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-red-100",children:e("span",{className:"text-lg",children:"❌"})}),t("div",{className:"text-sm",children:[e("p",{className:"font-semibold text-red-800",children:"Lỗi:"}),e("ul",{className:"mt-1 list-inside list-disc text-red-700",children:F.map((t,r)=>e("li",{children:t},r))})]})]})]})]}),t(c,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-blue-400 to-indigo-400"}),e(h,{className:"pb-4",children:t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-6 w-6 items-center justify-center rounded-md bg-blue-100",children:e(f,{className:"h-3.5 w-3.5 text-blue-600"})}),e(m,{className:"text-lg font-semibold text-gray-800",children:"Nội dung câu hỏi"}),t("span",{className:"inline-flex items-center gap-1 rounded-md bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700 ring-1 ring-inset ring-purple-700/10",children:["📚 Nhóm ",xe]})]})}),t(d,{className:"space-y-4",children:[t("div",{className:"space-y-2",children:[t(o,{className:"flex items-center gap-2 text-base font-semibold text-gray-800",children:[e(b,{className:"h-4 w-4 text-indigo-500"}),"Hình ảnh ",e("span",{className:"text-red-500",children:"*"})]}),e(v,{id:"image",label:"Hình ảnh",accept:"image/*",value:z.imageUrl||"",onChange:e=>{ye("imageUrl",e),se.imageUrl&&oe(e=>({...e,imageUrl:void 0}))},onPresignedUrlChange:e=>{ce(e)},maxSize:5,placeholder:"Upload ảnh hoặc paste URL",required:!0,autoUpload:!0,prefix:"questions"}),se.imageUrl&&e("p",{className:"mt-1 text-sm text-red-600",children:se.imageUrl}),Ce&&e(y,{src:Se,alt:"Preview for question",className:"mt-2 h-48 w-full max-w-md rounded-lg border border-gray-200"})]}),t("div",{children:[t(o,{htmlFor:"word",children:["Từ có chỗ trống ",e("span",{className:"text-red-500",children:"*"})]}),e(a,{id:"word",value:z.word||"",onChange:e=>{ye("word",e.target.value),se.word&&oe(e=>({...e,word:void 0}))},placeholder:"Ví dụ: c{0}r, B{0}{1}k, f{0}o{1}",className:"mt-1 "+(se.word?"border-red-500 focus:ring-red-500":""),maxLength:50}),se.word?e("p",{className:"mt-1 text-sm text-red-600",children:se.word}):t("div",{className:"mt-1 space-y-1",children:[t("p",{className:"text-xs text-gray-500",children:["Sử dụng ","{0}",", ","{1}",", ","{2}",",... để đánh dấu chỗ trống cần điền"]}),t("p",{className:"text-xs text-gray-400",children:["Ví dụ: ",t("code",{className:"bg-gray-100 px-1 rounded select-all cursor-pointer",title:"Click để copy",children:["c","{0}","{1}"]})]})]})]}),t("div",{children:[t(o,{htmlFor:"answer",children:["Đáp án đúng ",e("span",{className:"text-red-500",children:"*"})]}),e(a,{id:"answer",value:z.correctAnswer||"",onChange:e=>{ye("correctAnswer",e.target.value),se.correctAnswer&&oe(e=>({...e,correctAnswer:void 0}))},placeholder:"Ví dụ: a hoặc o,o (cách nhau bằng dấu phẩy)",className:"mt-1 "+(se.correctAnswer?"border-red-500 focus:ring-red-500":"")}),se.correctAnswer?e("p",{className:"mt-1 text-sm text-red-600",children:se.correctAnswer}):e("p",{className:"mt-1 text-xs text-gray-500",children:"Nhập các đáp án cách nhau bằng dấu phẩy. Số đáp án phải khớp với số placeholder"}),(()=>{if(!z.word||!z.correctAnswer)return null;const r=z.word,n=z.correctAnswer.split(",").map(e=>e.trim()).filter(e=>e.length>0),i=r.match(/\{(\d+)\}/g),s=i?i.length:0;let o=r;for(let e=0;e<Math.min(n.length,s);e++)o=o.replace(`{${e}}`,`[${n[e]}]`);return t("div",{className:"mt-3 rounded-lg border-2 border-blue-200 bg-blue-50 p-2",children:[e("p",{className:"text-blue-800",children:o}),n.length!==s&&t("p",{className:"mt-1 text-xs text-amber-600",children:["⚠️ Số đáp án (",n.length,") không khớp với số chỗ trống (",s,")"]})]})})()]})]})]}),t(c,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-amber-400 to-orange-400"}),e(h,{className:"pb-4",children:t("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"})}),t("div",{children:[t("div",{className:"flex items-center gap-2",children:[e(m,{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(d,{children:e(l,{id:"explanation",value:X,onChange:e=>Y(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 LookPictureFillWordHintCreator(t){return e(k,{...t})}export const LookPictureFillWordHint=LookPictureFillWordHintCreator;
|