@tinyweb_dev/oe-exam-sdk 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/exam-create.js +1 -179
- package/dist/api/exam-create.types.js +1 -1
- package/dist/api/exam-questions.d.ts +1 -1
- package/dist/api/exam-questions.js +1 -98
- package/dist/api/exam-questions.types.d.ts +45 -1
- package/dist/api/exam-questions.types.js +1 -1
- package/dist/api/exam-taking.js +1 -73
- package/dist/api/exam-taking.types.js +1 -1
- package/dist/api/index.js +1 -6
- package/dist/api/result-review.js +1 -50
- package/dist/api/result-review.types.js +1 -1
- package/dist/components/CueCardEditor.js +1 -58
- package/dist/components/common/ResolvedImage.js +1 -15
- package/dist/components/exams/CompletionDialog.d.ts +8 -0
- package/dist/components/exams/CompletionDialog.js +1 -0
- package/dist/components/exams/CreateExamInfoForm.d.ts +4 -1
- package/dist/components/exams/CreateExamInfoForm.js +1 -35
- package/dist/components/exams/CreateExamPageContainer.d.ts +3 -0
- package/dist/components/exams/CreateExamPageContainer.js +1 -103
- package/dist/components/exams/ExamCreator.d.ts +9 -3
- package/dist/components/exams/ExamCreator.js +1 -89
- package/dist/components/exams/ExamPartTabs.d.ts +1 -1
- package/dist/components/exams/ExamPartTabs.js +1 -40
- package/dist/components/exams/ExamPreviewDialog.d.ts +20 -0
- package/dist/components/exams/ExamPreviewDialog.js +1 -0
- package/dist/components/exams/ExamQuestionGrid.d.ts +1 -1
- package/dist/components/exams/ExamQuestionGrid.js +1 -18
- package/dist/components/exams/ExamQuestionsPageContainer.d.ts +3 -0
- package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
- package/dist/components/exams/ExamViewDialog.d.ts +24 -0
- package/dist/components/exams/ExamViewDialog.js +1 -0
- package/dist/components/exams/PartAudioUploader.js +1 -9
- package/dist/components/exams/TemplateSelector.js +1 -66
- package/dist/components/exams/UnsavedChangesDialog.d.ts +8 -0
- package/dist/components/exams/UnsavedChangesDialog.js +1 -0
- package/dist/components/exams/api.d.ts +1 -1
- package/dist/components/exams/api.js +1 -3
- package/dist/components/exams/create-exam.texts.d.ts +58 -0
- package/dist/components/exams/create-exam.texts.js +1 -0
- package/dist/components/exams/create-exam.validation.js +1 -18
- package/dist/components/exams/exam-question-status.d.ts +9 -0
- package/dist/components/exams/exam-question-status.js +1 -0
- package/dist/components/exams/exam-template.utils.js +1 -18
- package/dist/components/exams/index.d.ts +11 -1
- package/dist/components/exams/index.js +1 -10
- package/dist/components/exams/take/ExamTakingPageContainer.js +1 -153
- package/dist/components/exams/take/GenericQuestionRenderer.js +1 -14
- package/dist/components/exams/take/exam-taking.utils.js +1 -88
- package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -24
- package/dist/components/exams/take/index.js +1 -2
- package/dist/components/exams/take/types.js +1 -1
- package/dist/components/index.js +1 -42
- package/dist/components/questions/_shared/config/question-types.config.js +1 -333
- package/dist/components/questions/_shared/constants/color-palette.js +1 -21
- package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +1 -105
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +1 -222
- package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +1 -140
- package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +1 -123
- package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +1 -50
- package/dist/components/questions/_shared/hooks/useQuestionFormState.js +1 -54
- package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +1 -123
- package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +1 -121
- package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +1 -70
- package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +1 -194
- package/dist/components/questions/_shared/index.js +1 -7
- package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
- package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
- package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +1 -3
- package/dist/components/questions/_shared/types/color-region.config.js +1 -1
- package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
- package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
- package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
- package/dist/components/questions/_shared/types/index.js +1 -12
- package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +1 -3
- package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +1 -3
- package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
- package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
- package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
- package/dist/components/questions/_shared/types/question-group.type.js +1 -11
- package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
- package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
- package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
- package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
- package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
- package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
- package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
- package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +1 -3
- package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
- package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
- package/dist/components/questions/_shared/utils/answer-builder.js +1 -79
- package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +1 -39
- package/dist/components/questions/_shared/utils/id-generator.js +1 -3
- package/dist/components/questions/_shared/utils/question-validation.js +1 -21
- package/dist/components/questions/components/ColorNode.js +1 -85
- package/dist/components/questions/components/ColorRegionClient.js +1 -545
- package/dist/components/questions/components/ColorRegionCreator.js +1 -420
- package/dist/components/questions/components/ColorRegionNode.js +1 -59
- package/dist/components/questions/components/CreatorGuide.js +1 -103
- package/dist/components/questions/components/NameNode.js +1 -26
- package/dist/components/questions/components/QuestionBankOption.js +1 -46
- package/dist/components/questions/components/QuestionBasicInfoForm.js +1 -9
- package/dist/components/questions/components/QuestionCommonFields.js +1 -33
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -258
- package/dist/components/questions/components/QuestionTypeSelector.js +1 -32
- package/dist/components/questions/components/QuestionTypeSpecificForms.js +1 -6
- package/dist/components/questions/components/RegionNode.js +1 -94
- package/dist/components/questions/components/index.js +1 -14
- package/dist/components/questions/creator/QuestionCreator.js +1 -90
- package/dist/components/questions/creator/QuestionGroupCreator.js +1 -119
- package/dist/components/questions/creator/dedicated-component-router.js +1 -79
- package/dist/components/questions/creator/index.js +1 -3
- package/dist/components/questions/creator/question-type-registry.js +1 -80
- package/dist/components/questions/groups/ArticlesGroupCard.js +1 -121
- package/dist/components/questions/groups/BasicQuestionGroupCard.js +1 -50
- package/dist/components/questions/groups/DocumentGroupCard.js +1 -114
- package/dist/components/questions/groups/MatchAnswerGroupCard.js +1 -25
- package/dist/components/questions/index.js +1 -10
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +1 -37
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +1 -351
- package/dist/components/questions/types/answer-the-question/register.js +1 -67
- package/dist/components/questions/types/answer-the-question/transform.js +1 -36
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +1 -141
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +1 -118
- package/dist/components/questions/types/arrange-letters-into-words/index.js +1 -2
- package/dist/components/questions/types/arrange-letters-into-words/register.js +1 -4
- package/dist/components/questions/types/arrange-letters-into-words/transform.js +1 -24
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +1 -64
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +1 -169
- package/dist/components/questions/types/choose-correct-adjective/index.js +1 -2
- package/dist/components/questions/types/choose-correct-adjective/register.js +1 -55
- package/dist/components/questions/types/choose-correct-adjective/transform.js +1 -16
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -131
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -581
- package/dist/components/questions/types/choose-the-correct-answer/index.js +1 -2
- package/dist/components/questions/types/choose-the-correct-answer/register.js +1 -101
- package/dist/components/questions/types/choose-the-correct-answer/transform.js +1 -43
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -275
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +1 -448
- package/dist/components/questions/types/fill-in-blank/index.js +1 -2
- package/dist/components/questions/types/fill-in-blank/register.js +1 -138
- package/dist/components/questions/types/fill-in-blank/transform.js +1 -59
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +1 -117
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +1 -671
- package/dist/components/questions/types/fill-missing-words-in-grid/index.js +1 -2
- package/dist/components/questions/types/fill-missing-words-in-grid/register.js +1 -37
- package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +1 -15
- package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +1 -262
- package/dist/components/questions/types/gn-speaking-interview/register.js +1 -121
- package/dist/components/questions/types/gn-speaking-interview/transform.js +1 -48
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +1 -16
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +1 -27
- package/dist/components/questions/types/image-object-matching/index.js +1 -2
- package/dist/components/questions/types/image-object-matching/register.js +1 -3
- package/dist/components/questions/types/image-object-matching/transform.js +1 -2
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +1 -102
- package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +1 -344
- package/dist/components/questions/types/label-the-picture/index.js +1 -2
- package/dist/components/questions/types/label-the-picture/register.js +1 -54
- package/dist/components/questions/types/label-the-picture/transform.js +1 -28
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +1 -78
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +1 -232
- package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +1 -2
- package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +1 -82
- package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +1 -27
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +1 -70
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +1 -386
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +1 -2
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +1 -100
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +1 -33
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +1 -106
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +1 -457
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +1 -2
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +1 -110
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +1 -35
- package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +1 -8
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +1 -22
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +1 -291
- package/dist/components/questions/types/listen-and-speak-answer/index.js +1 -2
- package/dist/components/questions/types/listen-and-speak-answer/register.js +1 -84
- package/dist/components/questions/types/listen-and-speak-answer/transform.js +1 -27
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +1 -32
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +1 -295
- package/dist/components/questions/types/listen-and-speak-compare-images/register.js +1 -79
- package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +1 -27
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +1 -34
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +1 -285
- package/dist/components/questions/types/listen-and-speak-image-group/register.js +1 -81
- package/dist/components/questions/types/listen-and-speak-image-group/transform.js +1 -26
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +1 -38
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +1 -385
- package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +1 -87
- package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +1 -32
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +1 -247
- package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +1 -59
- package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +1 -18
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +1 -256
- package/dist/components/questions/types/listen-and-speak-question-list/register.js +1 -81
- package/dist/components/questions/types/listen-and-speak-question-list/transform.js +1 -26
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +1 -230
- package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +1 -56
- package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +1 -16
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +1 -72
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +1 -230
- package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +1 -66
- package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +1 -42
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +1 -96
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +1 -238
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +1 -2
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +1 -76
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +1 -19
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +1 -134
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +1 -617
- package/dist/components/questions/types/look-picture-fill-word-hint/index.js +1 -2
- package/dist/components/questions/types/look-picture-fill-word-hint/register.js +1 -82
- package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +1 -29
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +1 -38
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +1 -359
- package/dist/components/questions/types/match-by-writing-answer/index.js +1 -3
- package/dist/components/questions/types/match-by-writing-answer/register.js +1 -58
- package/dist/components/questions/types/match-by-writing-answer/transform.js +1 -61
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +1 -16
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +1 -23
- package/dist/components/questions/types/read-and-color-objects/index.js +1 -2
- package/dist/components/questions/types/read-and-color-objects/register.js +1 -3
- package/dist/components/questions/types/read-and-color-objects/transform.js +1 -36
- package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +1 -125
- package/dist/components/questions/types/read-displayed-content/register.js +1 -64
- package/dist/components/questions/types/read-displayed-content/transform.js +1 -12
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +1 -64
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +1 -511
- package/dist/components/questions/types/read-passage-and-answer-questions/index.js +1 -2
- package/dist/components/questions/types/read-passage-and-answer-questions/register.js +1 -58
- package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +1 -28
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +1 -373
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -41
- package/dist/components/questions/types/speaking-conversation/register.js +1 -110
- package/dist/components/questions/types/speaking-conversation/transform.js +1 -34
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -74
- package/dist/components/questions/types/speaking-cue-card/register.js +1 -52
- package/dist/components/questions/types/speaking-cue-card/transform.js +1 -12
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +1 -10
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +1 -161
- package/dist/components/questions/types/speaking-describe-image/index.js +1 -2
- package/dist/components/questions/types/speaking-describe-image/register.js +1 -69
- package/dist/components/questions/types/speaking-describe-image/transform.js +1 -16
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +1 -234
- package/dist/components/questions/types/spontaneous-qa/register.js +1 -93
- package/dist/components/questions/types/spontaneous-qa/transform.js +1 -34
- package/dist/components/questions/types/true-false/TrueFalseClient.js +1 -75
- package/dist/components/questions/types/true-false/TrueFalseCreator.js +1 -244
- package/dist/components/questions/types/true-false/index.js +1 -2
- package/dist/components/questions/types/true-false/register.js +1 -77
- package/dist/components/questions/types/true-false/transform.js +1 -32
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +1 -233
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +1 -631
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +1 -28
- package/dist/components/questions/types/word-fill-paragraph/index.js +1 -2
- package/dist/components/questions/types/word-fill-paragraph/register.js +1 -136
- package/dist/components/questions/types/word-fill-paragraph/transform.js +1 -56
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +1 -138
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +1 -261
- package/dist/components/questions/types/word-fill-structured-form/index.js +1 -2
- package/dist/components/questions/types/word-fill-structured-form/register.js +1 -67
- package/dist/components/questions/types/word-fill-structured-form/transform.js +1 -40
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +1 -84
- package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +1 -192
- package/dist/components/questions/types/word-ordering/index.js +1 -2
- package/dist/components/questions/types/word-ordering/register.js +1 -46
- package/dist/components/questions/types/word-ordering/transform.js +1 -24
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +1 -98
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +1 -320
- package/dist/components/questions/types/write-a-short-letter/index.js +1 -2
- package/dist/components/questions/types/write-a-short-letter/register.js +1 -68
- package/dist/components/questions/types/write-a-short-letter/transform.js +1 -25
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +1 -61
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +1 -196
- package/dist/components/questions/types/write-correct-verb-form/index.js +1 -2
- package/dist/components/questions/types/write-correct-verb-form/register.js +1 -62
- package/dist/components/questions/types/write-correct-verb-form/transform.js +1 -17
- package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +1 -22
- package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +1 -174
- package/dist/components/questions/types/write-sentences/index.js +1 -2
- package/dist/components/questions/types/write-sentences/register.js +1 -58
- package/dist/components/questions/types/write-sentences/transform.js +1 -28
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +1 -96
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +1 -161
- package/dist/components/questions/types/write-short-paragraph/index.js +1 -2
- package/dist/components/questions/types/write-short-paragraph/register.js +1 -4
- package/dist/components/questions/types/write-short-paragraph/transform.js +1 -21
- package/dist/components/questions/viewer/QuestionGroupViewer.js +1 -36
- package/dist/components/questions/viewer/QuestionViewer.js +1 -446
- package/dist/components/questions/viewer/index.js +1 -2
- package/dist/components/results/ResultReviewPageContainer.js +1 -98
- package/dist/components/results/ReviewQuestionRenderer.js +1 -72
- package/dist/components/results/index.js +1 -2
- package/dist/components/results/result-review.utils.js +1 -6
- package/dist/components/results/review-data.js +1 -150
- package/dist/components/results/review-renderer.utils.js +1 -97
- package/dist/components/results/review-types.js +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +1 -138
- package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +1 -12
- package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +1 -18
- package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +1 -11
- package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +1 -11
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +1 -9
- package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +1 -13
- package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +1 -8
- package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +1 -6
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +1 -5
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +1 -9
- package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +1 -41
- package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -25
- package/dist/components/themes/cambridge-yle/index.js +1 -13
- package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +1 -24
- package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +1 -88
- package/dist/components/themes/english-certification/EcWordFillParagraph.js +1 -55
- package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +1 -156
- package/dist/components/themes/english-certification/EcWritingTask.js +1 -34
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -114
- package/dist/components/themes/english-certification/EnglishCertificationBrand.js +1 -11
- package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +1 -21
- package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +1 -15
- package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +1 -19
- package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +1 -148
- package/dist/components/themes/english-certification/EnglishCertificationHeader.js +1 -31
- package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +1 -71
- package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +1 -33
- package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +1 -16
- package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +1 -22
- package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +1 -37
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -72
- package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +1 -90
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +1 -18
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +1 -29
- package/dist/components/themes/english-certification/english-certification-theme.js +1 -58
- package/dist/components/themes/english-certification/grouped-question-utils.js +1 -218
- package/dist/components/themes/english-certification/index.js +1 -21
- package/dist/components/themes/index.js +1 -6
- package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +1 -170
- package/dist/components/themes/summer-sky/SummerSkyLayout.js +1 -221
- package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +1 -18
- package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +1 -194
- package/dist/components/themes/types.js +1 -1
- package/dist/components/ui/alert-dialog.js +1 -26
- package/dist/components/ui/alert.js +1 -22
- package/dist/components/ui/avatar.js +1 -12
- package/dist/components/ui/badge.js +1 -38
- package/dist/components/ui/button.js +1 -35
- package/dist/components/ui/calendar.js +1 -76
- package/dist/components/ui/card.js +1 -16
- package/dist/components/ui/carousel.js +1 -91
- package/dist/components/ui/checkbox.js +1 -9
- package/dist/components/ui/collapsible.js +1 -13
- package/dist/components/ui/command.js +1 -34
- package/dist/components/ui/confirm-dialog.js +1 -49
- package/dist/components/ui/dialog.js +1 -23
- package/dist/components/ui/drawer.js +1 -35
- package/dist/components/ui/dropdown-menu.js +1 -33
- package/dist/components/ui/file-upload.js +1 -155
- package/dist/components/ui/form.js +1 -62
- package/dist/components/ui/hover-card.js +1 -10
- package/dist/components/ui/image-preview.js +1 -29
- package/dist/components/ui/input.js +1 -8
- package/dist/components/ui/label.js +1 -9
- package/dist/components/ui/language-switcher.js +1 -15
- package/dist/components/ui/multi-select.js +1 -90
- package/dist/components/ui/points-input.js +1 -33
- package/dist/components/ui/popover.js +1 -26
- package/dist/components/ui/progress.js +1 -16
- package/dist/components/ui/radio-group.js +1 -12
- package/dist/components/ui/scroll-area.js +1 -11
- package/dist/components/ui/select.js +1 -38
- package/dist/components/ui/separator.js +1 -8
- package/dist/components/ui/sheet.js +1 -40
- package/dist/components/ui/single-select.js +1 -76
- package/dist/components/ui/skeleton.js +1 -6
- package/dist/components/ui/switch.js +1 -8
- package/dist/components/ui/table.js +1 -20
- package/dist/components/ui/tabs.js +1 -17
- package/dist/components/ui/textarea.js +1 -8
- package/dist/components/ui/tooltip.js +1 -11
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -19
- package/dist/mcp/config.js +1 -32
- package/dist/mcp/http-client.js +1 -48
- package/dist/mcp/index.js +1 -29
- package/dist/mcp/tools/_helpers.js +1 -25
- package/dist/mcp/tools/exams.js +1 -439
- package/dist/mcp/tools/index.js +1 -6
- package/dist/mcp/tools/results.js +1 -19
- package/dist/shared/constants/ApiConstant.js +1 -276
- package/dist/shared/constants/QueryKeyConstant.js +1 -112
- package/dist/shared/constants/exam-level.enum.js +1 -41
- package/dist/shared/constants/landings/placement-test.constants.js +1 -431
- package/dist/shared/constants/question-skills.js +1 -267
- package/dist/shared/constants/routes.js +1 -81
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.js +1 -7
- package/dist/shared/lib/hooks/index.js +1 -10
- package/dist/shared/lib/hooks/useAlertDialog.js +1 -57
- package/dist/shared/lib/hooks/useAudioPlayCounter.js +1 -30
- package/dist/shared/lib/hooks/useAudioPreview.js +1 -81
- package/dist/shared/lib/hooks/useAudioUpload.js +1 -24
- package/dist/shared/lib/hooks/useConfirmDialog.js +1 -89
- package/dist/shared/lib/hooks/useDebouncedCallback.js +1 -32
- package/dist/shared/lib/hooks/useDelayedAutoPlay.js +1 -12
- package/dist/shared/lib/hooks/useFileUpload.js +1 -24
- package/dist/shared/lib/hooks/useImageUrl.js +1 -5
- package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -10
- package/dist/shared/lib/hooks/useReactHookForm.js +1 -31
- package/dist/shared/lib/hooks/useToast.js +1 -62
- package/dist/shared/lib/i18n/index.js +1 -54
- package/dist/shared/lib/i18n/messages/en/admin.js +1 -2104
- package/dist/shared/lib/i18n/messages/en/auth.js +1 -86
- package/dist/shared/lib/i18n/messages/en/common.js +1 -140
- package/dist/shared/lib/i18n/messages/en/file-upload.js +1 -18
- package/dist/shared/lib/i18n/messages/en/student.js +1 -133
- package/dist/shared/lib/i18n/messages/en/terms.js +1 -37
- package/dist/shared/lib/i18n/messages/en.js +1 -15
- package/dist/shared/lib/i18n/messages/vi/admin.js +1 -2104
- package/dist/shared/lib/i18n/messages/vi/auth.js +1 -86
- package/dist/shared/lib/i18n/messages/vi/common.js +1 -140
- package/dist/shared/lib/i18n/messages/vi/file-upload.js +1 -18
- package/dist/shared/lib/i18n/messages/vi/student.js +1 -133
- package/dist/shared/lib/i18n/messages/vi/terms.js +1 -37
- package/dist/shared/lib/i18n/messages/vi.js +1 -15
- package/dist/shared/lib/i18n/types.js +1 -1
- package/dist/shared/lib/stores/examQuestionStore.js +1 -212
- package/dist/shared/lib/stores/localeStore.js +1 -10
- package/dist/shared/lib/utils/public-page.js +1 -44
- package/dist/shared/lib/utils/question-reverse-transform.d.ts +6 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +1 -0
- package/dist/shared/lib/utils/question-transform-types.js +1 -1
- package/dist/shared/lib/utils/question-transform-validation.d.ts +6 -0
- package/dist/shared/lib/utils/question-transform-validation.js +1 -0
- package/dist/shared/lib/utils/question-transform.d.ts +6 -2
- package/dist/shared/lib/utils/question-transform.js +1 -174
- package/dist/shared/lib/utils.js +1 -168
- package/dist/shared/lib/validation/choose-correct-answer.validation.js +1 -111
- package/dist/shared/lib/validation/label-the-picture.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +1 -46
- package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +1 -21
- package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +1 -34
- package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +1 -40
- package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +1 -45
- package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +1 -39
- package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +1 -32
- package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +1 -28
- package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +1 -12
- package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +1 -47
- package/dist/shared/lib/validation/read-and-color-objects.validation.js +1 -29
- package/dist/shared/lib/validation/read-displayed-content.validation.js +1 -28
- package/dist/shared/lib/validation/speaking-conversation.validation.js +1 -75
- package/dist/shared/lib/validation/speaking-describe-image.validation.js +1 -31
- package/dist/shared/lib/validation/spontaneous-qa.validation.js +1 -48
- package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +1 -38
- package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +1 -39
- package/dist/shared/lib/validation/word-fill-paragraph.validation.js +1 -43
- package/dist/shared/lib/validation/word-ordering.validation.js +1 -33
- package/dist/shared/types/branch.types.js +1 -6
- package/dist/shared/types/common.types.js +1 -50
- package/dist/shared/types/entities/exam-schedule.types.js +1 -5
- package/dist/shared/types/entities/exam.types.js +1 -1
- package/dist/shared/types/entities/question.types.js +1 -4
- package/dist/shared/types/entities/result.types.js +1 -4
- package/dist/shared/types/entities/user.types.js +1 -4
- package/dist/shared/types/exam-taking.types.js +1 -2
- package/dist/shared/types/history.types.js +1 -34
- package/dist/shared/types/index.js +1 -1
- package/dist/shared/types/questions/choose-correct-adjective.js +1 -4
- package/dist/shared/types/questions/choose-the-correct-answer.js +1 -9
- package/dist/shared/types/questions/fill-missing-words-in-grid.js +1 -11
- package/dist/shared/types/questions/index.js +1 -40
- package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-info-exchange.js +1 -3
- package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +1 -3
- package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-with-story-images.js +1 -3
- package/dist/shared/types/questions/look-picture-choose-correct-answer.js +1 -5
- package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +1 -9
- package/dist/shared/types/questions/look-picture-fill-word-hint.js +1 -7
- package/dist/shared/types/questions/read-displayed-content.js +1 -1
- package/dist/shared/types/questions/speaking-conversation.js +1 -7
- package/dist/shared/types/questions/speaking-describe-image.js +1 -1
- package/dist/shared/types/questions/spontaneous-qa.js +1 -1
- package/dist/shared/types/questions/write-correct-verb-form.js +1 -12
- package/dist/shared/types/role.types.js +1 -1
- package/dist/shared/types/structure-management.js +1 -28
- package/dist/shared/types/student.types.js +1 -54
- package/dist/shared/types/teacher-dashboard-rooms.js +1 -2
- package/package.json +12 -3
package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js
CHANGED
|
@@ -1,671 +1 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
|
-
import { Button } from '../../../../components/ui/button';
|
|
5
|
-
import { Label } from '../../../../components/ui/label';
|
|
6
|
-
import { Input } from '../../../../components/ui/input';
|
|
7
|
-
import { Textarea } from '../../../../components/ui/textarea';
|
|
8
|
-
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
9
|
-
import { PointsInput } from '../../../../components/ui/points-input';
|
|
10
|
-
import { Pencil, Eye, Trash2, Grid3X3, Lightbulb, CheckCircle2, BookOpen } from 'lucide-react';
|
|
11
|
-
import { toast } from 'sonner';
|
|
12
|
-
import { t } from '../../../../shared/lib/i18n';
|
|
13
|
-
import { FillMissingWordsInGridClient } from './FillMissingWordsInGridClient';
|
|
14
|
-
function FillMissingWordsInGridCreatorContent({ initialData, onSave, onCancel, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
15
|
-
onChange, onUnsavedChangesChange, externalErrors = [], }) {
|
|
16
|
-
// Grid dimensions - FIXED: 4 rows, 7 columns
|
|
17
|
-
const [rows, setRows] = useState(4); // Fixed to 4
|
|
18
|
-
const [columns, setColumns] = useState(7); // Fixed to 7
|
|
19
|
-
// Points/Score
|
|
20
|
-
const [points, setPoints] = useState(initialData?.points || 1);
|
|
21
|
-
// Explanation (optional)
|
|
22
|
-
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
23
|
-
// Grid data: 2D array, null = empty cell (missing word)
|
|
24
|
-
const [grid, setGrid] = useState(() => {
|
|
25
|
-
if (initialData?.content?.grid) {
|
|
26
|
-
return initialData.content.grid;
|
|
27
|
-
}
|
|
28
|
-
// Initialize empty grid
|
|
29
|
-
return Array(rows)
|
|
30
|
-
.fill(null)
|
|
31
|
-
.map(() => Array(columns).fill(null));
|
|
32
|
-
});
|
|
33
|
-
// Missing words list - NEW: Support both API format (answers) and internal format (missingLetters)
|
|
34
|
-
const [missingLetters, setMissingLetters] = useState(() => {
|
|
35
|
-
if (initialData?.answer?.missingLetters) {
|
|
36
|
-
// Internal format
|
|
37
|
-
console.log('📝 Using missingLetters format');
|
|
38
|
-
return initialData.answer.missingLetters.map((item, index) => {
|
|
39
|
-
const [row1Based, column1Based] = item.position.split('-').map(Number);
|
|
40
|
-
const row = (row1Based || 1) - 1;
|
|
41
|
-
const column = (column1Based || 1) - 1;
|
|
42
|
-
return {
|
|
43
|
-
id: `word-${index}-${Date.now()}`,
|
|
44
|
-
row,
|
|
45
|
-
column,
|
|
46
|
-
correctAnswer: item.correctAnswer,
|
|
47
|
-
};
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
else if (initialData?.answer?.answers && typeof initialData.answer.answers === 'object') {
|
|
51
|
-
// API format
|
|
52
|
-
const answersObj = initialData.answer.answers;
|
|
53
|
-
const result = Object.entries(answersObj).map(([position, correctAnswer], index) => {
|
|
54
|
-
const [row1Based, column1Based] = position.split('-').map(Number);
|
|
55
|
-
const row = (row1Based || 1) - 1;
|
|
56
|
-
const column = (column1Based || 1) - 1;
|
|
57
|
-
return {
|
|
58
|
-
id: `word-${index}-${Date.now()}-${Math.random()}`,
|
|
59
|
-
row,
|
|
60
|
-
column,
|
|
61
|
-
correctAnswer: correctAnswer,
|
|
62
|
-
};
|
|
63
|
-
});
|
|
64
|
-
return result;
|
|
65
|
-
}
|
|
66
|
-
return [];
|
|
67
|
-
});
|
|
68
|
-
// Disabled cells list - NEW: cells that are disabled (not fillable, not missing)
|
|
69
|
-
const [disabledCells, setDisabledCells] = useState(() => {
|
|
70
|
-
if (initialData?.content?.disabledCells) {
|
|
71
|
-
return initialData.content.disabledCells;
|
|
72
|
-
}
|
|
73
|
-
return [];
|
|
74
|
-
});
|
|
75
|
-
// Track previous initialData to detect changes
|
|
76
|
-
const previousInitialDataRef = useRef(initialData);
|
|
77
|
-
const isInitializingRef = useRef(true); // Start as true - prevent false "unsaved changes" on initial mount
|
|
78
|
-
const onChangeRef = useRef(onChange);
|
|
79
|
-
const previousPayloadRef = useRef('');
|
|
80
|
-
const hasInitializedOnceRef = useRef(false); // Start as false - track if component has fully initialized
|
|
81
|
-
const hasSetInitialUnsavedStateRef = useRef(false); // Track if we've set initial unsaved state to false
|
|
82
|
-
// CRITICAL FIX: Store the ORIGINAL saved data from API (not updated when store updates)
|
|
83
|
-
// This is used to compare against to determine if there are unsaved changes
|
|
84
|
-
const originalSavedDataRef = useRef(undefined);
|
|
85
|
-
const hasOriginalDataRef = useRef(false); // Track if we have original data from API
|
|
86
|
-
// Update onChange ref when it changes
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
onChangeRef.current = onChange;
|
|
89
|
-
}, [onChange]);
|
|
90
|
-
// CRITICAL FIX: Initialize originalSavedDataRef ONLY ONCE when we first receive valid initialData from API
|
|
91
|
-
// This ref should NOT be updated when the store updates (which would make initialData prop change)
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
// Only set original data once, and only if initialData has valid content (from API)
|
|
94
|
-
if (!hasOriginalDataRef.current && initialData?.content?.grid) {
|
|
95
|
-
// Deep clone to prevent reference issues
|
|
96
|
-
originalSavedDataRef.current = JSON.parse(JSON.stringify(initialData));
|
|
97
|
-
hasOriginalDataRef.current = true;
|
|
98
|
-
console.log('📌 [FillMissingWordsInGridCreator] Saved original data from API:', originalSavedDataRef.current);
|
|
99
|
-
}
|
|
100
|
-
}, [initialData]);
|
|
101
|
-
// Track unsaved changes and notify parent
|
|
102
|
-
// FIXED: Compare against originalSavedDataRef (stable) instead of initialData (which updates with store)
|
|
103
|
-
useEffect(() => {
|
|
104
|
-
// Don't notify during initialization
|
|
105
|
-
if (isInitializingRef.current) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
// Use original saved data (from API) for comparison, not live initialData
|
|
109
|
-
const savedData = originalSavedDataRef.current;
|
|
110
|
-
// If we have original saved data, compare against it
|
|
111
|
-
if (hasOriginalDataRef.current && savedData?.content?.grid) {
|
|
112
|
-
// Existing question - check for changes from ORIGINAL saved state
|
|
113
|
-
const hasGridChanges = JSON.stringify(grid) !== JSON.stringify(savedData.content.grid);
|
|
114
|
-
// Normalize disabledCells arrays (sort before comparison)
|
|
115
|
-
const normalizedDisabledCells = [...(disabledCells || [])].sort();
|
|
116
|
-
const normalizedSavedDisabledCells = [...(savedData.content.disabledCells || [])].sort();
|
|
117
|
-
const hasDisabledCellsChanges = JSON.stringify(normalizedDisabledCells) !== JSON.stringify(normalizedSavedDisabledCells);
|
|
118
|
-
// Normalize points (convert to number for comparison)
|
|
119
|
-
const normalizedPoints = typeof points === 'string' ? Number(points) : points;
|
|
120
|
-
const normalizedSavedPoints = typeof savedData?.points === 'string'
|
|
121
|
-
? Number(savedData.points)
|
|
122
|
-
: (savedData?.points || 1);
|
|
123
|
-
const hasPointsChanges = normalizedPoints !== normalizedSavedPoints;
|
|
124
|
-
// Normalize answers objects (sort keys before comparison)
|
|
125
|
-
const savedAnswers = savedData?.answer?.answers || {};
|
|
126
|
-
const currentAnswers = missingLetters.reduce((acc, item) => {
|
|
127
|
-
acc[`${item.row + 1}-${item.column + 1}`] = item.correctAnswer;
|
|
128
|
-
return acc;
|
|
129
|
-
}, {});
|
|
130
|
-
const savedAnswersSorted = Object.keys(savedAnswers).sort().reduce((acc, key) => {
|
|
131
|
-
acc[key] = savedAnswers[key];
|
|
132
|
-
return acc;
|
|
133
|
-
}, {});
|
|
134
|
-
const currentAnswersSorted = Object.keys(currentAnswers).sort().reduce((acc, key) => {
|
|
135
|
-
acc[key] = currentAnswers[key];
|
|
136
|
-
return acc;
|
|
137
|
-
}, {});
|
|
138
|
-
const hasMissingLettersChanges = JSON.stringify(currentAnswersSorted) !== JSON.stringify(savedAnswersSorted);
|
|
139
|
-
const hasAnyChanges = hasGridChanges || hasDisabledCellsChanges || hasPointsChanges || hasMissingLettersChanges;
|
|
140
|
-
onUnsavedChangesChange?.(hasAnyChanges);
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
// New question (no original data) - check if user has entered any data
|
|
144
|
-
const hasGridContent = grid.some(row => row.some(cell => cell !== null && cell !== ''));
|
|
145
|
-
const hasMissingLettersContent = missingLetters.length > 0;
|
|
146
|
-
const hasDisabledCellsContent = disabledCells.length > 0;
|
|
147
|
-
const hasAnyData = hasGridContent || hasMissingLettersContent || hasDisabledCellsContent;
|
|
148
|
-
onUnsavedChangesChange?.(hasAnyData);
|
|
149
|
-
}
|
|
150
|
-
}, [grid, missingLetters, disabledCells, points, onUnsavedChangesChange]);
|
|
151
|
-
// Sync state from initialData when it changes
|
|
152
|
-
useEffect(() => {
|
|
153
|
-
const initialDataChanged = JSON.stringify(previousInitialDataRef.current) !== JSON.stringify(initialData);
|
|
154
|
-
if (initialDataChanged) {
|
|
155
|
-
isInitializingRef.current = true;
|
|
156
|
-
previousInitialDataRef.current = initialData;
|
|
157
|
-
if (initialData) {
|
|
158
|
-
// FIXED: Do not update rows/columns from initialData - keep fixed at 4x7
|
|
159
|
-
// if (initialData.content?.rows !== undefined) setRows(initialData.content.rows);
|
|
160
|
-
// if (initialData.content?.columns !== undefined) setColumns(initialData.content.columns);
|
|
161
|
-
if (initialData.content?.grid !== undefined)
|
|
162
|
-
setGrid(initialData.content.grid);
|
|
163
|
-
// NEW: Support both formats
|
|
164
|
-
if (initialData.answer?.missingLetters) {
|
|
165
|
-
setMissingLetters((prev) => {
|
|
166
|
-
const positionToId = new Map(prev.map((item) => [`${item.row}-${item.column}`, item.id]));
|
|
167
|
-
return initialData.answer.missingLetters.map((item, index) => {
|
|
168
|
-
const [row1Based, column1Based] = item.position.split('-').map(Number);
|
|
169
|
-
const row = (row1Based || 1) - 1;
|
|
170
|
-
const column = (column1Based || 1) - 1;
|
|
171
|
-
const positionKey = `${row}-${column}`;
|
|
172
|
-
const existingId = positionToId.get(positionKey);
|
|
173
|
-
const id = existingId || `word-${index}-${Date.now()}-${Math.random()}`;
|
|
174
|
-
return {
|
|
175
|
-
id,
|
|
176
|
-
row,
|
|
177
|
-
column,
|
|
178
|
-
correctAnswer: item.correctAnswer,
|
|
179
|
-
};
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
else if (initialData.answer?.answers) {
|
|
184
|
-
setMissingLetters((prev) => {
|
|
185
|
-
const positionToId = new Map(prev.map((item) => [`${item.row}-${item.column}`, item.id]));
|
|
186
|
-
const result = Object.entries(initialData.answer.answers).map(([position, correctAnswer], index) => {
|
|
187
|
-
const [row1Based, column1Based] = position.split('-').map(Number);
|
|
188
|
-
const row = (row1Based || 1) - 1;
|
|
189
|
-
const column = (column1Based || 1) - 1;
|
|
190
|
-
const positionKey = `${row}-${column}`;
|
|
191
|
-
const existingId = positionToId.get(positionKey);
|
|
192
|
-
const id = existingId || `word-${index}-${Date.now()}-${Math.random()}`;
|
|
193
|
-
return {
|
|
194
|
-
id,
|
|
195
|
-
row,
|
|
196
|
-
column,
|
|
197
|
-
correctAnswer: correctAnswer,
|
|
198
|
-
};
|
|
199
|
-
});
|
|
200
|
-
return result;
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
// NEW: Sync disabled cells (ensure it's an array)
|
|
204
|
-
if (initialData.content?.disabledCells !== undefined) {
|
|
205
|
-
const disabledCellsArray = Array.isArray(initialData.content.disabledCells)
|
|
206
|
-
? initialData.content.disabledCells
|
|
207
|
-
: [];
|
|
208
|
-
setDisabledCells(disabledCellsArray);
|
|
209
|
-
}
|
|
210
|
-
// Sync points (convert to number if it's a string)
|
|
211
|
-
if (initialData.points !== undefined) {
|
|
212
|
-
const pointsValue = initialData.points;
|
|
213
|
-
setPoints(typeof pointsValue === 'string' ? Number(pointsValue) : pointsValue);
|
|
214
|
-
}
|
|
215
|
-
// Sync explanation
|
|
216
|
-
if (initialData.explanation !== undefined) {
|
|
217
|
-
setExplanation(initialData.explanation);
|
|
218
|
-
}
|
|
219
|
-
// FIXED: Keep rows and columns fixed
|
|
220
|
-
// setRows(4);
|
|
221
|
-
// setColumns(7);
|
|
222
|
-
// setGrid(Array(4).fill(null).map(() => Array(7).fill(null)));
|
|
223
|
-
// setMissingLetters([]);
|
|
224
|
-
}
|
|
225
|
-
// SIMPLIFIED: After syncing state from initialData, mark initialization complete
|
|
226
|
-
// FIXED: Only set unsaved changes to false on FIRST initialization, not on every initialData change
|
|
227
|
-
setTimeout(() => {
|
|
228
|
-
isInitializingRef.current = false;
|
|
229
|
-
// Only on first initialization, set the initial status
|
|
230
|
-
if (!hasInitializedOnceRef.current) {
|
|
231
|
-
hasInitializedOnceRef.current = true;
|
|
232
|
-
hasSetInitialUnsavedStateRef.current = true;
|
|
233
|
-
// If we have initialData from API, question is already saved - no unsaved changes
|
|
234
|
-
if (initialData?.content?.grid) {
|
|
235
|
-
onUnsavedChangesChange?.(false);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
// Note: After first initialization, we don't reset unsaved changes state
|
|
239
|
-
// because the useEffect that tracks changes will handle it correctly
|
|
240
|
-
}, 100);
|
|
241
|
-
}
|
|
242
|
-
else if (!hasInitializedOnceRef.current) {
|
|
243
|
-
// If no initialData and haven't initialized yet, mark as initialized after short delay
|
|
244
|
-
setTimeout(() => {
|
|
245
|
-
isInitializingRef.current = false;
|
|
246
|
-
hasInitializedOnceRef.current = true;
|
|
247
|
-
hasSetInitialUnsavedStateRef.current = true;
|
|
248
|
-
}, 100);
|
|
249
|
-
}
|
|
250
|
-
}, [initialData, rows, columns, onUnsavedChangesChange]);
|
|
251
|
-
// Update grid when dimensions change (always update, not just when not initializing)
|
|
252
|
-
useEffect(() => {
|
|
253
|
-
setGrid((prev) => {
|
|
254
|
-
// Check if dimensions actually changed
|
|
255
|
-
const currentRows = prev.length;
|
|
256
|
-
const currentCols = prev[0]?.length || 0;
|
|
257
|
-
// If dimensions are the same, don't recreate grid
|
|
258
|
-
if (currentRows === rows && currentCols === columns) {
|
|
259
|
-
return prev;
|
|
260
|
-
}
|
|
261
|
-
const newGrid = [];
|
|
262
|
-
for (let i = 0; i < rows; i++) {
|
|
263
|
-
const newRow = [];
|
|
264
|
-
for (let j = 0; j < columns; j++) {
|
|
265
|
-
// Preserve existing values if within bounds
|
|
266
|
-
if (i < prev.length && j < prev[i]?.length) {
|
|
267
|
-
newRow.push(prev[i][j]);
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
newRow.push(null);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
newGrid.push(newRow);
|
|
274
|
-
}
|
|
275
|
-
return newGrid;
|
|
276
|
-
});
|
|
277
|
-
// Remove missing letters that are out of bounds (only when not initializing)
|
|
278
|
-
if (!isInitializingRef.current) {
|
|
279
|
-
setMissingLetters((prev) => {
|
|
280
|
-
const filtered = prev.filter((letter) => letter.row < rows && letter.column < columns);
|
|
281
|
-
const removedCount = prev.length - filtered.length;
|
|
282
|
-
if (removedCount > 0) {
|
|
283
|
-
// Show warning as text instead of toast
|
|
284
|
-
setGeneralErrors((prev) => [
|
|
285
|
-
...prev.filter((err) => !err.includes('ô còn thiếu ngoài phạm vi grid')),
|
|
286
|
-
`Đã xóa ${removedCount} ô còn thiếu ngoài phạm vi grid mới (${rows}x${columns})`,
|
|
287
|
-
]);
|
|
288
|
-
}
|
|
289
|
-
return filtered;
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
}, [rows, columns]);
|
|
293
|
-
// Convert to payload format and call onChange
|
|
294
|
-
useEffect(() => {
|
|
295
|
-
// Don't call onChange until we've initialized at least once
|
|
296
|
-
if (isInitializingRef.current) {
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
// NEW: Output API format directly - no transformation needed
|
|
300
|
-
const payload = {
|
|
301
|
-
content: {
|
|
302
|
-
grid,
|
|
303
|
-
rows,
|
|
304
|
-
columns,
|
|
305
|
-
disabledCells: [...disabledCells].sort(), // Sort for consistent comparison
|
|
306
|
-
},
|
|
307
|
-
answer: {
|
|
308
|
-
// Convert missingLetters array to answers object for API format
|
|
309
|
-
answers: missingLetters.reduce((acc, item) => {
|
|
310
|
-
acc[`${item.row + 1}-${item.column + 1}`] = item.correctAnswer;
|
|
311
|
-
return acc;
|
|
312
|
-
}, {}),
|
|
313
|
-
caseSensitive: false,
|
|
314
|
-
},
|
|
315
|
-
points: typeof points === 'string' ? Number(points) : points, // Ensure points is a number
|
|
316
|
-
explanation: explanation || '', // Include explanation in payload
|
|
317
|
-
};
|
|
318
|
-
const payloadString = JSON.stringify(payload);
|
|
319
|
-
// Only call onChange if payload actually changed
|
|
320
|
-
if (previousPayloadRef.current !== payloadString) {
|
|
321
|
-
previousPayloadRef.current = payloadString;
|
|
322
|
-
onChangeRef.current?.(payload);
|
|
323
|
-
}
|
|
324
|
-
}, [grid, rows, columns, missingLetters, disabledCells, points, explanation]);
|
|
325
|
-
// Parse externalErrors and set validation errors for missing letters and general errors
|
|
326
|
-
useEffect(() => {
|
|
327
|
-
if (externalErrors && externalErrors.length > 0) {
|
|
328
|
-
const newFieldErrors = {};
|
|
329
|
-
const newGeneralErrors = [];
|
|
330
|
-
externalErrors.forEach((error) => {
|
|
331
|
-
// Check if error is about missing letter answer
|
|
332
|
-
// Format: "Ô còn thiếu #X ở vị trí (row, col) phải có đáp án đúng"
|
|
333
|
-
const match = error.match(/Ô còn thiếu #(\d+) ở vị trí \((\d+), (\d+)\) phải có đáp án đúng/);
|
|
334
|
-
if (match) {
|
|
335
|
-
const [, , rowStr, colStr] = match;
|
|
336
|
-
const row = parseInt(rowStr) - 1; // Convert 1-based to 0-based
|
|
337
|
-
const col = parseInt(colStr) - 1; // Convert 1-based to 0-based
|
|
338
|
-
// Find the missing letter by position
|
|
339
|
-
const letter = missingLetters.find((item) => item.row === row && item.column === col);
|
|
340
|
-
if (letter) {
|
|
341
|
-
newFieldErrors[letter.id] = 'Vui lòng nhập đáp án đúng';
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
else {
|
|
345
|
-
// Other errors go to general errors
|
|
346
|
-
newGeneralErrors.push(error);
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
// Merge with existing validation errors
|
|
350
|
-
setValidationErrors((prev) => ({
|
|
351
|
-
...prev,
|
|
352
|
-
...newFieldErrors,
|
|
353
|
-
}));
|
|
354
|
-
// Set general errors from external (only if changed)
|
|
355
|
-
if (newGeneralErrors.length > 0) {
|
|
356
|
-
setGeneralErrors((prev) => {
|
|
357
|
-
const isSame = prev.length === newGeneralErrors.length &&
|
|
358
|
-
prev.every((err, i) => err === newGeneralErrors[i]);
|
|
359
|
-
return isSame ? prev : newGeneralErrors;
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
else {
|
|
364
|
-
// Clear general errors when externalErrors is empty (only if not already empty)
|
|
365
|
-
setGeneralErrors((prev) => prev.length === 0 ? prev : []);
|
|
366
|
-
}
|
|
367
|
-
}, [externalErrors, missingLetters]);
|
|
368
|
-
// UI State
|
|
369
|
-
const [isClientMode, setIsClientMode] = useState(false);
|
|
370
|
-
const [validationErrors, setValidationErrors] = useState({});
|
|
371
|
-
const [dimensionErrors, setDimensionErrors] = useState({});
|
|
372
|
-
const [generalErrors, setGeneralErrors] = useState([]);
|
|
373
|
-
// Update grid cell
|
|
374
|
-
const updateGridCell = useCallback((row, col, value) => {
|
|
375
|
-
setGrid((prev) => {
|
|
376
|
-
const newGrid = prev.map((r, i) => r.map((cell, j) => (i === row && j === col ? (value || null) : cell)));
|
|
377
|
-
return newGrid;
|
|
378
|
-
});
|
|
379
|
-
// Clear general errors related to this cell when user fills it
|
|
380
|
-
if (value && value.trim() !== '') {
|
|
381
|
-
setGeneralErrors((prev) => prev.filter((err) => !err.includes(`vị trí (${row + 1}, ${col + 1})`)));
|
|
382
|
-
}
|
|
383
|
-
// Immediately notify parent that there are unsaved changes when user types
|
|
384
|
-
onUnsavedChangesChange?.(true);
|
|
385
|
-
}, [onUnsavedChangesChange]);
|
|
386
|
-
// Toggle missing word (mark/unmark cell as missing)
|
|
387
|
-
const toggleMissingLetter = useCallback((row, col) => {
|
|
388
|
-
const positionKey = `${row}-${col}`;
|
|
389
|
-
const existing = missingLetters.find((item) => `${item.row}-${item.column}` === positionKey);
|
|
390
|
-
if (existing) {
|
|
391
|
-
// Remove from missing letters
|
|
392
|
-
setMissingLetters((prev) => prev.filter((item) => item.id !== existing.id));
|
|
393
|
-
// Clear validation error when removing
|
|
394
|
-
setValidationErrors((prev) => {
|
|
395
|
-
const newErrors = { ...prev };
|
|
396
|
-
delete newErrors[existing.id];
|
|
397
|
-
return newErrors;
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
// Add to missing letters and clear the cell value
|
|
402
|
-
const cellValue = grid[row]?.[col];
|
|
403
|
-
const newLetter = {
|
|
404
|
-
id: `word-${Date.now()}-${Math.random()}`,
|
|
405
|
-
row,
|
|
406
|
-
column: col,
|
|
407
|
-
correctAnswer: cellValue || '',
|
|
408
|
-
};
|
|
409
|
-
setMissingLetters((prev) => [...prev, newLetter]);
|
|
410
|
-
// Clear the cell value in grid
|
|
411
|
-
updateGridCell(row, col, '');
|
|
412
|
-
// Clear general errors related to this cell when marked as missing
|
|
413
|
-
setGeneralErrors((prev) => prev.filter((err) => !err.includes(`vị trí (${row + 1}, ${col + 1})`)));
|
|
414
|
-
}
|
|
415
|
-
// Immediately notify parent that there are unsaved changes
|
|
416
|
-
onUnsavedChangesChange?.(true);
|
|
417
|
-
}, [grid, missingLetters, updateGridCell, onUnsavedChangesChange]);
|
|
418
|
-
// Toggle disabled cell (mark/unmark cell as disabled)
|
|
419
|
-
const toggleDisabledCell = useCallback((row, col) => {
|
|
420
|
-
const position = `${row + 1}-${col + 1}`;
|
|
421
|
-
setDisabledCells(prev => {
|
|
422
|
-
if (prev.includes(position)) {
|
|
423
|
-
// Remove from disabled cells
|
|
424
|
-
return prev.filter(p => p !== position);
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
// Add to disabled cells and clear the cell value
|
|
428
|
-
updateGridCell(row, col, '');
|
|
429
|
-
// Remove from missing letters if present
|
|
430
|
-
setMissingLetters(prevMissing => prevMissing.filter(item => !(item.row === row && item.column === col)));
|
|
431
|
-
return [...prev, position];
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
// Clear general errors related to this cell
|
|
435
|
-
setGeneralErrors((prev) => prev.filter((err) => !err.includes(`vị trí (${row + 1}, ${col + 1})`)));
|
|
436
|
-
// Immediately notify parent that there are unsaved changes
|
|
437
|
-
onUnsavedChangesChange?.(true);
|
|
438
|
-
}, [updateGridCell, onUnsavedChangesChange]);
|
|
439
|
-
// Update missing letter answer
|
|
440
|
-
const updateMissingLetter = useCallback((id, field, value) => {
|
|
441
|
-
setMissingLetters((prev) => prev.map((letter) => (letter.id === id ? { ...letter, [field]: value } : letter)));
|
|
442
|
-
// Clear validation error when user starts typing
|
|
443
|
-
if (field === 'correctAnswer') {
|
|
444
|
-
setValidationErrors((prev) => {
|
|
445
|
-
const newErrors = { ...prev };
|
|
446
|
-
if (typeof value === 'string' && value.trim() !== '') {
|
|
447
|
-
delete newErrors[id];
|
|
448
|
-
}
|
|
449
|
-
return newErrors;
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
// Immediately notify parent that there are unsaved changes
|
|
453
|
-
onUnsavedChangesChange?.(true);
|
|
454
|
-
}, [onUnsavedChangesChange]);
|
|
455
|
-
// Remove missing letter
|
|
456
|
-
const removeMissingLetter = (id) => {
|
|
457
|
-
setMissingLetters((prev) => prev.filter((letter) => letter.id !== id));
|
|
458
|
-
// Clear validation error when removing
|
|
459
|
-
setValidationErrors((prev) => {
|
|
460
|
-
const newErrors = { ...prev };
|
|
461
|
-
delete newErrors[id];
|
|
462
|
-
return newErrors;
|
|
463
|
-
});
|
|
464
|
-
};
|
|
465
|
-
// Validate grid and missing letters
|
|
466
|
-
const validateGrid = useCallback(() => {
|
|
467
|
-
const errors = [];
|
|
468
|
-
const fieldErrors = {};
|
|
469
|
-
// Validate dimensions
|
|
470
|
-
if (!rows || rows < 1 || rows > 20) {
|
|
471
|
-
errors.push('Số hàng phải từ 1 đến 20');
|
|
472
|
-
}
|
|
473
|
-
if (!columns || columns < 1 || columns > 20) {
|
|
474
|
-
errors.push('Số cột phải từ 1 đến 20');
|
|
475
|
-
}
|
|
476
|
-
// Check if all cells have value OR are marked as missing OR are disabled
|
|
477
|
-
for (let row = 0; row < rows; row++) {
|
|
478
|
-
for (let col = 0; col < columns; col++) {
|
|
479
|
-
const cellValue = grid[row]?.[col];
|
|
480
|
-
const isMissing = missingLetters.some((item) => item.row === row && item.column === col);
|
|
481
|
-
const isDisabled = disabledCells.includes(`${row + 1}-${col + 1}`); // NEW
|
|
482
|
-
// Cell must have value OR be marked as missing OR be disabled
|
|
483
|
-
if (!cellValue && !isMissing && !isDisabled) {
|
|
484
|
-
errors.push(`Ô ở vị trí (${row + 1}, ${col + 1}) phải có giá trị, được đánh dấu là còn thiếu, hoặc bị vô hiệu hóa`);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
// Check if all marked missing letters have correctAnswer
|
|
489
|
-
missingLetters.forEach((letter) => {
|
|
490
|
-
if (!letter.correctAnswer || letter.correctAnswer.trim() === '') {
|
|
491
|
-
fieldErrors[letter.id] = 'Vui lòng nhập đáp án đúng';
|
|
492
|
-
}
|
|
493
|
-
else if (letter.correctAnswer.length > 100) {
|
|
494
|
-
fieldErrors[letter.id] = 'Đáp án không được vượt quá 100 ký tự';
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
// Update validation errors state
|
|
498
|
-
setValidationErrors(fieldErrors);
|
|
499
|
-
setGeneralErrors(errors);
|
|
500
|
-
return {
|
|
501
|
-
isValid: errors.length === 0 && Object.keys(fieldErrors).length === 0,
|
|
502
|
-
errors,
|
|
503
|
-
};
|
|
504
|
-
}, [grid, rows, columns, missingLetters]);
|
|
505
|
-
// Check if a cell is invalid (empty and not marked as missing and not disabled)
|
|
506
|
-
const isCellInvalid = useCallback((row, col) => {
|
|
507
|
-
const cellValue = grid[row]?.[col];
|
|
508
|
-
const isMissing = missingLetters.some((item) => item.row === row && item.column === col);
|
|
509
|
-
const isDisabled = disabledCells.includes(`${row + 1}-${col + 1}`); // NEW
|
|
510
|
-
// Cell is invalid if it has no value AND is not marked as missing AND is not disabled
|
|
511
|
-
return !cellValue && !isMissing && !isDisabled;
|
|
512
|
-
}, [grid, missingLetters, disabledCells] // NEW: Add disabledCells dependency
|
|
513
|
-
);
|
|
514
|
-
// Check if we should show validation highlights (when there are errors)
|
|
515
|
-
const shouldShowValidation = useCallback(() => {
|
|
516
|
-
return generalErrors.length > 0 || (externalErrors && externalErrors.length > 0);
|
|
517
|
-
}, [generalErrors, externalErrors]);
|
|
518
|
-
// Validate before switching to client mode
|
|
519
|
-
const handlePreview = () => {
|
|
520
|
-
const validation = validateGrid();
|
|
521
|
-
if (!validation.isValid) {
|
|
522
|
-
// Errors are already set in state by validateGrid
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
setIsClientMode(true);
|
|
526
|
-
};
|
|
527
|
-
// Handle save with validation
|
|
528
|
-
const handleSave = () => {
|
|
529
|
-
const validation = validateGrid();
|
|
530
|
-
if (!validation.isValid) {
|
|
531
|
-
toast.error('Vui lòng điền đầy đủ thông tin!');
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
// Additional validation
|
|
535
|
-
if (!rows || rows < 1 || rows > 20) {
|
|
536
|
-
toast.error('Số hàng phải từ 1 đến 20!');
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
if (!columns || columns < 1 || columns > 20) {
|
|
540
|
-
toast.error('Số cột phải từ 1 đến 20!');
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
if (missingLetters.length === 0) {
|
|
544
|
-
toast.error('Vui lòng đánh dấu ít nhất 1 ô còn thiếu!');
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
// Check all missing letters have answers
|
|
548
|
-
for (let i = 0; i < missingLetters.length; i++) {
|
|
549
|
-
const letter = missingLetters[i];
|
|
550
|
-
if (!letter.correctAnswer || letter.correctAnswer.trim() === '') {
|
|
551
|
-
toast.error(t('common.questionType.missingAnswer', { index: String(i + 1), row: String(letter.row + 1), col: String(letter.column + 1) }));
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
// All validation passed - NEW: Output API format with answers object
|
|
556
|
-
onSave?.({
|
|
557
|
-
content: {
|
|
558
|
-
grid,
|
|
559
|
-
rows,
|
|
560
|
-
columns,
|
|
561
|
-
disabledCells, // NEW: Include disabled cells in save
|
|
562
|
-
},
|
|
563
|
-
answer: {
|
|
564
|
-
answers: missingLetters.reduce((acc, item) => {
|
|
565
|
-
acc[`${item.row + 1}-${item.column + 1}`] = item.correctAnswer;
|
|
566
|
-
return acc;
|
|
567
|
-
}, {}),
|
|
568
|
-
caseSensitive: false,
|
|
569
|
-
},
|
|
570
|
-
points, // Include points in save
|
|
571
|
-
explanation: explanation || '', // Include explanation in save
|
|
572
|
-
});
|
|
573
|
-
};
|
|
574
|
-
// If in client mode, show preview using Client component
|
|
575
|
-
if (isClientMode) {
|
|
576
|
-
// Prepare data for Client component
|
|
577
|
-
const questionData = {
|
|
578
|
-
content: {
|
|
579
|
-
grid,
|
|
580
|
-
rows,
|
|
581
|
-
columns,
|
|
582
|
-
disabledCells,
|
|
583
|
-
},
|
|
584
|
-
answer: {
|
|
585
|
-
answers: missingLetters.reduce((acc, item) => {
|
|
586
|
-
acc[`${item.row + 1}-${item.column + 1}`] = item.correctAnswer;
|
|
587
|
-
return acc;
|
|
588
|
-
}, {}),
|
|
589
|
-
caseSensitive: false,
|
|
590
|
-
},
|
|
591
|
-
};
|
|
592
|
-
// Auto-fill correct answers for preview
|
|
593
|
-
const correctAnswers = missingLetters.reduce((acc, item) => {
|
|
594
|
-
acc[`${item.row + 1}-${item.column + 1}`] = item.correctAnswer;
|
|
595
|
-
return acc;
|
|
596
|
-
}, {});
|
|
597
|
-
return (_jsx("div", { className: "space-y-6", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { children: "Fill Missing Words In Grid (Preview)" }), _jsx("p", { className: "mt-1 text-sm text-gray-500", children: "Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc - Hi\u1EC3n th\u1ECB \u0111\u00E1p \u00E1n \u0111\u00FAng" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Quay l\u1EA1i Edit Mode"] })] }) }), _jsx(CardContent, { children: _jsx(FillMissingWordsInGridClient, { questionData: questionData, isReviewMode: true, userAnswer: correctAnswers, explanation: explanation }) })] }) }));
|
|
598
|
-
}
|
|
599
|
-
return (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50", children: [_jsx("div", { className: "h-1.5 bg-gradient-to-r from-cyan-500 via-blue-500 to-indigo-500" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-cyan-500 to-blue-600 shadow-md", children: _jsx(Grid3X3, { className: "h-5 w-5 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-xl font-bold text-gray-900", children: "\u0110i\u1EC1n t\u1EEB c\u00F2n thi\u1EBFu v\u00E0o grid" }), _jsx("p", { className: "text-sm text-gray-500", children: "T\u1EA1o grid v\u00E0 nh\u1EADp c\u00E1c t\u1EEB, \u0111\u00E1nh d\u1EA5u c\u00E1c \u00F4 c\u00F2n thi\u1EBFu" })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: handlePreview, className: "gap-2 border-blue-200 text-blue-600 hover:bg-blue-50 hover:text-blue-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-cyan-200 bg-gradient-to-r from-cyan-50 to-blue-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-cyan-100", children: _jsx(Lightbulb, { className: "h-4 w-4 text-cyan-600" }) }), _jsxs("div", { className: "text-sm text-cyan-900", children: [_jsx("p", { className: "font-semibold text-cyan-800", children: "H\u01B0\u1EDBng d\u1EABn t\u1EA1o c\u00E2u h\u1ECFi" }), _jsxs("ul", { className: "mt-2 space-y-1.5 text-cyan-700", children: [_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-cyan-400" }), "Nh\u1EADp t\u1EEB v\u00E0o c\u00E1c \u00F4 trong grid (4 h\u00E0ng \u00D7 7 c\u1ED9t)"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-cyan-400" }), _jsxs("span", { children: [_jsx("strong", { children: "Double-click:" }), " \u0110\u00E1nh d\u1EA5u \u00F4 c\u00F2n thi\u1EBFu (m\u00E0u xanh d\u01B0\u01A1ng)"] })] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-cyan-400" }), _jsxs("span", { children: [_jsx("strong", { children: "Triple-click:" }), " V\u00F4 hi\u1EC7u h\u00F3a \u00F4 (m\u00E0u \u0111\u1ECF - kh\u00F4ng c\u1EA7n \u0111i\u1EC1n)"] })] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-cyan-400" }), "Nh\u1EADp \u0111\u00E1p \u00E1n \u0111\u00FAng cho m\u1ED7i \u00F4 c\u00F2n thi\u1EBFu"] })] })] })] }) }), _jsx("div", { className: "rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4", children: _jsx(PointsInput, { defaultValue: points, onChange: (e) => setPoints(Number(e.target.value) || 1) }, `points-input-${points}`) }), _jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex items-center justify-between", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-indigo-100", children: _jsx(Grid3X3, { className: "h-3.5 w-3.5 text-indigo-600" }) }), _jsx(Label, { className: "text-base font-semibold text-gray-800", children: "Grid - Nh\u1EADp t\u1EEB v\u00E0o c\u00E1c \u00F4" })] }) }), _jsx("div", { className: "overflow-x-auto rounded-xl border-2 border-indigo-200 bg-gradient-to-br from-indigo-50/50 to-blue-50/50 p-5 shadow-sm", children: _jsxs("div", { className: "inline-block min-w-full", children: [_jsxs("div", { className: "grid gap-1 mb-1", style: { gridTemplateColumns: `auto repeat(${columns}, minmax(0, 1fr))` }, children: [_jsx("div", { className: "h-8" }), Array.from({ length: columns }, (_, i) => (_jsx("div", { className: "flex h-8 items-center justify-center text-xs font-semibold text-gray-600", children: i + 1 }, `col-header-${i}`)))] }), _jsx("div", { className: "grid gap-1", children: grid.map((row, rowIndex) => (_jsxs("div", { className: "grid gap-1", style: { gridTemplateColumns: `auto repeat(${columns}, minmax(0, 1fr))` }, children: [_jsx("div", { className: "flex h-12 items-center justify-center text-xs font-semibold text-gray-600 border-2 border-transparent", children: rowIndex + 1 }), row.map((cell, colIndex) => {
|
|
600
|
-
const isMissing = missingLetters.some((item) => item.row === rowIndex && item.column === colIndex);
|
|
601
|
-
const isDisabled = disabledCells.includes(`${rowIndex + 1}-${colIndex + 1}`); // NEW
|
|
602
|
-
const isInvalid = shouldShowValidation() && isCellInvalid(rowIndex, colIndex);
|
|
603
|
-
return (_jsxs("div", { className: `relative flex h-12 items-center justify-center border-2 rounded transition-colors ${isDisabled
|
|
604
|
-
? 'border-red-400 bg-red-100 hover:bg-red-150 cursor-pointer' // NEW: Red styling for disabled
|
|
605
|
-
: isMissing
|
|
606
|
-
? 'border-blue-500 bg-blue-50 hover:bg-blue-100 cursor-pointer'
|
|
607
|
-
: isInvalid
|
|
608
|
-
? 'border-red-500 bg-red-50 hover:bg-red-100'
|
|
609
|
-
: cell
|
|
610
|
-
? 'border-gray-400 bg-white hover:bg-gray-50'
|
|
611
|
-
: 'border-gray-300 bg-gray-100 hover:bg-gray-200'}`, onClick: (e) => {
|
|
612
|
-
// Triple-click to toggle disabled
|
|
613
|
-
if (e.detail === 3) {
|
|
614
|
-
e.preventDefault();
|
|
615
|
-
e.stopPropagation();
|
|
616
|
-
toggleDisabledCell(rowIndex, colIndex);
|
|
617
|
-
}
|
|
618
|
-
}, onDoubleClick: (e) => {
|
|
619
|
-
e.preventDefault();
|
|
620
|
-
e.stopPropagation();
|
|
621
|
-
if (!isDisabled) {
|
|
622
|
-
toggleMissingLetter(rowIndex, colIndex);
|
|
623
|
-
}
|
|
624
|
-
}, children: [_jsx(Input, { type: "text", maxLength: 10, value: cell || '', onChange: (e) => {
|
|
625
|
-
if (!isMissing && !isDisabled) { // NEW: Also check disabled
|
|
626
|
-
// Trim whitespace and limit length
|
|
627
|
-
const value = e.target.value.trim().slice(0, 10);
|
|
628
|
-
updateGridCell(rowIndex, colIndex, value);
|
|
629
|
-
}
|
|
630
|
-
}, onDoubleClick: (e) => {
|
|
631
|
-
e.preventDefault();
|
|
632
|
-
e.stopPropagation();
|
|
633
|
-
if (!isDisabled) { // NEW: Only toggle missing if not disabled
|
|
634
|
-
toggleMissingLetter(rowIndex, colIndex);
|
|
635
|
-
}
|
|
636
|
-
}, disabled: isMissing || isDisabled, readOnly: isMissing || isDisabled, className: `h-full border-0 bg-transparent text-center text-lg font-semibold focus:ring-2 focus:ring-blue-500 ${isMissing || isDisabled ? 'cursor-not-allowed pointer-events-none' : ''} ${isDisabled ? 'text-gray-500' : isMissing ? 'text-blue-600' : ''}`, placeholder: "" }), isMissing && (_jsx("div", { className: "absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-xs font-bold text-white", children: "?" })), isDisabled && (_jsx("div", { className: "absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs font-bold text-white", children: "\uD83D\uDD12" }))] }, `${rowIndex}-${colIndex}`));
|
|
637
|
-
})] }, `row-${rowIndex}`))) })] }) })] }), missingLetters.length > 0 && (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-blue-100", children: _jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-blue-600" }) }), _jsxs(Label, { className: "text-base font-semibold text-gray-800", children: ["Danh s\u00E1ch \u00F4 c\u00F2n thi\u1EBFu", _jsx("span", { className: "ml-2 rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700", children: missingLetters.length })] })] }), _jsx("div", { className: "space-y-3", children: [...missingLetters]
|
|
638
|
-
.sort((a, b) => {
|
|
639
|
-
if (a.row !== b.row)
|
|
640
|
-
return a.row - b.row;
|
|
641
|
-
return a.column - b.column;
|
|
642
|
-
})
|
|
643
|
-
.map((letter, index) => {
|
|
644
|
-
const cellValue = grid[letter.row]?.[letter.column];
|
|
645
|
-
return (_jsx("div", { className: "rounded-xl border-2 border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-4 shadow-sm transition-all hover:border-blue-300 hover:shadow-md", children: _jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-500 text-sm font-bold text-white shadow-sm", children: index + 1 }), _jsxs("div", { className: "flex-1 space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-sm font-semibold text-gray-700", children: ["V\u1ECB tr\u00ED (", letter.row + 1, ", ", letter.column + 1, ")"] }), cellValue && (_jsxs("span", { className: "rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-500", children: ["Hi\u1EC7n t\u1EA1i: \"", cellValue, "\""] }))] }), _jsxs("div", { children: [_jsxs(Label, { htmlFor: `answer-${letter.id}`, className: "text-sm font-medium text-gray-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `answer-${letter.id}`, type: "text", maxLength: 100, value: letter.correctAnswer, onChange: (e) => updateMissingLetter(letter.id, 'correctAnswer', e.target.value.trim()), onBlur: () => {
|
|
646
|
-
if (!letter.correctAnswer || letter.correctAnswer.trim() === '') {
|
|
647
|
-
setValidationErrors((prev) => ({
|
|
648
|
-
...prev,
|
|
649
|
-
[letter.id]: 'Vui lòng nhập đáp án đúng',
|
|
650
|
-
}));
|
|
651
|
-
}
|
|
652
|
-
else if (letter.correctAnswer.length > 100) {
|
|
653
|
-
setValidationErrors((prev) => ({
|
|
654
|
-
...prev,
|
|
655
|
-
[letter.id]: 'Đáp án không được vượt quá 100 ký tự',
|
|
656
|
-
}));
|
|
657
|
-
}
|
|
658
|
-
}, placeholder: "Nh\u1EADp \u0111\u00E1p \u00E1n \u0111\u00FAng...", className: `mt-1.5 border-gray-200 bg-white transition-all focus:border-blue-300 focus:ring-blue-200 ${validationErrors[letter.id]
|
|
659
|
-
? 'border-red-400 focus:border-red-500 focus:ring-red-200'
|
|
660
|
-
: ''}` }), validationErrors[letter.id] && (_jsx("p", { className: "mt-1.5 text-sm text-red-600", children: validationErrors[letter.id] }))] })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeMissingLetter(letter.id), className: "h-8 w-8 text-red-400 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }) }, letter.id));
|
|
661
|
-
}) })] }))] })] }), _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) => {
|
|
662
|
-
setExplanation(e.target.value);
|
|
663
|
-
onUnsavedChangesChange?.(true);
|
|
664
|
-
}, 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" }) })] })] }));
|
|
665
|
-
}
|
|
666
|
-
// Export with both new and legacy names for backward compatibility
|
|
667
|
-
export function FillMissingWordsInGridCreator(props) {
|
|
668
|
-
return _jsx(FillMissingWordsInGridCreatorContent, { ...props });
|
|
669
|
-
}
|
|
670
|
-
// Legacy export for backward compatibility
|
|
671
|
-
export const WriteMissingLettersCreator = FillMissingWordsInGridCreator;
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useState as t,useRef as n,useEffect as s,useCallback as i}from"react";import{Button as l}from"../../../../components/ui/button";import{Label as o}from"../../../../components/ui/label";import{Input as c}from"../../../../components/ui/input";import{Textarea as a}from"../../../../components/ui/textarea";import{Card as d,CardContent as m,CardHeader as u,CardTitle as h}from"../../../../components/ui/card";import{PointsInput as g}from"../../../../components/ui/points-input";import{Pencil as p,Eye as f,Trash2 as b,Grid3X3 as w,Lightbulb as x,CheckCircle2 as N,BookOpen as v}from"lucide-react";import{toast as y}from"sonner";import{t as $}from"../../../../shared/lib/i18n";import{FillMissingWordsInGridClient as C}from"./FillMissingWordsInGridClient";function A({initialData:y,onSave:$,onCancel:A,onChange:j,onUnsavedChangesChange:k,externalErrors:O=[]}){const[S,D]=t(4),[M,J]=t(7),[V,G]=t(y?.points||1),[I,L]=t(y?.explanation||""),[F,T]=t(()=>y?.content?.grid?y.content.grid:Array(S).fill(null).map(()=>Array(M).fill(null))),[W,P]=t(()=>{if(y?.answer?.missingLetters)return console.log("📝 Using missingLetters format"),y.answer.missingLetters.map((e,r)=>{const[t,n]=e.position.split("-").map(Number),s=(t||1)-1,i=(n||1)-1;return{id:`word-${r}-${Date.now()}`,row:s,column:i,correctAnswer:e.correctAnswer}});if(y?.answer?.answers&&"object"==typeof y.answer.answers){const e=y.answer.answers;return Object.entries(e).map(([e,r],t)=>{const[n,s]=e.split("-").map(Number),i=(n||1)-1,l=(s||1)-1;return{id:`word-${t}-${Date.now()}-${Math.random()}`,row:i,column:l,correctAnswer:r}})}return[]}),[z,E]=t(()=>y?.content?.disabledCells?y.content.disabledCells:[]),q=n(y),H=n(!0),U=n(j),B=n(""),K=n(!1),Q=n(!1),R=n(void 0),X=n(!1);s(()=>{U.current=j},[j]),s(()=>{!X.current&&y?.content?.grid&&(R.current=JSON.parse(JSON.stringify(y)),X.current=!0,console.log("📌 [FillMissingWordsInGridCreator] Saved original data from API:",R.current))},[y]),s(()=>{if(H.current)return;const e=R.current;if(X.current&&e?.content?.grid){const r=JSON.stringify(F)!==JSON.stringify(e.content.grid),t=[...z||[]].sort(),n=[...e.content.disabledCells||[]].sort(),s=JSON.stringify(t)!==JSON.stringify(n),i=("string"==typeof V?Number(V):V)!==("string"==typeof e?.points?Number(e.points):e?.points||1),l=e?.answer?.answers||{},o=W.reduce((e,r)=>(e[`${r.row+1}-${r.column+1}`]=r.correctAnswer,e),{}),c=Object.keys(l).sort().reduce((e,r)=>(e[r]=l[r],e),{}),a=Object.keys(o).sort().reduce((e,r)=>(e[r]=o[r],e),{}),d=JSON.stringify(a)!==JSON.stringify(c),m=r||s||i||d;k?.(m)}else{const e=F.some(e=>e.some(e=>null!==e&&""!==e)),r=W.length>0,t=z.length>0,n=e||r||t;k?.(n)}},[F,W,z,V,k]),s(()=>{if(JSON.stringify(q.current)!==JSON.stringify(y)){if(H.current=!0,q.current=y,y){if(void 0!==y.content?.grid&&T(y.content.grid),y.answer?.missingLetters?P(e=>{const r=new Map(e.map(e=>[`${e.row}-${e.column}`,e.id]));return y.answer.missingLetters.map((e,t)=>{const[n,s]=e.position.split("-").map(Number),i=(n||1)-1,l=(s||1)-1,o=`${i}-${l}`;return{id:r.get(o)||`word-${t}-${Date.now()}-${Math.random()}`,row:i,column:l,correctAnswer:e.correctAnswer}})}):y.answer?.answers&&P(e=>{const r=new Map(e.map(e=>[`${e.row}-${e.column}`,e.id]));return Object.entries(y.answer.answers).map(([e,t],n)=>{const[s,i]=e.split("-").map(Number),l=(s||1)-1,o=(i||1)-1,c=`${l}-${o}`;return{id:r.get(c)||`word-${n}-${Date.now()}-${Math.random()}`,row:l,column:o,correctAnswer:t}})}),void 0!==y.content?.disabledCells){const e=Array.isArray(y.content.disabledCells)?y.content.disabledCells:[];E(e)}if(void 0!==y.points){const e=y.points;G("string"==typeof e?Number(e):e)}void 0!==y.explanation&&L(y.explanation)}setTimeout(()=>{H.current=!1,K.current||(K.current=!0,Q.current=!0,y?.content?.grid&&k?.(!1))},100)}else K.current||setTimeout(()=>{H.current=!1,K.current=!0,Q.current=!0},100)},[y,S,M,k]),s(()=>{T(e=>{const r=e.length,t=e[0]?.length||0;if(r===S&&t===M)return e;const n=[];for(let r=0;r<S;r++){const t=[];for(let n=0;n<M;n++)r<e.length&&n<e[r]?.length?t.push(e[r][n]):t.push(null);n.push(t)}return n}),H.current||P(e=>{const r=e.filter(e=>e.row<S&&e.column<M),t=e.length-r.length;return t>0&&se(e=>[...e.filter(e=>!e.includes("ô còn thiếu ngoài phạm vi grid")),`Đã xóa ${t} ô còn thiếu ngoài phạm vi grid mới (${S}x${M})`]),r})},[S,M]),s(()=>{if(H.current)return;const e={content:{grid:F,rows:S,columns:M,disabledCells:[...z].sort()},answer:{answers:W.reduce((e,r)=>(e[`${r.row+1}-${r.column+1}`]=r.correctAnswer,e),{}),caseSensitive:!1},points:"string"==typeof V?Number(V):V,explanation:I||""},r=JSON.stringify(e);B.current!==r&&(B.current=r,U.current?.(e))},[F,S,M,W,z,V,I]),s(()=>{if(O&&O.length>0){const e={},r=[];O.forEach(t=>{const n=t.match(/Ô còn thiếu #(\d+) ở vị trí \((\d+), (\d+)\) phải có đáp án đúng/);if(n){const[,,r,t]=n,s=parseInt(r)-1,i=parseInt(t)-1,l=W.find(e=>e.row===s&&e.column===i);l&&(e[l.id]="Vui lòng nhập đáp án đúng")}else r.push(t)}),ee(r=>({...r,...e})),r.length>0&&se(e=>e.length===r.length&&e.every((e,t)=>e===r[t])?e:r)}else se(e=>0===e.length?e:[])},[O,W]);const[Y,Z]=t(!1),[_,ee]=t({}),[re,te]=t({}),[ne,se]=t([]),ie=i((e,r,t)=>{T(n=>n.map((n,s)=>n.map((n,i)=>s===e&&i===r?t||null:n))),t&&""!==t.trim()&&se(t=>t.filter(t=>!t.includes(`vị trí (${e+1}, ${r+1})`))),k?.(!0)},[k]),le=i((e,r)=>{const t=`${e}-${r}`,n=W.find(e=>`${e.row}-${e.column}`===t);if(n)P(e=>e.filter(e=>e.id!==n.id)),ee(e=>{const r={...e};return delete r[n.id],r});else{const t=F[e]?.[r],n={id:`word-${Date.now()}-${Math.random()}`,row:e,column:r,correctAnswer:t||""};P(e=>[...e,n]),ie(e,r,""),se(t=>t.filter(t=>!t.includes(`vị trí (${e+1}, ${r+1})`)))}k?.(!0)},[F,W,ie,k]),oe=i((e,r)=>{const t=`${e+1}-${r+1}`;E(n=>n.includes(t)?n.filter(e=>e!==t):(ie(e,r,""),P(t=>t.filter(t=>!(t.row===e&&t.column===r))),[...n,t])),se(t=>t.filter(t=>!t.includes(`vị trí (${e+1}, ${r+1})`))),k?.(!0)},[ie,k]),ce=i((e,r,t)=>{P(n=>n.map(n=>n.id===e?{...n,[r]:t}:n)),"correctAnswer"===r&&ee(r=>{const n={...r};return"string"==typeof t&&""!==t.trim()&&delete n[e],n}),k?.(!0)},[k]),ae=i(()=>{const e=[],r={};(!S||S<1||S>20)&&e.push("Số hàng phải từ 1 đến 20"),(!M||M<1||M>20)&&e.push("Số cột phải từ 1 đến 20");for(let r=0;r<S;r++)for(let t=0;t<M;t++){const n=F[r]?.[t],s=W.some(e=>e.row===r&&e.column===t),i=z.includes(`${r+1}-${t+1}`);n||s||i||e.push(`Ô ở vị trí (${r+1}, ${t+1}) phải có giá trị, được đánh dấu là còn thiếu, hoặc bị vô hiệu hóa`)}return W.forEach(e=>{e.correctAnswer&&""!==e.correctAnswer.trim()?e.correctAnswer.length>100&&(r[e.id]="Đáp án không được vượt quá 100 ký tự"):r[e.id]="Vui lòng nhập đáp án đúng"}),ee(r),se(e),{isValid:0===e.length&&0===Object.keys(r).length,errors:e}},[F,S,M,W]),de=i((e,r)=>{const t=F[e]?.[r],n=W.some(t=>t.row===e&&t.column===r),s=z.includes(`${e+1}-${r+1}`);return!t&&!n&&!s},[F,W,z]),me=i(()=>ne.length>0||O&&O.length>0,[ne,O]);if(Y){const t={content:{grid:F,rows:S,columns:M,disabledCells:z},answer:{answers:W.reduce((e,r)=>(e[`${r.row+1}-${r.column+1}`]=r.correctAnswer,e),{}),caseSensitive:!1}},n=W.reduce((e,r)=>(e[`${r.row+1}-${r.column+1}`]=r.correctAnswer,e),{});return e("div",{className:"space-y-6",children:r(d,{children:[e(u,{children:r("div",{className:"flex items-center justify-between",children:[r("div",{children:[e(h,{children:"Fill Missing Words In Grid (Preview)"}),e("p",{className:"mt-1 text-sm text-gray-500",children:"Chế độ xem trước - Hiển thị đáp án đúng"})]}),r(l,{variant:"outline",size:"sm",onClick:()=>Z(!1),children:[e(p,{className:"mr-2 h-4 w-4"}),"Quay lại Edit Mode"]})]})}),e(m,{children:e(C,{questionData:t,isReviewMode:!0,userAnswer:n,explanation:I})})]})})}return r("div",{className:"space-y-6",children:[r(d,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50",children:[e("div",{className:"h-1.5 bg-gradient-to-r from-cyan-500 via-blue-500 to-indigo-500"}),e(u,{className:"pb-4",children:r("div",{className:"flex items-center justify-between",children:[r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-cyan-500 to-blue-600 shadow-md",children:e(w,{className:"h-5 w-5 text-white"})}),r("div",{children:[e(h,{className:"text-xl font-bold text-gray-900",children:"Điền từ còn thiếu vào grid"}),e("p",{className:"text-sm text-gray-500",children:"Tạo grid và nhập các từ, đánh dấu các ô còn thiếu"})]})]}),r(l,{variant:"outline",size:"sm",onClick:()=>{ae().isValid&&Z(!0)},className:"gap-2 border-blue-200 text-blue-600 hover:bg-blue-50 hover:text-blue-700",children:[e(f,{className:"h-4 w-4"}),"Xem trước"]})]})}),r(m,{className:"space-y-6",children:[e("div",{className:"rounded-xl border border-cyan-200 bg-gradient-to-r from-cyan-50 to-blue-50 p-5 shadow-sm",children:r("div",{className:"flex items-start gap-3",children:[e("div",{className:"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-cyan-100",children:e(x,{className:"h-4 w-4 text-cyan-600"})}),r("div",{className:"text-sm text-cyan-900",children:[e("p",{className:"font-semibold text-cyan-800",children:"Hướng dẫn tạo câu hỏi"}),r("ul",{className:"mt-2 space-y-1.5 text-cyan-700",children:[r("li",{className:"flex items-center gap-2",children:[e("span",{className:"h-1.5 w-1.5 rounded-full bg-cyan-400"}),"Nhập từ vào các ô trong grid (4 hàng × 7 cột)"]}),r("li",{className:"flex items-center gap-2",children:[e("span",{className:"h-1.5 w-1.5 rounded-full bg-cyan-400"}),r("span",{children:[e("strong",{children:"Double-click:"})," Đánh dấu ô còn thiếu (màu xanh dương)"]})]}),r("li",{className:"flex items-center gap-2",children:[e("span",{className:"h-1.5 w-1.5 rounded-full bg-cyan-400"}),r("span",{children:[e("strong",{children:"Triple-click:"})," Vô hiệu hóa ô (màu đỏ - không cần điền)"]})]}),r("li",{className:"flex items-center gap-2",children:[e("span",{className:"h-1.5 w-1.5 rounded-full bg-cyan-400"}),"Nhập đáp án đúng cho mỗi ô còn thiếu"]})]})]})]})}),e("div",{className:"rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4",children:e(g,{defaultValue:V,onChange:e=>G(Number(e.target.value)||1)},`points-input-${V}`)}),r("div",{className:"space-y-4",children:[e("div",{className:"flex items-center justify-between",children:r("div",{className:"flex items-center gap-2",children:[e("div",{className:"flex h-6 w-6 items-center justify-center rounded-md bg-indigo-100",children:e(w,{className:"h-3.5 w-3.5 text-indigo-600"})}),e(o,{className:"text-base font-semibold text-gray-800",children:"Grid - Nhập từ vào các ô"})]})}),e("div",{className:"overflow-x-auto rounded-xl border-2 border-indigo-200 bg-gradient-to-br from-indigo-50/50 to-blue-50/50 p-5 shadow-sm",children:r("div",{className:"inline-block min-w-full",children:[r("div",{className:"grid gap-1 mb-1",style:{gridTemplateColumns:`auto repeat(${M}, minmax(0, 1fr))`},children:[e("div",{className:"h-8"}),Array.from({length:M},(r,t)=>e("div",{className:"flex h-8 items-center justify-center text-xs font-semibold text-gray-600",children:t+1},`col-header-${t}`))]}),e("div",{className:"grid gap-1",children:F.map((t,n)=>r("div",{className:"grid gap-1",style:{gridTemplateColumns:`auto repeat(${M}, minmax(0, 1fr))`},children:[e("div",{className:"flex h-12 items-center justify-center text-xs font-semibold text-gray-600 border-2 border-transparent",children:n+1}),t.map((t,s)=>{const i=W.some(e=>e.row===n&&e.column===s),l=z.includes(`${n+1}-${s+1}`),o=me()&&de(n,s);return r("div",{className:"relative flex h-12 items-center justify-center border-2 rounded transition-colors "+(l?"border-red-400 bg-red-100 hover:bg-red-150 cursor-pointer":i?"border-blue-500 bg-blue-50 hover:bg-blue-100 cursor-pointer":o?"border-red-500 bg-red-50 hover:bg-red-100":t?"border-gray-400 bg-white hover:bg-gray-50":"border-gray-300 bg-gray-100 hover:bg-gray-200"),onClick:e=>{3===e.detail&&(e.preventDefault(),e.stopPropagation(),oe(n,s))},onDoubleClick:e=>{e.preventDefault(),e.stopPropagation(),l||le(n,s)},children:[e(c,{type:"text",maxLength:10,value:t||"",onChange:e=>{if(!i&&!l){const r=e.target.value.trim().slice(0,10);ie(n,s,r)}},onDoubleClick:e=>{e.preventDefault(),e.stopPropagation(),l||le(n,s)},disabled:i||l,readOnly:i||l,className:`h-full border-0 bg-transparent text-center text-lg font-semibold focus:ring-2 focus:ring-blue-500 ${i||l?"cursor-not-allowed pointer-events-none":""} ${l?"text-gray-500":i?"text-blue-600":""}`,placeholder:""}),i&&e("div",{className:"absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-xs font-bold text-white",children:"?"}),l&&e("div",{className:"absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs font-bold text-white",children:"🔒"})]},`${n}-${s}`)})]},`row-${n}`))})]})})]}),W.length>0&&r("div",{className:"space-y-4",children:[r("div",{className:"flex items-center gap-2",children:[e("div",{className:"flex h-6 w-6 items-center justify-center rounded-md bg-blue-100",children:e(N,{className:"h-3.5 w-3.5 text-blue-600"})}),r(o,{className:"text-base font-semibold text-gray-800",children:["Danh sách ô còn thiếu",e("span",{className:"ml-2 rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700",children:W.length})]})]}),e("div",{className:"space-y-3",children:[...W].sort((e,r)=>e.row!==r.row?e.row-r.row:e.column-r.column).map((t,n)=>{const s=F[t.row]?.[t.column];return e("div",{className:"rounded-xl border-2 border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-4 shadow-sm transition-all hover:border-blue-300 hover:shadow-md",children:r("div",{className:"flex items-start gap-4",children:[e("div",{className:"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-500 text-sm font-bold text-white shadow-sm",children:n+1}),r("div",{className:"flex-1 space-y-3",children:[r("div",{className:"flex items-center gap-2",children:[r("span",{className:"text-sm font-semibold text-gray-700",children:["Vị trí (",t.row+1,", ",t.column+1,")"]}),s&&r("span",{className:"rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-500",children:['Hiện tại: "',s,'"']})]}),r("div",{children:[r(o,{htmlFor:`answer-${t.id}`,className:"text-sm font-medium text-gray-700",children:["Đáp án đúng ",e("span",{className:"text-red-500",children:"*"})]}),e(c,{id:`answer-${t.id}`,type:"text",maxLength:100,value:t.correctAnswer,onChange:e=>ce(t.id,"correctAnswer",e.target.value.trim()),onBlur:()=>{t.correctAnswer&&""!==t.correctAnswer.trim()?t.correctAnswer.length>100&&ee(e=>({...e,[t.id]:"Đáp án không được vượt quá 100 ký tự"})):ee(e=>({...e,[t.id]:"Vui lòng nhập đáp án đúng"}))},placeholder:"Nhập đáp án đúng...",className:"mt-1.5 border-gray-200 bg-white transition-all focus:border-blue-300 focus:ring-blue-200 "+(_[t.id]?"border-red-400 focus:border-red-500 focus:ring-red-200":"")}),_[t.id]&&e("p",{className:"mt-1.5 text-sm text-red-600",children:_[t.id]})]})]}),e(l,{type:"button",variant:"ghost",size:"icon",onClick:()=>{return e=t.id,P(r=>r.filter(r=>r.id!==e)),void ee(r=>{const t={...r};return delete t[e],t});var e},className:"h-8 w-8 text-red-400 hover:bg-red-50 hover:text-red-600",children:e(b,{className:"h-4 w-4"})})]})},t.id)})})]})]})]}),r(d,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-amber-400 to-orange-400"}),e(u,{className:"pb-4",children:r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 shadow-sm",children:e(v,{className:"h-4 w-4 text-white"})}),r("div",{children:[r("div",{className:"flex items-center gap-2",children:[e(h,{className:"text-lg font-semibold text-gray-800",children:"Giải thích"}),e("span",{className:"inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2.5 py-0.5 text-xs font-medium text-amber-600",children:"Không bắt buộc"})]}),e("p",{className:"text-sm text-gray-500 mt-0.5",children:"Giải thích cho đáp án đúng — hiển thị sau khi học sinh hoàn thành câu hỏi"})]})]})}),e(m,{children:e(a,{id:"explanation",value:I,onChange:e=>{L(e.target.value),k?.(!0)},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 FillMissingWordsInGridCreator(r){return e(A,{...r})}export const WriteMissingLettersCreator=FillMissingWordsInGridCreator;
|