@tinyweb_dev/oe-exam-sdk 0.1.2 → 0.1.4
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/README.md +58 -4
- package/dist/api/contest-rooms.d.ts +13 -0
- package/dist/api/contest-rooms.js +1 -0
- package/dist/api/contest-rooms.types.d.ts +47 -0
- package/dist/api/contest-rooms.types.js +1 -0
- package/dist/api/exam-entry.d.ts +14 -0
- package/dist/api/exam-entry.js +1 -0
- package/dist/api/exam-entry.types.d.ts +93 -0
- package/dist/api/exam-entry.types.js +1 -0
- package/dist/api/exam-questions.d.ts +1 -1
- package/dist/api/exam-questions.js +1 -1
- package/dist/api/exam-questions.types.d.ts +45 -1
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +1 -1
- 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 -1
- package/dist/components/exams/CreateExamPageContainer.d.ts +3 -0
- package/dist/components/exams/CreateExamPageContainer.js +1 -1
- package/dist/components/exams/ExamCreator.d.ts +9 -3
- package/dist/components/exams/ExamCreator.js +1 -1
- package/dist/components/exams/ExamPartTabs.d.ts +1 -1
- package/dist/components/exams/ExamPartTabs.js +1 -1
- 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 -1
- package/dist/components/exams/ExamQuestionsPageContainer.d.ts +3 -0
- package/dist/components/exams/ExamQuestionsPageContainer.js +1 -1
- package/dist/components/exams/ExamViewDialog.d.ts +24 -0
- package/dist/components/exams/ExamViewDialog.js +1 -0
- package/dist/components/exams/TemplateSelector.js +1 -1
- 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/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 -1
- package/dist/components/exams/entry/EntryRestartDialog.d.ts +12 -0
- package/dist/components/exams/entry/EntryRestartDialog.js +1 -0
- package/dist/components/exams/entry/StudentExamEntryPageContainer.d.ts +31 -0
- package/dist/components/exams/entry/StudentExamEntryPageContainer.js +1 -0
- package/dist/components/exams/entry/exam-entry.states.d.ts +22 -0
- package/dist/components/exams/entry/exam-entry.states.js +1 -0
- package/dist/components/exams/entry/exam-entry.utils.d.ts +16 -0
- package/dist/components/exams/entry/exam-entry.utils.js +1 -0
- package/dist/components/exams/entry/fullscreen-loading.d.ts +5 -0
- package/dist/components/exams/entry/fullscreen-loading.js +1 -0
- package/dist/components/exams/entry/index.d.ts +7 -0
- package/dist/components/exams/entry/index.js +1 -0
- 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/index.d.ts +19 -1
- package/dist/components/exams/index.js +1 -1
- package/dist/components/exams/rooms/StudentContestRoomsPageContainer.d.ts +18 -0
- package/dist/components/exams/rooms/StudentContestRoomsPageContainer.js +1 -0
- package/dist/components/exams/rooms/contest-rooms.utils.d.ts +32 -0
- package/dist/components/exams/rooms/contest-rooms.utils.js +1 -0
- package/dist/components/exams/rooms/index.d.ts +3 -0
- package/dist/components/exams/rooms/index.js +1 -0
- package/dist/components/exams/take/ExamTakingApiContext.d.ts +10 -0
- package/dist/components/exams/take/ExamTakingApiContext.js +1 -0
- package/dist/components/exams/take/ExamTakingPageContainer.d.ts +28 -4
- package/dist/components/exams/take/ExamTakingPageContainer.js +1 -1
- package/dist/components/exams/take/components/QuestionRenderer.d.ts +25 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +1 -0
- package/dist/components/exams/take/components/index.d.ts +10 -0
- package/dist/components/exams/take/components/index.js +1 -0
- package/dist/components/exams/take/components/question-renderers/ArticlesViewer.d.ts +7 -0
- package/dist/components/exams/take/components/question-renderers/ArticlesViewer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.d.ts +22 -0
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.d.ts +14 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.d.ts +3 -0
- package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +21 -0
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.d.ts +19 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.d.ts +20 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.d.ts +32 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.d.ts +19 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.d.ts +22 -0
- package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +19 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/components/speaking/FindObjectInPicture.module.css +133 -0
- package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.d.ts +9 -0
- package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingCompareImages.module.css +153 -0
- package/dist/components/exams/take/components/speaking/SpeakingContentFrame.d.ts +7 -0
- package/dist/components/exams/take/components/speaking/SpeakingContentFrame.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingDragObjects.module.css +238 -0
- package/dist/components/exams/take/components/speaking/SpeakingExamLayout.d.ts +27 -0
- package/dist/components/exams/take/components/speaking/SpeakingExamLayout.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingHeader.d.ts +6 -0
- package/dist/components/exams/take/components/speaking/SpeakingHeader.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingInfoExchange.module.css +193 -0
- package/dist/components/exams/take/components/speaking/SpeakingInfoExchangeSummerSky.module.css +238 -0
- package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.d.ts +5 -0
- package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingMicButton.d.ts +14 -0
- package/dist/components/exams/take/components/speaking/SpeakingMicButton.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingNavButton.d.ts +7 -0
- package/dist/components/exams/take/components/speaking/SpeakingNavButton.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingPartBadge.d.ts +6 -0
- package/dist/components/exams/take/components/speaking/SpeakingPartBadge.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingReadDisplayedContent.module.css +115 -0
- package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.d.ts +11 -0
- package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.d.ts +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingStoryImages.module.css +231 -0
- package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.d.ts +13 -0
- package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.js +1 -0
- package/dist/components/exams/take/components/speaking/index.d.ts +1 -0
- package/dist/components/exams/take/components/speaking/index.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.d.ts +26 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.d.ts +32 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.d.ts +34 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/index.d.ts +13 -0
- package/dist/components/exams/take/components/speaking/renderers/index.js +1 -0
- package/dist/components/exams/take/hooks/index.d.ts +2 -0
- package/dist/components/exams/take/hooks/index.js +1 -0
- package/dist/components/exams/take/hooks/useAnswerAutosave.d.ts +27 -0
- package/dist/components/exams/take/hooks/useAnswerAutosave.js +1 -0
- package/dist/components/exams/take/hooks/useAttemptSubmit.d.ts +29 -0
- package/dist/components/exams/take/hooks/useAttemptSubmit.js +1 -0
- package/dist/components/exams/take/hooks/useAutoSave.d.ts +20 -0
- package/dist/components/exams/take/hooks/useAutoSave.js +1 -0
- package/dist/components/exams/take/hooks/useExamCountdown.d.ts +22 -0
- package/dist/components/exams/take/hooks/useExamCountdown.js +1 -0
- package/dist/components/exams/take/hooks/useExamTimer.d.ts +21 -0
- package/dist/components/exams/take/hooks/useExamTimer.js +1 -0
- package/dist/components/exams/take/hooks/useProctoringCamera.d.ts +19 -0
- package/dist/components/exams/take/hooks/useProctoringCamera.js +1 -0
- package/dist/components/exams/take/hooks/useProctoringSnapshot.d.ts +25 -0
- package/dist/components/exams/take/hooks/useProctoringSnapshot.js +1 -0
- package/dist/components/exams/take/hooks/useSpeakingConversationAudio.d.ts +19 -0
- package/dist/components/exams/take/hooks/useSpeakingConversationAudio.js +1 -0
- package/dist/components/exams/take/hooks/useSpeakingNavigation.d.ts +37 -0
- package/dist/components/exams/take/hooks/useSpeakingNavigation.js +1 -0
- package/dist/components/exams/take/hooks/useSpeakingRecorder.d.ts +45 -5
- package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -1
- package/dist/components/exams/take/hooks/useSpeakingRendererSetup.d.ts +39 -0
- package/dist/components/exams/take/hooks/useSpeakingRendererSetup.js +1 -0
- package/dist/components/exams/take/hooks/useToeicAudio.d.ts +17 -0
- package/dist/components/exams/take/hooks/useToeicAudio.js +1 -0
- package/dist/components/exams/take/index.d.ts +9 -1
- package/dist/components/exams/take/index.js +1 -1
- package/dist/components/exams/take/types/speaking-cue-card.types.d.ts +10 -0
- package/dist/components/exams/take/types/speaking-cue-card.types.js +1 -0
- package/dist/components/exams/take/types/speaking.types.d.ts +153 -0
- package/dist/components/exams/take/types/speaking.types.js +1 -0
- package/dist/components/exams/take/types.d.ts +286 -1
- package/dist/components/exams/take/utils/answer-transformers.d.ts +26 -0
- package/dist/components/exams/take/utils/answer-transformers.js +1 -0
- package/dist/components/exams/take/utils/coloring-assignments.d.ts +5 -0
- package/dist/components/exams/take/utils/coloring-assignments.js +1 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +362 -0
- package/dist/components/exams/take/utils/question-transformers.js +1 -0
- package/dist/components/features/exam-entry/entry-state-cards.d.ts +19 -0
- package/dist/components/features/exam-entry/entry-state-cards.js +1 -0
- package/dist/components/features/exam-entry/index.d.ts +2 -0
- package/dist/components/features/exam-entry/index.js +1 -0
- package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.d.ts +12 -0
- package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.js +1 -0
- package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.d.ts +14 -0
- package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.js +1 -0
- package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.d.ts +16 -0
- package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.js +1 -0
- package/dist/components/features/exam-entry/themed/components/CloseButton.d.ts +9 -0
- package/dist/components/features/exam-entry/themed/components/CloseButton.js +1 -0
- package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.d.ts +13 -0
- package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.js +1 -0
- package/dist/components/features/exam-entry/themed/components/ModeSelector.d.ts +7 -0
- package/dist/components/features/exam-entry/themed/components/ModeSelector.js +1 -0
- package/dist/components/features/exam-entry/themed/components/StartButton.d.ts +11 -0
- package/dist/components/features/exam-entry/themed/components/StartButton.js +1 -0
- package/dist/components/features/exam-entry/themed/components/index.d.ts +4 -0
- package/dist/components/features/exam-entry/themed/components/index.js +1 -0
- package/dist/components/features/exam-entry/themed/index.d.ts +1 -0
- package/dist/components/features/exam-entry/themed/index.js +1 -0
- package/dist/components/results/ResultReviewPageContainer.d.ts +3 -1
- package/dist/components/results/ResultReviewPageContainer.js +1 -1
- package/dist/components/results/hooks/useResultReviewPolling.d.ts +27 -0
- package/dist/components/results/hooks/useResultReviewPolling.js +1 -0
- package/dist/components/results/index.d.ts +3 -0
- package/dist/components/results/index.js +1 -1
- package/dist/components/results/review-status.utils.d.ts +12 -0
- package/dist/components/results/review-status.utils.js +1 -0
- package/dist/components/shared/audio/SpeakingAudioPlayer.d.ts +12 -0
- package/dist/components/shared/audio/SpeakingAudioPlayer.js +1 -0
- package/dist/components/shared/audio/SpeakingAudioPlayer.module.css +115 -0
- package/dist/components/shared/audio/assets/fr-image-part1-border.png +0 -0
- package/dist/components/shared/audio/assets/ic-speaker.webp +0 -0
- package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -1
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.d.ts +11 -3
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -1
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.d.ts +5 -1
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -1
- package/dist/components/themes/index.d.ts +11 -0
- package/dist/components/themes/index.js +1 -1
- package/dist/components/themes/spaceship/SpaceshipAudioPlayer.d.ts +8 -0
- package/dist/components/themes/spaceship/SpaceshipAudioPlayer.js +1 -0
- package/dist/components/themes/spaceship/SpaceshipLayout.d.ts +2 -0
- package/dist/components/themes/spaceship/SpaceshipLayout.js +1 -0
- package/dist/components/themes/spaceship/assets/images/X-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/X.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/audio-bar.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/background.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/bg_sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/big-blue-badge-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/big-blue-badge.png +0 -0
- package/dist/components/themes/spaceship/assets/images/big-blue-badge.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/bingo.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/blue-badge-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/blue-badge.png +0 -0
- package/dist/components/themes/spaceship/assets/images/blue-wrapper-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/blue-wrapper.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/board_red.png +0 -0
- package/dist/components/themes/spaceship/assets/images/button_feedback_vi.png +0 -0
- package/dist/components/themes/spaceship/assets/images/circle-btn-right-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/circle-btn-right.png +0 -0
- package/dist/components/themes/spaceship/assets/images/circle-btn-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/circle-btn.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/focus-vector.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/head-phone_red.png +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-mute-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-mute.png +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-play-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-play-violet.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-play.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/micro.png +0 -0
- package/dist/components/themes/spaceship/assets/images/next-small.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/next-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/next.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/purple-circle.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/question-badge-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/question-badge.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/red-badge-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/red-badge.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/red-wrapper-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/red-wrapper.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/robot-avatar.png +0 -0
- package/dist/components/themes/spaceship/assets/images/sound_red.png +0 -0
- package/dist/components/themes/spaceship/assets/images/student-avatar.png +0 -0
- package/dist/components/themes/spaceship/assets/images/teacher-play-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/teacher-play.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/timer-badge.png +0 -0
- package/dist/components/themes/spaceship/assets/styles/SpaceshipLayout.module.css +347 -0
- package/dist/components/themes/spaceship/assets/styles/SpaceshipWaitingRoom.module.css +395 -0
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.js +1 -1
- package/dist/shared/lib/stores/examTakingStore.d.ts +54 -0
- package/dist/shared/lib/stores/examTakingStore.js +1 -0
- package/dist/shared/lib/stores/gradingTransientStore.d.ts +23 -0
- package/dist/shared/lib/stores/gradingTransientStore.js +1 -0
- package/dist/shared/lib/themes/config/starters.config.d.ts +3 -0
- package/dist/shared/lib/themes/config/starters.config.js +1 -0
- package/dist/shared/lib/themes/index.d.ts +18 -0
- package/dist/shared/lib/themes/index.js +1 -0
- package/dist/shared/lib/themes/types.d.ts +39 -0
- package/dist/shared/lib/themes/types.js +1 -0
- package/dist/shared/lib/toast.d.ts +19 -0
- package/dist/shared/lib/toast.js +1 -0
- 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-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 -1
- package/package.json +19 -4
package/README.md
CHANGED
|
@@ -18,9 +18,11 @@ import {
|
|
|
18
18
|
ExamQuestionsPageContainer,
|
|
19
19
|
ExamTakingPageContainer,
|
|
20
20
|
ResultReviewPageContainer,
|
|
21
|
+
StudentExamEntryPageContainer,
|
|
21
22
|
} from '@tinyweb_dev/oe-exam-sdk';
|
|
22
23
|
import {
|
|
23
24
|
createExamCreateApi,
|
|
25
|
+
createExamEntryApi,
|
|
24
26
|
createExamQuestionsApi,
|
|
25
27
|
createExamTakingApi,
|
|
26
28
|
createResultReviewApi,
|
|
@@ -47,12 +49,26 @@ export function ExamQuestionsPage({ examId, router }) {
|
|
|
47
49
|
);
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
export function
|
|
52
|
+
export function ExamEnterPage({ roomId, inviteToken, router }) {
|
|
53
|
+
return (
|
|
54
|
+
<StudentExamEntryPageContainer
|
|
55
|
+
roomId={roomId}
|
|
56
|
+
inviteToken={inviteToken}
|
|
57
|
+
api={createExamEntryApi({ baseUrl })}
|
|
58
|
+
onNavigate={(href) => router.push(href)}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ExamTakePage({ roomId, router }) {
|
|
51
64
|
return (
|
|
52
65
|
<ExamTakingPageContainer
|
|
53
|
-
|
|
66
|
+
roomId={roomId}
|
|
54
67
|
api={createExamTakingApi({ baseUrl })}
|
|
55
68
|
onNavigate={(href) => router.replace(href)}
|
|
69
|
+
resultPath={(attemptId) => `/student/results/${attemptId}/review`}
|
|
70
|
+
onSubmitted={({ attemptId }) => router.replace(`/student/results/${attemptId}/review`)}
|
|
71
|
+
proctoringEnabled
|
|
56
72
|
/>
|
|
57
73
|
);
|
|
58
74
|
}
|
|
@@ -77,8 +93,9 @@ export function ResultReviewPage({ attemptId, token, router }) {
|
|
|
77
93
|
@tinyweb_dev/oe-exam-sdk/shared # shared constants/types/helpers
|
|
78
94
|
@tinyweb_dev/oe-exam-sdk/mcp # MCP server entry và tools
|
|
79
95
|
@tinyweb_dev/oe-exam-sdk/components # shared UI components
|
|
80
|
-
@tinyweb_dev/oe-exam-sdk/components/exams # màn tạo đề / câu hỏi / làm bài
|
|
81
|
-
@tinyweb_dev/oe-exam-sdk/components/exams/
|
|
96
|
+
@tinyweb_dev/oe-exam-sdk/components/exams # màn tạo đề / câu hỏi / làm bài / vào phòng
|
|
97
|
+
@tinyweb_dev/oe-exam-sdk/components/exams/entry # màn vào phòng (entry) + state machine
|
|
98
|
+
@tinyweb_dev/oe-exam-sdk/components/exams/take # màn làm bài + renderer generic + hooks
|
|
82
99
|
@tinyweb_dev/oe-exam-sdk/components/questions # hệ thống tạo câu hỏi
|
|
83
100
|
@tinyweb_dev/oe-exam-sdk/components/themes # cambridge-yle, english-certification, summer-sky
|
|
84
101
|
@tinyweb_dev/oe-exam-sdk/components/results # màn xem chi tiết kết quả
|
|
@@ -109,6 +126,40 @@ Các adapter mặc định nhận:
|
|
|
109
126
|
|
|
110
127
|
`createExamTakingApi` đã bao gồm các API cần cho màn làm bài: start/resume attempt, questions, save answers, submit, upload audio, proctoring snapshot, realtime grading, grading summary/status/detail, room/attempt detail và `keepalive` save.
|
|
111
128
|
|
|
129
|
+
`createExamEntryApi` phục vụ màn vào phòng: room detail, active attempt theo room, start/delete attempt, join qua invite token và đăng ký contest (registration form).
|
|
130
|
+
|
|
131
|
+
## Màn vào phòng (entry)
|
|
132
|
+
|
|
133
|
+
`StudentExamEntryPageContainer` port toàn bộ luồng `/student/exams/:roomId/enter`: join qua invite, gating quyền truy cập / đề thi / form đăng ký, state machine theo cửa sổ thời gian (BEFORE_START / WITHIN_WINDOW / EXPIRED / ALREADY_SUBMITTED), resume attempt đang dở và dialog "làm tiếp / làm lại từ đầu".
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
<StudentExamEntryPageContainer
|
|
137
|
+
roomId={roomId}
|
|
138
|
+
inviteToken={inviteToken}
|
|
139
|
+
api={createExamEntryApi({ baseUrl })}
|
|
140
|
+
onNavigate={(href) => router.push(href)}
|
|
141
|
+
takePath={(id) => `/student/exams/${id}/take`}
|
|
142
|
+
renderRegistrationForm={({ room, onSubmit, isSubmitting }) => (
|
|
143
|
+
<MyRegistrationDialog room={room} onSubmit={onSubmit} isSubmitting={isSubmitting} />
|
|
144
|
+
)}
|
|
145
|
+
/>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Themed waiting room (TOEIC / Summer Sky / Cambridge YLE) và registration form là phần phụ thuộc host app — truyền qua `renderRegistrationForm` hoặc bọc container.
|
|
149
|
+
|
|
150
|
+
## Hooks màn làm bài
|
|
151
|
+
|
|
152
|
+
`@tinyweb_dev/oe-exam-sdk/components/exams/take` export các hook tái sử dụng:
|
|
153
|
+
|
|
154
|
+
```txt
|
|
155
|
+
useExamCountdown # đếm ngược theo startedAt + duration, auto-submit khi hết giờ
|
|
156
|
+
useAnswerAutosave # lưu debounce theo batch + flush keepalive khi ẩn tab/unload + retry khi online
|
|
157
|
+
useProctoringSnapshot # chụp webcam định kỳ và upload snapshot
|
|
158
|
+
useAttemptSubmit # nộp bài (flush trước), chống double-submit, trả pending/auto-graded answer ids
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`ExamTakingPageContainer` đã nối sẵn các hook trên. `onSubmitted` nhận `{ attemptId, pendingAnswerIds }` để điều hướng sang màn review đúng ngữ cảnh; bật proctoring qua prop `proctoringEnabled` và truyền `achieversExamTitle` nếu cần gate video intro của giáo viên.
|
|
162
|
+
|
|
112
163
|
## Theme làm bài
|
|
113
164
|
|
|
114
165
|
`ExamTakingPageContainer` tự chọn layout theo `examTemplate.theme`:
|
|
@@ -129,12 +180,15 @@ Renderer câu hỏi trong màn làm bài hiện là `GenericQuestionRenderer`.
|
|
|
129
180
|
attemptId={attemptId}
|
|
130
181
|
token={token}
|
|
131
182
|
api={createResultReviewApi({ baseUrl })}
|
|
183
|
+
isPrintMode={isPrintMode}
|
|
132
184
|
topActionsSlot={null}
|
|
133
185
|
beforeReportSlot={null}
|
|
134
186
|
afterReportSlot={null}
|
|
135
187
|
/>
|
|
136
188
|
```
|
|
137
189
|
|
|
190
|
+
Container tự poll lại kết quả khi còn câu đang chấm hoặc LLM assessment chưa sẵn sàng, dừng poll ở `isPrintMode` và phát tín hiệu `body[data-pdf-ready="true"]` cho headless PDF renderer. Lỗi 403 "kết quả chưa công khai" được nhận diện qua helper `isResultNotPublic` và hiển thị màn thông báo riêng thay vì toast lỗi.
|
|
191
|
+
|
|
138
192
|
Các phần phụ thuộc app chính như gift dialog, upgrade modal, summary video/remotion nên truyền qua slot từ host app.
|
|
139
193
|
|
|
140
194
|
## MCP server
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ContestRoomListItem, ContestRoomsContestDetail, ContestRoomsFetchApiOptions, ContestRoomsSdkApi } from './contest-rooms.types';
|
|
2
|
+
/**
|
|
3
|
+
* Default fetch-based adapter for the student contest rooms-list flow. Mirrors
|
|
4
|
+
* the host app endpoints used by the original `/student/contests/:contestId/rooms`
|
|
5
|
+
* page: contest detail, room list and room registration.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createContestRoomsApi(options?: ContestRoomsFetchApiOptions): ContestRoomsSdkApi;
|
|
8
|
+
/** In-memory mock adapter for demo/storybook/tests. */
|
|
9
|
+
export declare function createMockContestRoomsApi(seed: {
|
|
10
|
+
contest: ContestRoomsContestDetail;
|
|
11
|
+
rooms: ContestRoomListItem[];
|
|
12
|
+
}): ContestRoomsSdkApi;
|
|
13
|
+
export type { ContestRoomListItem, ContestRoomRegistrationStatus, ContestRoomVisibility, ContestRoomsContestDetail, ContestRoomsFetchApiOptions, ContestRoomsSdkApi, } from './contest-rooms.types';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{API_BASE_URL as t,API_ENDPOINTS as e}from"../shared/constants/ApiConstant";export function createContestRoomsApi(n={}){const o=function({baseUrl:e=t??"",fetcher:n=fetch,headers:o}){return async function(t,s){const r=await n(`${e}${t}`,{credentials:"include",...s,headers:{"Content-Type":"application/json",...o,...s?.headers}});if(!r.ok)throw new Error(await async function(t){const e=await t.json().catch(()=>{});if("object"==typeof e&&null!==e&&"message"in e&&"string"==typeof e.message)return e.message;return t.statusText}(r));if(204!==r.status)return r.json()}}(n);return{getContestDetail:t=>o(e.STUDENT.CONTEST_DETAIL(t)),getContestRooms:t=>o(e.STUDENT.CONTEST_ROOMS(t)),registerRoom:(t,n)=>o(e.STUDENT.CONTEST_ROOM_REGISTER(t,n),{method:"POST"}),cancelRoomRegistration:(t,n)=>o(e.STUDENT.CONTEST_ROOM_REGISTER(t,n),{method:"DELETE"})}}export function createMockContestRoomsApi(t){const e=[...t.rooms];return{getContestDetail:async()=>t.contest,getContestRooms:async()=>e,registerRoom:async(t,n)=>{const o=e.find(t=>t.id===n);o&&(o.registrationStatus="PENDING")},cancelRoomRegistration:async(t,n)=>{const o=e.find(t=>t.id===n);o&&(o.registrationStatus=null)}}}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ApiData } from './exam-entry.types';
|
|
2
|
+
export type ContestRoomVisibility = 'public' | 'internal' | 'private';
|
|
3
|
+
export type ContestRoomRegistrationStatus = 'PENDING' | 'APPROVED' | 'REJECTED';
|
|
4
|
+
/** Contest summary needed by the rooms list header/gating. */
|
|
5
|
+
export interface ContestRoomsContestDetail {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
status?: 'UPCOMING' | 'IN_PROGRESS' | 'COMPLETED';
|
|
9
|
+
allowMultipleSubmissions?: boolean | null;
|
|
10
|
+
}
|
|
11
|
+
/** Room item as listed on `/student/contests/:contestId/rooms`. */
|
|
12
|
+
export interface ContestRoomListItem {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
code: string;
|
|
16
|
+
examName: string;
|
|
17
|
+
status: 'UPCOMING' | 'IN_PROGRESS' | 'COMPLETED';
|
|
18
|
+
startDatetime?: string;
|
|
19
|
+
endDatetime?: string;
|
|
20
|
+
isLocked: boolean;
|
|
21
|
+
hasAttempt: boolean;
|
|
22
|
+
attemptStatus?: string;
|
|
23
|
+
isCompleted?: boolean;
|
|
24
|
+
visibility?: ContestRoomVisibility;
|
|
25
|
+
registrationStatus?: ContestRoomRegistrationStatus | null;
|
|
26
|
+
registrationCount?: number;
|
|
27
|
+
allowMultipleSubmissions?: boolean | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* API surface the rooms-list container depends on. Methods may return either
|
|
31
|
+
* the raw value or an `{ data }` envelope; the container unwraps both.
|
|
32
|
+
*/
|
|
33
|
+
export interface ContestRoomsSdkApi {
|
|
34
|
+
/** Fetch contest detail (used for header + multi-submission gating). */
|
|
35
|
+
getContestDetail: (contestId: string) => Promise<ApiData<ContestRoomsContestDetail> | ContestRoomsContestDetail>;
|
|
36
|
+
/** Fetch the list of rooms for a contest. */
|
|
37
|
+
getContestRooms: (contestId: string) => Promise<ApiData<ContestRoomListItem[]> | ContestRoomListItem[]>;
|
|
38
|
+
/** Register for an internal-visibility room. */
|
|
39
|
+
registerRoom: (contestId: string, roomId: string) => Promise<unknown>;
|
|
40
|
+
/** Cancel a pending/approved room registration. */
|
|
41
|
+
cancelRoomRegistration?: (contestId: string, roomId: string) => Promise<unknown>;
|
|
42
|
+
}
|
|
43
|
+
export interface ContestRoomsFetchApiOptions {
|
|
44
|
+
baseUrl?: string;
|
|
45
|
+
fetcher?: typeof fetch;
|
|
46
|
+
headers?: HeadersInit;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExamEntryAttempt, ExamEntryFetchApiOptions, ExamEntryRoom, ExamEntrySdkApi } from './exam-entry.types';
|
|
2
|
+
/**
|
|
3
|
+
* Default fetch-based adapter for the student exam entry flow. Mirrors the host
|
|
4
|
+
* app endpoints used by the original `/student/exams/:roomId/enter` page:
|
|
5
|
+
* room detail, active attempt by room, start/delete attempt, invite join and
|
|
6
|
+
* contest registration.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createExamEntryApi(options?: ExamEntryFetchApiOptions): ExamEntrySdkApi;
|
|
9
|
+
/** In-memory mock adapter for demo/storybook/tests. */
|
|
10
|
+
export declare function createMockExamEntryApi(seed: {
|
|
11
|
+
room: ExamEntryRoom;
|
|
12
|
+
attempt?: ExamEntryAttempt | null;
|
|
13
|
+
}): ExamEntrySdkApi;
|
|
14
|
+
export type { ExamEntryAttempt, ExamEntryContest, ExamEntryExam, ExamEntryFetchApiOptions, ExamEntryRegistrationField, ExamEntryRegistrationFormConfig, ExamEntryRoom, ExamEntrySdkApi, } from './exam-entry.types';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{API_BASE_URL as t,API_ENDPOINTS as e}from"../shared/constants/ApiConstant";export function createExamEntryApi(n={}){const o=function({baseUrl:e=t??"",fetcher:n=fetch,headers:o}){return async function(t,a){const r=await n(`${e}${t}`,{credentials:"include",...a,headers:{"Content-Type":"application/json",...o,...a?.headers}});if(!r.ok)throw new Error(await async function(t){const e=await t.json().catch(()=>{});if("object"==typeof e&&null!==e&&"message"in e&&"string"==typeof e.message)return e.message;return t.statusText}(r));if(204!==r.status)return r.json()}}(n);return{getRoom:t=>o(e.STUDENT.ROOM_DETAIL(t)),getActiveAttemptByRoom:t=>o(e.STUDENT.ATTEMPTS_ACTIVE_BY_ROOM(t)),startAttempt:t=>o(e.STUDENT.ATTEMPTS_START,{method:"POST",body:JSON.stringify({examRoomId:t})}),deleteAttempt:t=>o(e.STUDENT.ATTEMPT_DELETE(t),{method:"DELETE"}),joinViaInvite:(t,n)=>o(e.STUDENT.ROOM_JOIN_INVITE(t,n),{method:"POST"}),registerContest:(t,n)=>o(e.STUDENT.CONTEST_REGISTER(t),{method:"POST",body:JSON.stringify(n)})}}export function createMockExamEntryApi(t){let e=t.attempt??null;return{getRoom:async()=>t.room,getActiveAttemptByRoom:async()=>e,startAttempt:async()=>(e={id:`mock-attempt-${Date.now()}`,startedAt:(new Date).toISOString(),durationMinutes:t.room.exam?.durationMinutes??30,examRoomId:t.room.id,status:"IN_PROGRESS"},e),deleteAttempt:async()=>{e=null},joinViaInvite:async()=>{},registerContest:async()=>{}}}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ExamTakingAttempt } from './exam-taking.types';
|
|
2
|
+
/** Generic envelope used by the OE Exam API ({ data: ... }). */
|
|
3
|
+
export interface ApiData<T> {
|
|
4
|
+
data: T;
|
|
5
|
+
}
|
|
6
|
+
/** Registration form field config for contest rooms. */
|
|
7
|
+
export interface ExamEntryRegistrationField {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
type: string;
|
|
11
|
+
required: boolean;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
options?: string[];
|
|
14
|
+
userPropertyMapping?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Registration form config attached to a room. */
|
|
17
|
+
export interface ExamEntryRegistrationFormConfig {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
fields: ExamEntryRegistrationField[];
|
|
20
|
+
}
|
|
21
|
+
/** Exam summary nested inside a room. */
|
|
22
|
+
export interface ExamEntryExam {
|
|
23
|
+
id: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
durationMinutes?: number;
|
|
27
|
+
totalScore?: number;
|
|
28
|
+
passScore?: number;
|
|
29
|
+
level?: string;
|
|
30
|
+
theme?: string;
|
|
31
|
+
template?: {
|
|
32
|
+
theme?: string;
|
|
33
|
+
subTitle?: string;
|
|
34
|
+
title?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Contest summary nested inside a room. */
|
|
38
|
+
export interface ExamEntryContest {
|
|
39
|
+
id: string;
|
|
40
|
+
code?: string | null;
|
|
41
|
+
name?: string;
|
|
42
|
+
status?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Room as needed by the entry screen. Mirrors the fields the entry flow reads
|
|
46
|
+
* from `studentRoomsApi.getById` in the host app.
|
|
47
|
+
*/
|
|
48
|
+
export interface ExamEntryRoom {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
code?: string;
|
|
53
|
+
examId?: string;
|
|
54
|
+
startDatetime?: string;
|
|
55
|
+
endDatetime?: string;
|
|
56
|
+
isLocked?: boolean;
|
|
57
|
+
hasJoined?: boolean;
|
|
58
|
+
canAccess?: boolean;
|
|
59
|
+
allowMultipleSubmissions?: boolean;
|
|
60
|
+
hasSubmittedFormData?: boolean;
|
|
61
|
+
registrationFormConfig?: ExamEntryRegistrationFormConfig;
|
|
62
|
+
exam?: ExamEntryExam;
|
|
63
|
+
contest?: ExamEntryContest;
|
|
64
|
+
}
|
|
65
|
+
/** Attempt as needed by the entry screen. */
|
|
66
|
+
export type ExamEntryAttempt = ExamTakingAttempt;
|
|
67
|
+
/**
|
|
68
|
+
* API surface the entry container depends on. All methods may return either the
|
|
69
|
+
* raw value or an `{ data }` envelope; the container unwraps both.
|
|
70
|
+
*/
|
|
71
|
+
export interface ExamEntrySdkApi {
|
|
72
|
+
/** Fetch room detail by id. */
|
|
73
|
+
getRoom: (roomId: string) => Promise<ApiData<ExamEntryRoom> | ExamEntryRoom>;
|
|
74
|
+
/** Fetch the active (IN_PROGRESS) attempt for a specific room, or null. */
|
|
75
|
+
getActiveAttemptByRoom: (roomId: string) => Promise<ApiData<ExamEntryAttempt | null> | ExamEntryAttempt | null>;
|
|
76
|
+
/** Start a new attempt for the room. */
|
|
77
|
+
startAttempt: (roomId: string) => Promise<ApiData<ExamEntryAttempt | {
|
|
78
|
+
attempt: ExamEntryAttempt;
|
|
79
|
+
}> | ExamEntryAttempt | {
|
|
80
|
+
attempt: ExamEntryAttempt;
|
|
81
|
+
}>;
|
|
82
|
+
/** Delete an attempt (used by "restart from scratch"). */
|
|
83
|
+
deleteAttempt: (attemptId: string) => Promise<unknown>;
|
|
84
|
+
/** Join a room via an invite token (best-effort; may already be a member). */
|
|
85
|
+
joinViaInvite?: (roomId: string, inviteToken: string) => Promise<unknown>;
|
|
86
|
+
/** Register for the contest backing the room (submits registration form data). */
|
|
87
|
+
registerContest?: (contestId: string, formData: Record<string, unknown>) => Promise<unknown>;
|
|
88
|
+
}
|
|
89
|
+
export interface ExamEntryFetchApiOptions {
|
|
90
|
+
baseUrl?: string;
|
|
91
|
+
fetcher?: typeof fetch;
|
|
92
|
+
headers?: HeadersInit;
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -6,4 +6,4 @@ export declare function createMockExamQuestionsApi(seed: {
|
|
|
6
6
|
exam: ExamWithTemplate;
|
|
7
7
|
questions?: Partial<ExamQuestion>[];
|
|
8
8
|
}): ExamQuestionsSdkApi;
|
|
9
|
-
export type { ExamQuestionsSdkApi, ExamWithTemplate, FetchApiOptions, SaveExamInput, SaveQuestionInput, UpdatePartAudioInput } from './exam-questions.types';
|
|
9
|
+
export type { ExamQuestionsSdkApi, ExamWithTemplate, ExistingQuestionBank, FetchApiOptions, QuestionSearchItem, SaveExamInput, SaveQuestionInput, SaveQuestionResult, SearchQuestionBankInput, SearchQuestionBankResult, UpdatePartAudioInput, } from './exam-questions.types';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{API_BASE_URL as t,API_ENDPOINTS as e}from"../shared/constants/ApiConstant";import{transformQuestionToApiFormat as a}from"../shared/lib/utils/question-transform";export function createExamQuestionsApi(
|
|
1
|
+
import{API_BASE_URL as t,API_ENDPOINTS as e}from"../shared/constants/ApiConstant";import{transformQuestionToApiFormat as s}from"../shared/lib/utils/question-transform";import{transformApiQuestionsToFrontend as a}from"../shared/lib/utils/question-reverse-transform";export function createExamQuestionsApi(n={}){const i=function({baseUrl:e=t??"",fetcher:s=fetch,headers:a,apiKey:n}){return async function(t,i){const o=n?{"X-API-Key":n}:void 0,r=await s(`${e}${t}`,{...n?{}:{credentials:"include"},...i,headers:{"Content-Type":"application/json",...o,...a,...i?.headers}});if(!r.ok){const t=await r.json().catch(()=>({}));throw new Error(t.message||r.statusText)}return r.json()}}(n);return{getExam:t=>i(e.MANAGE.EXAM_DETAIL(t)),getQuestions:async t=>{const s=await i(e.MANAGE.EXAM_QUESTIONS(t)),n=Array.isArray(s.data)?s.data:s.data.questions??[];return a(n)},saveQuestion:async t=>{const{examId:a,question:n,template:o}=t;if(!a)throw new Error("examId is required");const r=s(n,function(t){const{question:e,template:s,addToQuestionBank:a,questionBankCode:n,questionBankTitle:i,sectionId:o,examLevel:r,subjectId:u}=t,d=s?.parts.findIndex(t=>t.id===e.partId)??-1,p=d>=0?d+1:void 0,c=e.sectionId;return{partId:e.partId,partNo:p,questionNumber:e.questionNumber,sectionId:o??c,addToQuestionBank:a,questionBankCode:n,questionBankTitle:i,examLevel:r,subjectId:u}}(t)),u=await i(e.MANAGE.EXAM_QUESTIONS(a),{method:"POST",body:JSON.stringify({examId:a,questions:[r]})});if((u.data.summary?.errorCount??0)>0)throw new Error("Unable to save question");const d=u.data.results?.[0]?.data,p={...n,id:d?.id??n.id,...d?.questionGroup?{questionGroup:d.questionGroup}:{},isCompleted:!0},c=(m=d?.questionBank,"object"!=typeof m||null===m||Array.isArray(m)?void 0:m);var m;return{question:p,questionBank:c?{id:String(c.id??""),code:String(c.code??""),title:String(c.title??"")}:void 0}},saveExam:async()=>{},updatePartAudio:t=>async function(t,{examId:s,partId:a,audioUrl:n,template:i}){await t(e.MANAGE.EXAM_TEMPLATE(s),{method:"PATCH",body:JSON.stringify({id:i.id,title:i.name,subTitle:"",theme:i.theme||"",totalQuestions:i.totalQuestions,totalParts:i.parts.length,description:i.description||"",examLevel:i.level||"",type:i.type,parts:i.parts.map(t=>({...t,audioUrl:t.id===a?n:t.audioUrl}))})})}(i,t),searchQuestions:async({questionType:t,keyword:s,page:a,size:n})=>{const o=new URLSearchParams;t&&o.set("questionType",t),s&&o.set("keyword",s),o.set("page",String(a)),o.set("size",String(n));const r=o.toString()?`?${o.toString()}`:"";return i(`${e.MANAGE.QUESTION_BANKS}${r}`)}}}export const createExamQuestionsFetchApi=createExamQuestionsApi;export function createMockExamQuestionsApi(t){const e=new Map((t.questions??[]).map(t=>[(t.questionNumber??1)-1,t]));let s=t.exam;return{getExam:async()=>({data:s}),getQuestions:async()=>Array.from(e.values()),saveQuestion:async({question:t,questionIndex:s})=>{const a={...t,questionNumber:t.questionNumber??s+1,isCompleted:!0};return e.set(s,a),{question:a}},saveExam:async()=>{},updatePartAudio:async({partId:t,audioUrl:e})=>{s.template&&"parts"in s.template&&(s={...s,template:{...s.template,parts:s.template.parts.map(s=>s.id===t?{...s,audioUrl:e}:s)}})},searchQuestions:async({questionType:t})=>({data:Array.from(e.values()).filter(e=>!t||e.type===t).map(t=>({id:t.id??String(t.questionNumber??""),questionType:t.type??"",content:t.content,correctAnswer:t.answer,points:t.points,difficulty:t.difficulty,skill:t.skill,explanation:t.explanation})),meta:{totalItems:e.size}})}}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type { QuestionType } from '../shared/types/common.types';
|
|
1
2
|
import type { ExamData, ExamPart, ExamQuestion, ExamTemplate } from '../shared/types/entities/exam.types';
|
|
2
3
|
export interface ExamQuestionsSdkApi {
|
|
3
4
|
getExam: (examId: string) => Promise<ExamQuestionsApiResponse<ExamWithTemplate> | ExamWithTemplate>;
|
|
4
5
|
getQuestions?: (examId: string) => Promise<ExamQuestionsApiResponse<Partial<ExamQuestion>[]> | Partial<ExamQuestion>[]>;
|
|
5
|
-
saveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | void>;
|
|
6
|
+
saveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | SaveQuestionResult | void>;
|
|
6
7
|
saveExam?: (input: SaveExamInput) => Promise<void>;
|
|
7
8
|
updatePartAudio?: (input: UpdatePartAudioInput) => Promise<void>;
|
|
9
|
+
searchQuestions?: (input: SearchQuestionBankInput) => Promise<SearchQuestionBankResult>;
|
|
8
10
|
}
|
|
9
11
|
export interface ExamQuestionsApiResponse<T> {
|
|
10
12
|
data: T;
|
|
@@ -24,6 +26,7 @@ export interface EmbeddedExamTemplate extends Omit<ExamTemplate, 'name' | 'level
|
|
|
24
26
|
title: string;
|
|
25
27
|
subTitle?: string;
|
|
26
28
|
examLevel: string;
|
|
29
|
+
totalParts?: number;
|
|
27
30
|
parts: Array<ExamPart & {
|
|
28
31
|
title?: string;
|
|
29
32
|
}>;
|
|
@@ -36,6 +39,20 @@ export interface SaveQuestionInput {
|
|
|
36
39
|
examDifficulty?: string;
|
|
37
40
|
examLevel?: string;
|
|
38
41
|
subjectId?: string;
|
|
42
|
+
sectionId?: number;
|
|
43
|
+
addToQuestionBank?: boolean;
|
|
44
|
+
questionBankCode?: string;
|
|
45
|
+
questionBankTitle?: string;
|
|
46
|
+
existingQuestionBankId?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface SaveQuestionResult {
|
|
49
|
+
question: Partial<ExamQuestion>;
|
|
50
|
+
questionBank?: ExistingQuestionBank | null;
|
|
51
|
+
}
|
|
52
|
+
export interface ExistingQuestionBank {
|
|
53
|
+
id: string;
|
|
54
|
+
code: string;
|
|
55
|
+
title: string;
|
|
39
56
|
}
|
|
40
57
|
export interface SaveExamInput {
|
|
41
58
|
examId: string;
|
|
@@ -49,6 +66,33 @@ export interface UpdatePartAudioInput {
|
|
|
49
66
|
audioUrl: string;
|
|
50
67
|
template: ExamTemplate;
|
|
51
68
|
}
|
|
69
|
+
export interface SearchQuestionBankInput {
|
|
70
|
+
questionType?: QuestionType;
|
|
71
|
+
keyword?: string;
|
|
72
|
+
page: number;
|
|
73
|
+
size: number;
|
|
74
|
+
}
|
|
75
|
+
export interface QuestionSearchItem {
|
|
76
|
+
id: string;
|
|
77
|
+
questionType: QuestionType | string;
|
|
78
|
+
content: unknown;
|
|
79
|
+
correctAnswer: unknown;
|
|
80
|
+
points?: string | number;
|
|
81
|
+
difficulty?: string;
|
|
82
|
+
skill?: string;
|
|
83
|
+
explanation?: string | null;
|
|
84
|
+
createdAt?: string;
|
|
85
|
+
updatedAt?: string;
|
|
86
|
+
code?: string;
|
|
87
|
+
title?: string | null;
|
|
88
|
+
usageCount?: number;
|
|
89
|
+
}
|
|
90
|
+
export interface SearchQuestionBankResult {
|
|
91
|
+
data: QuestionSearchItem[];
|
|
92
|
+
meta?: {
|
|
93
|
+
totalItems?: number;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
52
96
|
export interface FetchApiOptions {
|
|
53
97
|
baseUrl?: string;
|
|
54
98
|
fetcher?: typeof fetch;
|
package/dist/api/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './exam-create';
|
|
2
2
|
export * from './exam-questions';
|
|
3
3
|
export * from './exam-taking';
|
|
4
|
+
export * from './exam-entry';
|
|
5
|
+
export * from './contest-rooms';
|
|
4
6
|
export * from './result-review';
|
|
5
7
|
export * from '../shared/constants/ApiConstant';
|
|
6
8
|
export * from '../shared/constants/QueryKeyConstant';
|
package/dist/api/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./exam-create";export*from"./exam-questions";export*from"./exam-taking";export*from"./result-review";export*from"../shared/constants/ApiConstant";export*from"../shared/constants/QueryKeyConstant";
|
|
1
|
+
export*from"./exam-create";export*from"./exam-questions";export*from"./exam-taking";export*from"./exam-entry";export*from"./contest-rooms";export*from"./result-review";export*from"../shared/constants/ApiConstant";export*from"../shared/constants/QueryKeyConstant";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface CompletionDialogProps {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
completedCount: number;
|
|
5
|
+
totalQuestions: number;
|
|
6
|
+
onConfirm: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function CompletionDialog({ open, onOpenChange, completedCount, totalQuestions, onConfirm, }: CompletionDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as n}from"react/jsx-runtime";import{AlertDialog as t,AlertDialogAction as h,AlertDialogCancel as c,AlertDialogContent as o,AlertDialogDescription as i,AlertDialogFooter as r,AlertDialogHeader as l,AlertDialogTitle as s}from"../../components/ui/alert-dialog";import{AlertTriangle as a,CheckCircle as d}from"lucide-react";export function CompletionDialog({open:m,onOpenChange:p,completedCount:g,totalQuestions:u,onConfirm:x}){const C=g===u;return e(t,{open:m,onOpenChange:p,children:n(o,{children:[n(l,{children:[n(s,{className:"flex items-center gap-2",children:[C?e(d,{className:"h-5 w-5 text-green-500"}):e(a,{className:"h-5 w-5 text-amber-500"}),C?"Lưu đề thi thành công":"Đề thi chưa hoàn thành"]}),e(i,{asChild:!0,children:n("div",{className:"space-y-2 text-sm text-muted-foreground",children:[n("p",{children:["Bạn đã hoàn thành ",n("strong",{children:[g,"/",u]})," câu hỏi."]}),C?e("p",{children:"Đề thi đã sẵn sàng để gán vào phòng thi."}):n("p",{children:["Còn ",e("strong",{children:u-g})," câu chưa được tạo. Bạn có thể tiếp tục tạo sau hoặc thoát và quay lại sau."]})]})})]}),n(r,{children:[e(c,{children:C?"Tiếp tục chỉnh sửa":"Tiếp tục tạo"}),e(h,{onClick:x,className:C?"bg-green-600 hover:bg-green-700":"bg-blue-600 hover:bg-blue-700",children:C?"Xác nhận":"Lưu và thoát"})]})]})})}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { type UseFormReturn } from 'react-hook-form';
|
|
2
2
|
import type { ExamFormData, ExamLevelOption } from '../../api/exam-create';
|
|
3
|
+
import type { CreateExamTexts } from './create-exam.texts';
|
|
3
4
|
export interface CreateExamInfoFormProps {
|
|
4
5
|
form: UseFormReturn<ExamFormData>;
|
|
5
6
|
examLevels: ExamLevelOption[];
|
|
6
7
|
isLoadingExamLevels?: boolean;
|
|
8
|
+
/** Optional label overrides (e.g. to reuse this form for homework). */
|
|
9
|
+
texts?: CreateExamTexts;
|
|
7
10
|
}
|
|
8
|
-
export declare function CreateExamInfoForm({ form, examLevels, isLoadingExamLevels }: CreateExamInfoFormProps): import("react").JSX.Element;
|
|
11
|
+
export declare function CreateExamInfoForm({ form, examLevels, isLoadingExamLevels, texts, }: CreateExamInfoFormProps): import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useCallback as a,useEffect as
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useCallback as a,useEffect as n,useRef as o}from"react";import{Controller as l}from"react-hook-form";import{FileText as i,Loader2 as t}from"lucide-react";import{Card as d,CardContent as c,CardDescription as m,CardHeader as s,CardTitle as u}from"../../components/ui/card";import{Input as h}from"../../components/ui/input";import{Label as f}from"../../components/ui/label";import{Select as p,SelectContent as g,SelectItem as x,SelectTrigger as b,SelectValue as N}from"../../components/ui/select";import{Textarea as v}from"../../components/ui/textarea";import{useT as y}from"../../shared/lib/i18n";const C=["EASY","MEDIUM","HARD"];export function CreateExamInfoForm({form:f,examLevels:S,isLoadingExamLevels:w=!1,texts:M}){const q=y(),D=o(!1),I=f.watch("name"),V=o(f.setValue);V.current=f.setValue;const j=a(e=>e.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/đ/g,"d").replace(/Đ/g,"D").toLowerCase().trim().replace(/[^a-z0-9\s-]/g,"").replace(/[\s]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").substring(0,100),[]);n(()=>{D.current||V.current("code",I?j(I):"",{shouldValidate:!1})},[I,j]);const A=e=>Boolean(f.formState.errors[e]),H=e=>f.formState.errors[e]?.message;return r(d,{className:"border border-gray-200",children:[r(s,{className:"border-b border-gray-100 bg-gray-50/50",children:[r(u,{className:"flex items-center gap-2 text-base font-semibold",children:[e(i,{className:"h-4 w-4 text-blue-600"}),M?.basicInfo??q("admin.exams.form.basicInfo")]}),e(m,{children:M?.requiredHint??q("admin.exams.form.requiredHint")})]}),e(c,{className:"space-y-4 pt-6",children:r("div",{className:"grid grid-cols-1 gap-4 md:grid-cols-2",children:[e(P,{label:M?.nameLabel??q("admin.exams.form.nameLabel"),error:H("name"),required:!0,hint:q("admin.exams.form.charCount",{count:String(I?.length||0),max:String(255)}),children:e(h,{id:"name",maxLength:255,className:A("name")?"border-red-500 focus:ring-red-500":"",placeholder:M?.namePlaceholder??q("admin.exams.form.namePlaceholder"),...f.register("name")})}),e(P,{label:M?.codeLabel??q("admin.exams.form.codeLabel"),error:H("code"),required:!0,hint:q("admin.exams.form.charCount",{count:String(f.watch("code")?.length||0),max:String(100)}),children:e(L,{form:f,hasError:A("code"),placeholder:M?.codePlaceholder??q("admin.exams.form.codePlaceholder"),onManualEdit:e=>{D.current=e.length>0}})}),e(P,{label:q("admin.exams.form.totalPoints"),error:H("totalPoints"),required:!0,children:e(l,{name:"totalPoints",control:f.control,render:({field:r})=>e(h,{id:"totalPoints",type:"text",inputMode:"numeric",placeholder:"100",value:r.value,onBlur:r.onBlur,onChange:e=>r.onChange(e.target.value.replace(/[^0-9]/g,"")),className:A("totalPoints")?"border-red-500 focus:ring-red-500":""})})}),e(P,{label:q("admin.exams.form.level"),error:H("level"),children:e(l,{name:"level",control:f.control,render:({field:a})=>r(p,{value:a.value,onValueChange:a.onChange,disabled:w,children:[e(b,{className:A("level")?"border-red-500 focus:ring-red-500":"",children:e(N,{placeholder:q(w?"admin.exams.form.loading":"admin.exams.form.levelPlaceholder")})}),e(g,{children:w?e("div",{className:"flex items-center justify-center p-4",children:e(t,{className:"h-4 w-4 animate-spin text-gray-400"})}):S.map(r=>e(x,{value:r.id,children:r.name},r.id))})]})})}),e(P,{label:q("admin.exams.form.difficulty"),error:H("difficulty"),children:e(l,{name:"difficulty",control:f.control,render:({field:a})=>r(p,{value:a.value,onValueChange:a.onChange,children:[e(b,{className:A("difficulty")?"border-red-500 focus:ring-red-500":"",children:e(N,{placeholder:q("admin.exams.form.difficultyPlaceholder")})}),e(g,{children:C.map(r=>e(x,{value:r,children:E(r,q)},r))})]})})}),e(P,{label:M?.durationLabel??q("admin.exams.form.duration"),error:H("duration"),children:e(h,{id:"duration",type:"number",inputMode:"numeric",min:5,max:300,placeholder:"45",className:A("duration")?"border-red-500 focus:ring-red-500":"",...f.register("duration")})}),e(P,{className:"md:col-span-2",label:M?.descriptionLabel??q("admin.exams.form.description"),error:H("description"),hint:q("admin.exams.form.charCount",{count:String(f.watch("description")?.length||0),max:String(2e3)}),children:e(v,{id:"description",rows:3,maxLength:2e3,placeholder:M?.descriptionPlaceholder??q("admin.exams.form.descriptionPlaceholder"),className:A("description")?"border-red-500 focus:ring-red-500":"",...f.register("description")})})]})})]})}function L({form:r,hasError:a,placeholder:n,onManualEdit:o}){const{onChange:l,...i}=r.register("code");return e(h,{id:"code",maxLength:100,className:a?"border-red-500 focus:ring-red-500":"",placeholder:n,...i,onChange:e=>{l(e),o(e.target.value)}})}function P({label:a,error:n,hint:o,required:l,className:i,children:t}){return r("div",{className:i,children:[r(f,{children:[a," ",l&&e("span",{className:"text-red-500",children:"*"})]}),e("div",{className:"mt-1",children:t}),n&&e("p",{className:"mt-1 text-sm text-red-600",children:n}),o&&e("p",{className:"mt-1 text-xs text-gray-500",children:o})]})}function E(e,r){switch(e){case"EASY":return r("admin.exams.form.easy");case"MEDIUM":return r("admin.exams.form.medium");case"HARD":return r("admin.exams.form.hard");default:return e}}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { UserRole } from '../../shared/types/common.types';
|
|
2
2
|
import type { CreateExamSdkApi } from '../../api/exam-create';
|
|
3
|
+
import type { CreateExamTexts } from './create-exam.texts';
|
|
3
4
|
export interface CreateExamPageContainerProps {
|
|
4
5
|
role?: UserRole.ADMIN | UserRole.TEACHER;
|
|
5
6
|
api: CreateExamSdkApi;
|
|
6
7
|
listPath?: string;
|
|
7
8
|
onNavigate?: (href: string) => void;
|
|
9
|
+
/** Optional label overrides (e.g. to reuse this flow for homework). */
|
|
10
|
+
texts?: CreateExamTexts;
|
|
8
11
|
}
|
|
9
12
|
export declare function CreateExamPageContainer(props: CreateExamPageContainerProps): import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as
|
|
1
|
+
"use client";import{jsx as e,jsxs as t,Fragment as a}from"react/jsx-runtime";import{Suspense as n,useEffect as i,useState as r}from"react";import{zodResolver as s}from"@hookform/resolvers/zod";import{useForm as l}from"react-hook-form";import{ArrowLeft as o,ArrowRight as m,LayoutTemplate as c,Loader2 as d,X as p}from"lucide-react";import{Button as h}from"../../components/ui/button";import{Card as u,CardContent as x,CardHeader as f,CardTitle as g}from"../../components/ui/card";import{Dialog as b,DialogContent as v,DialogDescription as N,DialogHeader as y,DialogTitle as w}from"../../components/ui/dialog";import{useToast as C}from"../../shared/lib/hooks/useToast";import{useT as T}from"../../shared/lib/i18n";import{buildExamTemplatePayload as E}from"../../api/exam-create";import{CreateExamInfoForm as k}from"./CreateExamInfoForm";import{TemplateSelector as D}from"./TemplateSelector";import{examFormSchema as L}from"./create-exam.validation";function I({api:n,listPath:c="/manage/exams",onNavigate:p,texts:u}){const x=T(),{success:f,warning:g,error:I}=C(),[S,M]=r(!1),[z,P]=r(null),[q,O]=r([]),[F,H]=r(!1),[R,U]=r(!1),V=l({resolver:s(L),mode:"onBlur",defaultValues:{code:"",name:"",level:"",difficulty:"",duration:"",totalPoints:"100",description:""}});i(()=>{if(!n.getExamLevels)return;let e=!1;return H(!0),n.getExamLevels().then(t=>{e||O(t)}).catch(e=>I({message:x("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})).finally(()=>{e||H(!1)}),()=>{e=!0}},[n,I,x]);const Y=e=>{p?p(e):"undefined"!=typeof window&&(window.location.href=e)},$=V.handleSubmit(async e=>{if(z){U(!0);try{const a=function(e){return"object"==typeof e&&null!==e&&"data"in e}(t=await n.createExam({name:e.name,code:e.code||void 0,examLevel:e.level||void 0,difficulty:A(e.difficulty),totalScore:Number.parseInt(e.totalPoints,10),durationMinutes:e.duration?Number.parseInt(e.duration,10):void 0,description:e.description||void 0,isComplete:!1,isActive:!1,examTemplateId:z.id}))?t.data:t;await n.updateExamTemplate({examId:a.id,template:E(z)}),f({message:x("admin.exams.create.success"),description:u?.successDescription?.replace("{name}",a.name)??x("admin.exams.create.successDesc",{name:a.name})}),Y(`${c}/${a.id}/questions`)}catch(e){I({message:x("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})}finally{U(!1)}var t}else g({message:x("admin.exams.create.incompleteInfo"),description:u?.missingTemplateDescription??"Vui lòng chọn template đề thi trước khi tiếp tục."})},()=>g({message:x("admin.exams.create.incompleteInfo"),description:x("admin.exams.create.incompleteInfoDesc")}));return t("div",{className:"space-y-6",children:[t("div",{className:"flex items-center gap-4",children:[e(h,{variant:"ghost",size:"icon",onClick:()=>Y(c),className:"h-8 w-8",children:e(o,{className:"h-4 w-4"})}),t("div",{children:[e("h1",{className:"text-3xl font-bold tracking-tight text-gray-900",children:u?.title??x("admin.exams.create.title")}),e("p",{className:"mt-1 text-gray-600",children:u?.step1??x("admin.exams.create.step1")})]})]}),e("form",{id:"exam-form",onSubmit:$,children:e(k,{form:V,examLevels:q,isLoadingExamLevels:F,texts:u})}),e(j,{selectedTemplate:z,onOpen:()=>M(!0),onClear:()=>P(null),texts:u}),e(b,{open:S,onOpenChange:M,children:t(v,{className:"flex max-h-[92vh] min-h-[92vh] max-w-5xl flex-col overflow-hidden",children:[t(y,{children:[e(w,{children:u?.templateDialogTitle??"Chọn template đề thi"}),e(N,{children:u?.templateDialogDescription??"Chọn loại đề thi bạn muốn tạo."})]}),e(D,{api:n,onTemplateSelect:e=>{P(e),M(!1)},onCancel:()=>M(!1)})]})}),t("div",{className:"flex justify-end gap-3",children:[e(h,{variant:"outline",onClick:()=>Y(c),disabled:R,children:u?.cancelLabel??x("admin.exams.create.cancel")}),e(h,{type:"submit",form:"exam-form",className:"bg-blue-600 hover:bg-blue-700",disabled:R||!z,children:t(a,R?{children:[e(d,{className:"mr-2 h-4 w-4 animate-spin"}),u?.savingLabel??x("admin.exams.create.saving")]}:{children:[u?.saveAndContinueLabel??x("admin.exams.create.saveAndContinue"),e(m,{className:"ml-2 h-4 w-4"})]})})]})]})}export function CreateExamPageContainer(t){return e(n,{fallback:e(S,{}),children:e(I,{...t})})}function j({selectedTemplate:a,onOpen:n,onClear:i,texts:r}){return t(u,{className:"border border-gray-200",children:[e(f,{className:"border-b border-gray-100 bg-gray-50/50",children:t(g,{className:"flex items-center gap-2 text-base font-semibold",children:[e(c,{className:"h-4 w-4 text-blue-600"}),r?.templateSectionTitle??"Chọn Template"," ",e("span",{className:"text-red-500",children:"*"})]})}),e(x,{className:"pt-6",children:t("div",a?{className:"flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-green-100",children:e(c,{className:"h-4 w-4 text-green-600"})}),t("div",{children:[e("p",{className:"text-sm font-medium text-green-800",children:a.name}),t("p",{className:"text-xs text-green-600",children:[a.totalQuestions," câu hỏi • ",a.parts.length," phần"]})]})]}),t("div",{className:"flex items-center gap-2",children:[e(h,{type:"button",variant:"outline",size:"sm",onClick:n,className:"border-green-300 text-green-700 hover:bg-green-100",children:r?.changeTemplateLabel??"Đổi template"}),e(h,{type:"button",variant:"ghost",size:"icon",onClick:i,className:"h-7 w-7 text-green-600 hover:bg-green-100 hover:text-green-800",children:e(p,{className:"h-3.5 w-3.5"})})]})]}:{className:"flex flex-col items-center gap-3 rounded-lg border-2 border-dashed border-gray-300 py-8",children:[e(c,{className:"h-8 w-8 text-gray-400"}),e("p",{className:"text-sm text-gray-500",children:r?.noTemplateText??"Chưa chọn template đề thi"}),t(h,{type:"button",variant:"outline",onClick:n,children:[e(c,{className:"mr-2 h-4 w-4"}),r?.selectTemplateLabel??"Chọn template"]})]})})]})}function S(){return e("div",{className:"flex h-screen items-center justify-center",children:e(d,{className:"h-8 w-8 animate-spin text-blue-600"})})}function A(e){return"EASY"===e?"EASY":"MEDIUM"===e?"MEDIUM":"HARD"===e?"HARD":void 0}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type ExistingQuestionBank } from '../questions/components/QuestionBankOption';
|
|
1
2
|
import type { ExamData, ExamQuestion, ExamTemplate } from '../../shared/types/entities/exam.types';
|
|
2
|
-
import type { SaveQuestionInput } from '../../api/exam-questions.types';
|
|
3
|
+
import type { SaveQuestionInput, SearchQuestionBankInput, SearchQuestionBankResult } from '../../api/exam-questions.types';
|
|
3
4
|
export interface ExamCreatorProps {
|
|
4
5
|
template: ExamTemplate;
|
|
5
6
|
examId?: string;
|
|
@@ -7,10 +8,15 @@ export interface ExamCreatorProps {
|
|
|
7
8
|
examLevel?: string;
|
|
8
9
|
subjectId?: string;
|
|
9
10
|
initialQuestions?: Partial<ExamQuestion>[];
|
|
10
|
-
onSaveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> |
|
|
11
|
+
onSaveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | {
|
|
12
|
+
question: Partial<ExamQuestion>;
|
|
13
|
+
questionBank?: ExistingQuestionBank | null;
|
|
14
|
+
} | void>;
|
|
11
15
|
onSave?: (examData: ExamData) => void | Promise<void>;
|
|
12
16
|
onCancel?: () => void;
|
|
13
17
|
onPartAudioChange?: (partId: string, audioUrl: string) => void | Promise<void>;
|
|
14
18
|
partAudioOverrides?: Record<string, string>;
|
|
19
|
+
searchQuestions?: (input: SearchQuestionBankInput) => Promise<SearchQuestionBankResult>;
|
|
20
|
+
saveExamRef?: React.MutableRefObject<(() => void) | null>;
|
|
15
21
|
}
|
|
16
|
-
export declare function ExamCreator(
|
|
22
|
+
export declare function ExamCreator(props: ExamCreatorProps): import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useMemo as r,useRef as o,useState as i}from"react";import{ChevronLeft as a,ChevronRight as s,Save as n}from"lucide-react";import{Button as l}from"../../components/ui/button";import{QuestionCreator as d}from"../questions/creator/QuestionCreator";import{useToast as u}from"../../shared/lib/hooks/useToast";import{useT as m}from"../../shared/lib/i18n";import{ExamPartTabs as c}from"./ExamPartTabs";import{ExamQuestionGrid as p}from"./ExamQuestionGrid";import{PartAudioUploader as f}from"./PartAudioUploader";import{getEffectiveQuestionConfig as g,getPartByQuestionIndex as x,normalizeTemplateGroupConfig as h}from"./exam-template.utils";export function ExamCreator({template:b,examId:v,examDifficulty:w,examLevel:C,subjectId:y,initialQuestions:I=[],onSaveQuestion:N,onSave:S,onCancel:Q,onPartAudioChange:k,partAudioOverrides:q}){const A=m(),{success:E,warning:T,error:j}=u(),O=o(null),[D,M]=i(0),[P,L]=i(()=>Object.fromEntries(I.map(e=>[(e.questionNumber??1)-1,e]))),[_,U]=i(()=>new Set(I.map(e=>(e.questionNumber??1)-1))),[V,z]=i({}),[B,G]=i(!1),[R,$]=i(!1),H=o({}),F=x(b,D),J=F?h(F):void 0,K=F?g(F,D):void 0,W=!(!J?.groups||K?.groups),X=K&&"sectionName"in K?K.sectionName:void 0,Y=X&&!W?`${F?.id}_${X}`:F?.id,Z=P[D],ee=r(()=>({examId:v,templateId:b.id,questions:Object.values(P),currentQuestionIndex:D,lastSaved:(new Date).toISOString(),createdAt:(new Date).toISOString(),updatedAt:(new Date).toISOString(),isCompleted:_.size>=b.totalQuestions}),[_.size,D,v,P,b.id,b.totalQuestions]),te=e=>{R&&T({message:A("common.examCreator.saveBeforeExit")}),M(Math.max(0,Math.min(b.totalQuestions-1,e))),$(!1),H.current={}},re=async e=>{if(!F)return;if(!1===await(O.current?.triggerValidation?.()))return;const t={...Z,...H.current,...e,type:e?.type??H.current.type??Z?.type??K?.questionType,partId:F.id,questionNumber:D+1,difficulty:e?.difficulty??w??Z?.difficulty,skill:F.skill,isCompleted:!0};G(!0);try{const e=await(N?.({question:t,questionIndex:D,template:b,examId:v,examDifficulty:w,examLevel:C,subjectId:y}))||t;L(t=>({...t,[D]:e})),U(e=>new Set(e).add(D)),$(!1),E({message:A("common.examCreator.saveSuccess",{index:String(D+1)})})}catch(e){j({message:A("common.examCreator.saveError",{index:String(D+1)}),description:e instanceof Error?e.message:void 0})}finally{G(!1)}};return e("div",{className:"flex min-h-0 flex-1 flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50/30",children:t("div",{className:"flex min-h-0 flex-1 overflow-hidden",children:[e("div",{className:"w-80 overflow-y-auto border-r border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(c,{template:b,currentPartId:F?.id??"",completedQuestions:_,onPartChange:e=>te(b.parts.find(t=>t.id===e)?.startIndex??0)})}),t("div",{className:"flex flex-1 flex-col overflow-hidden",children:[e("div",{className:"min-h-0 flex-1 overflow-y-auto p-4 pb-4",children:t("div",{className:"mx-auto max-w-4xl",children:["LISTENING"===F?.skill&&e(f,{partId:F.id,partName:F.name,audioUrl:q?.[F.id]??F.audioUrl??"",examId:v,onAudioChange:e=>{k?.(F.id,e)},isReadOnly:!k}),e(d,{initialData:Z,prefilledType:K?.questionType,prefilledSkill:F?.skill,optionCount:K?.optionCount,optionMode:K?.optionMode,defaultValue:K?.defaultValue,defaultQuestion:K?.defaultQuestion,viewMode:K?.viewMode,groups:K?.groups||J?.groups,groupConfig:K?.groupConfig||J?.groupConfig,groupType:K?.groupType||J?.groupType,articleCount:K?.articleCount??F?.articleCount,questionConfig:K?.questionConfig||F?.questionConfig,questionIndexInPart:W?D-(F?.startIndex??0):D-(K?.startIndex??F?.startIndex??0),partId:Y,onChange:e=>{H.current=e,$(!0)},onSave:re,onCancel:Q,isEmbedded:!0,externalErrors:V[D],validationRef:O,suggestedAnswers:K?.suggestedAnswers||F?.suggestedAnswers,hasSuggestedAnswers:K?.hasSuggestedAnswers??F?.hasSuggestedAnswers},D)]})}),e("div",{className:"flex-shrink-0 border-t border-gray-200/80 bg-white/95 shadow-[0_-4px_20px_rgba(0,0,0,0.08)] backdrop-blur-sm",children:t("div",{className:"mx-auto flex max-w-4xl items-center justify-between px-6 py-4",children:[t(l,{variant:"outline",onClick:()=>te(D-1),disabled:0===D,children:[e(a,{className:"mr-2 h-4 w-4"}),"Câu trước"]}),t(l,{onClick:()=>{re()},disabled:B,className:"bg-green-600 hover:bg-green-700",children:[e(n,{className:"mr-2 h-4 w-4"}),B?"Đang lưu...":"Lưu câu hỏi"]}),t(l,{variant:"outline",onClick:()=>te(D+1),disabled:D>=b.totalQuestions-1,children:["Câu tiếp",e(s,{className:"ml-2 h-4 w-4"})]})]})})]}),t("div",{className:"w-80 overflow-y-auto border-l border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:[e(p,{template:b,currentQuestionIndex:D,completedQuestions:_,onQuestionChange:te}),t(l,{onClick:()=>{(async()=>{R?T({message:A("common.examCreator.saveBeforeExit")}):await(S?.(ee))})()},className:"mt-4 w-full bg-blue-600 hover:bg-blue-700",children:[e(n,{className:"mr-2 h-4 w-4"}),"Hoàn tất đề thi"]})]})]})})}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t,Fragment as n}from"react/jsx-runtime";import{useCallback as o,useEffect as i,useMemo as r,useRef as a,useState as s}from"react";import{CheckCircle as d,ChevronLeft as l,ChevronRight as u,Loader2 as c,Save as m}from"lucide-react";import{Button as p}from"../../components/ui/button";import{QuestionBankOption as g}from"../questions/components/QuestionBankOption";import{QuestionSearchDropdown as f}from"../questions/components/QuestionSearchDropdown";import{QuestionCreator as h}from"../questions/creator/QuestionCreator";import{useToast as x}from"../../shared/lib/hooks/useToast";import{useT as b}from"../../shared/lib/i18n";import{useExamQuestionStore as v}from"../../shared/lib/stores/examQuestionStore";import{transformApiQuestionToFrontend as C}from"../../shared/lib/utils/question-reverse-transform";import{ExamPartTabs as w}from"./ExamPartTabs";import{ExamQuestionGrid as y}from"./ExamQuestionGrid";import{PartAudioUploader as N}from"./PartAudioUploader";import{CompletionDialog as I}from"./CompletionDialog";import{UnsavedChangesDialog as k}from"./UnsavedChangesDialog";import{clearQuestionStatuses as q,getQuestionStatus as Q,QuestionStatus as S,setQuestionStatus as E}from"./exam-question-status";import{getEffectiveQuestionConfig as D,getPartByQuestionIndex as T,normalizeTemplateGroupConfig as A}from"./exam-template.utils";export function ExamCreator(L){const{template:B,examId:V=`exam-${Date.now()}`,initialQuestions:j=[]}=L,U=b(),{success:P,warning:$,error:M}=x(),R=a(null),z=a({}),F=a(!1),G=a(!1),H=a(!1),K=v(e=>e.initialized),W=v(e=>e.examId),J=v(e=>e.initializeExam),X=v(e=>e.resetStore),Y=v(e=>e.currentQuestionIndex),Z=v(e=>e.setCurrentQuestionIndex),ee=v(e=>e.questions),te=v(e=>e.upsertQuestionData),ne=v(e=>e.clearQuestionData),oe=v(e=>e.markQuestionCompleted),ie=v(e=>e.getQuestionData),re=v(e=>e.setSharedHintLetters),[ae,se]=s({}),[de,le]=s(!1),[ue,ce]=s(!1),[me,pe]=s(!1),[ge,fe]=s(null),[he,xe]=s(!1),[be,ve]=s(!1),[Ce,we]=s({}),[ye,Ne]=s({}),[Ie,ke]=s({}),[qe,Qe]=s(),[Se,Ee]=s(),De=T(B,Y),Te=De?.id??"",Ae=De?D(De,Y):void 0,Oe=De?A(De):void 0,_e=Boolean(Oe?.groups&&!Ae?.groups),Le=_(Ae),Be=Le&&!_e?`${Te}_${Le}`:Te,Ve=ee[Y]?.data,je=Ce[Y]??{checked:!1,code:"",title:""},Ue=r(()=>{const e=new Set;return Object.entries(ee).forEach(([t,n])=>{n.isCompleted&&e.add(Number(t))}),e},[ee]),Pe=r(()=>({id:V,templateId:B.id,questions:Object.values(ee).map(e=>e.data),currentQuestionIndex:Y,lastSaved:(new Date).toISOString(),createdAt:(new Date).toISOString(),updatedAt:(new Date).toISOString(),isCompleted:Ue.size>=B.totalQuestions}),[Ue.size,Y,V,ee,B.id,B.totalQuestions]);i(()=>{const e=W&&W!==V;e&&X(),K&&!e||J({examId:V,templateId:B.id,totalQuestions:B.totalQuestions})},[V,J,K,X,W,B.id,B.totalQuestions]),i(()=>{if(!K||0===j.length)return;F.current=!0,j.forEach(e=>{const t=(e.questionNumber??1)-1,n={...e,isCompleted:!0};te(t,n),oe(t,!0),z.current[t]=n,function(e,t,n,o){if("LOOK_PICTURE_FILL_WORD_HINT"!==t.type||!t.partId)return;const i=t.answer,r=t.questionGroup?.groupNumber;if(!i?.hintLetters?.length||!r)return;const a=T(n,e),s=a?D(a,e):void 0,d=_(s),l=d?`${t.partId}_${d}`:t.partId;o(l,r,i.hintLetters)}(t,n,B,re);const o=e.questionBank;o?.id&&Ne(e=>({...e,[t]:o}))});const e=window.setTimeout(()=>{F.current=!1},1e3);return()=>window.clearTimeout(e)},[j,K,oe,re,B,te]),i(()=>{pe(Q(V,Y+1)===S.UNSAVED)},[Y,V]),i(()=>{ce(Object.values(ee).some(e=>Object.keys(e.data).length>0&&!e.isCompleted))},[ee]),i(()=>{const e=e=>{ue&&(e.preventDefault(),e.returnValue="")};return window.addEventListener("beforeunload",e),()=>window.removeEventListener("beforeunload",e)},[ue]);const $e=o(e=>{H.current=!0,pe(!1),Z(Math.max(0,Math.min(B.totalQuestions-1,e))),window.setTimeout(()=>{H.current=!1},500)},[Z,B.totalQuestions]),Me=e=>{if(me)return fe(e),void xe(!0);$e(e)},Re=o(e=>{F.current||G.current||H.current||(te(Y,{questionNumber:Y+1,partId:Te,...e}),E(V,Y+1,S.UNSAVED),pe(!0))},[Te,Y,V,te]),ze=async e=>{if(!De)return;if(!function(e,t,n,o){if(!e.checked||t?.id)return!0;const i=e.code.trim()?void 0:"Vui lòng nhập mã câu hỏi cho ngân hàng",r=e.title.trim()?void 0:"Vui lòng nhập tiêu đề câu hỏi cho ngân hàng";return n(i),o(r),!i&&!r}(je,ye[Y],Qe,Ee))return;if(!1===await(R.current?.triggerValidation?.()))return;const t=R.current?.getFormData?.(),n=function(e,t,n,o,i,r,a){return{...n,...e,...t?{answer:t}:{},type:e?.type??n?.type??o,id:n?.id??e?.id,partId:i.id,questionNumber:r+1,difficulty:e?.difficulty??a??n?.difficulty,skill:i.skill,isCompleted:!0}}(e,t,ie(Y),Ae?.questionType,De,Y,L.examDifficulty);le(!0);try{const e=await(L.onSaveQuestion?.({question:n,questionIndex:Y,template:B,examId:V,examDifficulty:L.examDifficulty,examLevel:L.examLevel,subjectId:L.subjectId,sectionId:(o=Ae,o&&"sectionId"in o?o.sectionId:void 0),addToQuestionBank:!ye[Y]?.id&&je.checked,questionBankCode:je.checked?je.code:void 0,questionBankTitle:je.checked?je.title:void 0,existingQuestionBankId:ye[Y]?.id})),t=function(e,t){return e&&"question"in e?{...t,...e.question,isCompleted:!0}:{...t,...e??{},isCompleted:!0}}(e,n);te(Y,t),oe(Y,!0),z.current[Y]=t,function(e,t,n){const o=e&&"questionBank"in e?e.questionBank:e?.questionBank;o?.id&&n(e=>({...e,[t]:o}))}(e,Y,Ne),G.current=!0,window.setTimeout(()=>{G.current=!1},500),E(V,Y+1,S.SAVED),pe(!1),P({message:U("common.examCreator.saveSuccess",{index:String(Y+1)})})}catch(e){M({message:U("common.examCreator.saveError",{index:String(Y+1)}),description:e instanceof Error?e.message:void 0})}finally{le(!1)}var o};return L.saveExamRef&&(L.saveExamRef.current=()=>{me?$({message:U("common.examCreator.saveBeforeExit")}):ve(!0)}),t("div",{className:"flex min-h-0 flex-1 flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50/30",children:[t("div",{className:"flex min-h-0 flex-1 overflow-hidden",children:[e("div",{className:"w-80 overflow-y-auto border-r border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(w,{template:B,currentPartId:Te,completedQuestions:Ue,onPartChange:e=>Me(B.parts.find(t=>t.id===e)?.startIndex??0)})}),t("div",{className:"flex flex-1 flex-col overflow-hidden",children:[e("div",{className:"min-h-0 flex-1 overflow-y-auto p-4 pb-4",children:t("div",{className:"mx-auto max-w-4xl",children:["LISTENING"===De?.skill&&e(N,{partId:De.id,partName:De.name,audioUrl:L.partAudioOverrides?.[De.id]??De.audioUrl??"",examId:V,onAudioChange:e=>{L.onPartAudioChange?.(De.id,e)},isReadOnly:!L.onPartAudioChange}),e(h,{initialData:Ve,prefilledType:Ae?.questionType,prefilledSkill:De?.skill,optionCount:Ae?.optionCount,optionMode:Ae?.optionMode,defaultValue:Ae?.defaultValue,defaultQuestion:Ae?.defaultQuestion,viewMode:Ae?.viewMode,groups:Ae?.groups||Oe?.groups,groupConfig:Ae?.groupConfig||Oe?.groupConfig,groupType:Ae?.groupType||Oe?.groupType,articleCount:Ae?.articleCount??De?.articleCount,questionConfig:Ae?.questionConfig||De?.questionConfig,questionIndexInPart:_e?Y-(De?.startIndex??0):Y-(Ae?.startIndex??De?.startIndex??0),partId:Be,onChange:Re,onUnsavedChangesChange:pe,onSave:ze,onCancel:L.onCancel,isEmbedded:!0,externalErrors:ae[Y],validationRef:R,suggestedAnswers:Ae?.suggestedAnswers||De?.suggestedAnswers,hasSuggestedAnswers:Ae?.hasSuggestedAnswers??De?.hasSuggestedAnswers,headerExtra:e(f,{questionType:Ae?.questionType||De?.questionType,onSelect:e=>{const t={...C({question_type:e.type,content:e.content,correct_answer:e.answer,points:e.points,difficult_level:e.difficulty,question_skill_type:e.skill,explanation:e.explanation}),id:void 0,partId:Te,questionNumber:Y+1,isCompleted:!1};te(Y,t);const n=e.id,o=e.code,i=e.title;(o||i)&&ke(e=>({...e,[Y]:{code:o,title:i}})),n&&(o||i)&&Ne(e=>({...e,[Y]:{id:n,code:o??"",title:i??""}})),E(V,Y+1,S.UNSAVED),pe(!0),oe(Y,!1),P({message:U("common.examCreator.bankSelected")})},selectedQuestionLabel:Ie[Y]?`${Ie[Y].code?`#${Ie[Y].code} - `:""}${Ie[Y].title||""}`:void 0,disabled:!(Ae?.questionType||De?.questionType),compact:!0,searchQuestions:L.searchQuestions})},Y),e("div",{className:"mt-8",children:e(g,{checked:je.checked,onCheckedChange:e=>O(Y,{checked:e},we,pe,V),code:je.code,onCodeChange:e=>O(Y,{code:e},we,pe,V),title:je.title,onTitleChange:e=>O(Y,{title:e},we,pe,V),codeError:qe,error:Se,existingBank:ye[Y]||null})})]})}),e("div",{className:"flex-shrink-0 border-t border-gray-200/80 bg-white/95 shadow-[0_-4px_20px_rgba(0,0,0,0.08)] backdrop-blur-sm",children:t("div",{className:"mx-auto flex max-w-4xl items-center justify-between px-6 py-4",children:[t(p,{variant:"outline",onClick:()=>Me(Y-1),disabled:0===Y,className:"gap-2 border-gray-300 hover:bg-gray-50",children:[e(l,{className:"h-4 w-4"}),"Câu trước"]}),t("div",{className:"flex items-center gap-4",children:[me?t("div",{className:"flex items-center gap-2 rounded-full bg-amber-50 px-3 py-1.5 ring-1 ring-amber-200",children:[e("span",{className:"h-2 w-2 animate-pulse rounded-full bg-amber-500"}),e("span",{className:"text-xs font-medium text-amber-700",children:"Chưa lưu"})]}):ee[Y]?.isCompleted?t("div",{className:"flex items-center gap-2 rounded-full bg-green-50 px-3 py-1.5 ring-1 ring-green-200",children:[e(d,{className:"h-3.5 w-3.5 text-green-500"}),e("span",{className:"text-xs font-medium text-green-700",children:"Đã lưu"})]}):null,t("div",{className:"flex items-center gap-2 rounded-full bg-gray-100 px-4 py-2",children:[t("span",{className:"text-sm font-bold text-gray-900",children:["Câu ",Y+1]}),e("span",{className:"text-sm text-gray-400",children:"/"}),e("span",{className:"text-sm text-gray-500",children:B.totalQuestions})]}),e(p,{onClick:()=>{ze(ie(Y))},disabled:de||!me,className:"gap-2 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 shadow-md hover:shadow-lg transition-all",children:t(n,de?{children:[e(c,{className:"h-4 w-4 animate-spin"}),"Đang lưu..."]}:{children:[e(m,{className:"h-4 w-4"}),"Lưu câu hỏi"]})})]}),t(p,{variant:"outline",onClick:()=>Me(Y+1),disabled:Y>=B.totalQuestions-1||!ee[Y]?.isCompleted,className:"gap-2 border-gray-300 hover:bg-gray-50",children:["Câu tiếp theo",e(u,{className:"h-4 w-4"})]})]})})]}),e("div",{className:"w-80 overflow-y-auto border-l border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(y,{template:B,currentQuestionIndex:Y,completedQuestions:Ue,onQuestionChange:Me})})]}),e(I,{open:be,onOpenChange:ve,completedCount:Ue.size,totalQuestions:B.totalQuestions,onConfirm:()=>{ve(!1),q(V),X(),L.onSave?.(Pe)}}),e(k,{open:he,onOpenChange:xe,onConfirm:()=>{const e=z.current[Y];e?(te(Y,e),E(V,Y+1,S.SAVED)):ne(Y),pe(!1),null!==ge&&$e(ge),fe(null),xe(!1)},onCancel:()=>xe(!1),questionNumber:Y+1})]})}function O(e,t,n,o,i){n(n=>{const o=n[e]??{checked:!1,code:"",title:""};return{...n,[e]:{...o,...t}}}),o(!0),E(i,e+1,S.UNSAVED)}function _(e){return e&&"sectionName"in e?e.sectionName:void 0}
|
|
@@ -5,5 +5,5 @@ interface ExamPartTabsProps {
|
|
|
5
5
|
completedQuestions: Set<number>;
|
|
6
6
|
onPartChange: (partId: string) => void;
|
|
7
7
|
}
|
|
8
|
-
export declare function ExamPartTabs({ template, currentPartId, completedQuestions, onPartChange }: ExamPartTabsProps): import("react").JSX.Element;
|
|
8
|
+
export declare function ExamPartTabs({ template, currentPartId, completedQuestions, onPartChange, }: ExamPartTabsProps): import("react").JSX.Element;
|
|
9
9
|
export {};
|