@tinyweb_dev/oe-exam-sdk 0.2.4 → 0.2.6
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/components/exams/take/ExamTakingPageContainer.js +6 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +19 -3
- package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.d.ts +10 -0
- package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.js +38 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -11
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.d.ts +22 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +60 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.js +75 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.d.ts +13 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +31 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.js +92 -0
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +2 -1
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +2 -2
- package/dist/components/exams/take/components/question-renderers/index.d.ts +4 -0
- package/dist/components/exams/take/components/question-renderers/index.js +4 -0
- package/dist/components/exams/take/exam-taking.utils.d.ts +1 -0
- package/dist/components/exams/take/exam-taking.utils.js +9 -1
- package/dist/components/exams/take/types.d.ts +3 -2
- package/dist/components/exams/take/utils/answer-transformers.js +28 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +73 -1
- package/dist/components/exams/take/utils/question-transformers.js +188 -0
- package/dist/components/questions/_shared/config/question-types.config.js +24 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +9 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +49 -1
- package/dist/components/questions/_shared/types/fill-in-blank-group.type.d.ts +50 -0
- package/dist/components/questions/_shared/types/fill-in-blank-group.type.js +2 -0
- package/dist/components/questions/_shared/types/index.d.ts +4 -0
- package/dist/components/questions/_shared/types/index.js +4 -0
- package/dist/components/questions/_shared/types/match-word-to-picture.type.d.ts +62 -0
- package/dist/components/questions/_shared/types/match-word-to-picture.type.js +1 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +40 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.js +1 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +42 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.js +2 -0
- package/dist/components/questions/_shared/types/write-a-short-letter.type.d.ts +4 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +6 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
- package/dist/components/questions/creator/question-type-registry.js +8 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +13 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +32 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.d.ts +1 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +42 -6
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +40 -20
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.d.ts +1 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +5 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +64 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +158 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.d.ts +7 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +30 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +20 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +99 -0
- package/dist/components/questions/types/fill-in-blank-group/index.d.ts +5 -0
- package/dist/components/questions/types/fill-in-blank-group/index.js +5 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.d.ts +4 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +102 -0
- package/dist/components/questions/types/fill-in-blank-group/register.d.ts +3 -0
- package/dist/components/questions/types/fill-in-blank-group/register.js +37 -0
- package/dist/components/questions/types/fill-in-blank-group/transform.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/transform.js +68 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +31 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.js +267 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +65 -0
- package/dist/components/questions/types/match-word-to-picture/WordBankEditor.d.ts +8 -0
- package/dist/components/questions/types/match-word-to-picture/WordBankEditor.js +15 -0
- package/dist/components/questions/types/match-word-to-picture/index.d.ts +6 -0
- package/dist/components/questions/types/match-word-to-picture/index.js +6 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.js +53 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.d.ts +4 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.js +75 -0
- package/dist/components/questions/types/match-word-to-picture/register.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/register.js +55 -0
- package/dist/components/questions/types/match-word-to-picture/transform.d.ts +11 -0
- package/dist/components/questions/types/match-word-to-picture/transform.js +43 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts +20 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +109 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +30 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +180 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +6 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +26 -0
- package/dist/components/questions/types/matching-with-lines-group/index.d.ts +4 -0
- package/dist/components/questions/types/matching-with-lines-group/index.js +4 -0
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.d.ts +8 -0
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +104 -0
- package/dist/components/questions/types/matching-with-lines-group/register.d.ts +3 -0
- package/dist/components/questions/types/matching-with-lines-group/register.js +37 -0
- package/dist/components/questions/types/matching-with-lines-group/transform.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/transform.js +44 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +25 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.js +144 -0
- package/dist/components/questions/types/true-false-correct-group/index.d.ts +5 -0
- package/dist/components/questions/types/true-false-correct-group/index.js +5 -0
- package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.d.ts +4 -0
- package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.js +79 -0
- package/dist/components/questions/types/true-false-correct-group/register.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/register.js +36 -0
- package/dist/components/questions/types/true-false-correct-group/transform.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/transform.js +49 -0
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +11 -1
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +8 -2
- package/dist/components/questions/types/write-a-short-letter/register.js +4 -0
- package/dist/components/questions/types/write-a-short-letter/transform.js +4 -0
- package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.d.ts +2 -1
- package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +2 -2
- package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +10 -0
- package/dist/components/questions/viewer/FillInBlankGroupViewer.js +14 -0
- package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.d.ts +9 -0
- package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +11 -0
- package/dist/components/questions/viewer/MatchWordToPictureViewer.d.ts +9 -0
- package/dist/components/questions/viewer/MatchWordToPictureViewer.js +8 -0
- package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.d.ts +7 -0
- package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.js +48 -0
- package/dist/components/questions/viewer/QuestionViewer.js +24 -3
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +9 -0
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -0
- package/dist/components/questions/viewer/index.d.ts +5 -0
- package/dist/components/questions/viewer/index.js +5 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
- package/dist/components/results/renderers/review-question-body-movers.js +74 -3
- package/dist/components/results/review-data.js +59 -0
- package/dist/components/themes/english-certification/EcWritingTask.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +8 -0
- package/dist/shared/lib/hooks/usePresignedFileUrl.d.ts +3 -1
- package/dist/shared/lib/hooks/usePresignedFileUrl.js +30 -12
- package/dist/shared/lib/utils/question-reverse-transform.js +142 -0
- package/dist/shared/lib/utils/question-transform.js +8 -0
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
- package/dist/shared/types/questions/fill-in-blank-group.d.ts +45 -0
- package/dist/shared/types/questions/fill-in-blank-group.js +1 -0
- package/dist/shared/types/questions/index.d.ts +4 -0
- package/dist/shared/types/questions/index.js +8 -0
- package/dist/shared/types/questions/match-word-to-picture.d.ts +35 -0
- package/dist/shared/types/questions/match-word-to-picture.js +5 -0
- package/dist/shared/types/questions/matching-with-lines-group.d.ts +39 -0
- package/dist/shared/types/questions/matching-with-lines-group.js +1 -0
- package/dist/shared/types/questions/true-false-correct-group.d.ts +39 -0
- package/dist/shared/types/questions/true-false-correct-group.js +1 -0
- package/package.json +7 -2
|
@@ -631,6 +631,12 @@ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (a
|
|
|
631
631
|
map.set(`${question.id}-item-${index}`, question.id);
|
|
632
632
|
});
|
|
633
633
|
}
|
|
634
|
+
if (part.questionType === 'FILL_IN_BLANK_GROUP' &&
|
|
635
|
+
Array.isArray(content?.items)) {
|
|
636
|
+
content.items.forEach((_, index) => {
|
|
637
|
+
map.set(`${question.id}-item-${index}`, question.id);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
634
640
|
});
|
|
635
641
|
});
|
|
636
642
|
return map;
|
|
@@ -8,9 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* 2. Transforms data to renderer-specific props
|
|
9
9
|
* 3. Renders the correct component for each question type
|
|
10
10
|
*/
|
|
11
|
-
import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
|
|
11
|
+
import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
|
|
12
12
|
import { SpeakingQuestionRenderer } from './speaking/renderers';
|
|
13
|
-
import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, } from '../utils/question-transformers';
|
|
13
|
+
import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, } from '../utils/question-transformers';
|
|
14
14
|
import { transformCrossOutWordGroup } from '../utils/cross-out-word-group.transformer';
|
|
15
15
|
import { normalizeColoringAssignments } from '../utils/coloring-assignments';
|
|
16
16
|
export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode = false, onExit, onPartComplete, onPartBack, speakingTheme, initialQuestionIndex, skipTeacherVideo, onTeacherIntroPlayed, currentQuestionIndex, }) {
|
|
@@ -66,7 +66,7 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
66
66
|
const firstQuestion = questions[0];
|
|
67
67
|
const data = transformWriteShortLetter(firstQuestion);
|
|
68
68
|
const writingAnswer = answers[firstQuestion.id] || '';
|
|
69
|
-
return (_jsx(MoversWritingRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions, prompt: data.prompt, requirements: data.requirements, minWords: data.minWords, maxWords: data.maxWords, answer: writingAnswer, onAnswerChange: (value) => onAnswerChange(firstQuestion.id, value), isReviewMode: isReviewMode, guideImages: data.guideImages }));
|
|
69
|
+
return (_jsx(MoversWritingRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions, prompt: data.prompt, requirements: data.requirements, minWords: data.minWords, maxWords: data.maxWords, answer: writingAnswer, onAnswerChange: (value) => onAnswerChange(firstQuestion.id, value), isReviewMode: isReviewMode, guideImages: data.guideImages, passage: data.passage }));
|
|
70
70
|
}
|
|
71
71
|
case 'READ_AND_COLOR_OBJECTS': {
|
|
72
72
|
const data = transformColorObjects(questions);
|
|
@@ -157,6 +157,14 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
157
157
|
const data = transformTrueFalseGroup(questions);
|
|
158
158
|
return (_jsx(MoversTrueFalseRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
159
159
|
}
|
|
160
|
+
case 'TRUE_FALSE_CORRECT_GROUP': {
|
|
161
|
+
const data = transformTrueFalseCorrectGroup(questions);
|
|
162
|
+
return (_jsx(MoversTrueFalseCorrectGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
163
|
+
}
|
|
164
|
+
case 'FILL_IN_BLANK_GROUP': {
|
|
165
|
+
const data = transformFillInBlankGroup(questions);
|
|
166
|
+
return (_jsx(MoversFillInBlankGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: data.questions.filter((question) => !question.isExample).length, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl || part.audioUrl, imageUrl: data.imageUrl, showHints: data.showHints, hints: data.hints, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
167
|
+
}
|
|
160
168
|
case 'CROSS_OUT_WORD_GROUP': {
|
|
161
169
|
const data = transformCrossOutWordGroup(questions);
|
|
162
170
|
return (_jsx(MoversCrossOutWordGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
@@ -165,6 +173,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
165
173
|
const data = transformWordOrderAndMatchGroup(questions);
|
|
166
174
|
return (_jsx(MoversWordOrderAndMatchGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, matchOptions: data.matchOptions, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
167
175
|
}
|
|
176
|
+
case 'MATCHING_WITH_LINES_GROUP': {
|
|
177
|
+
const data = transformMatchingWithLinesGroup(questions);
|
|
178
|
+
return (_jsx(MoversMatchingWithLinesGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, data: data, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
179
|
+
}
|
|
168
180
|
case 'TRUE_FALSE': {
|
|
169
181
|
const data = transformTrueFalse(questions);
|
|
170
182
|
return (_jsx(MoversTrueFalseRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
@@ -190,6 +202,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
190
202
|
const data = transformMatchByWritingAnswer(questions);
|
|
191
203
|
return (_jsx(MoversMatchByWritingAnswerRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, audioUrl: part.audioUrl, options: data.options, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, viewMode: data.viewMode, groupImageUrl: data.groupImageUrl }));
|
|
192
204
|
}
|
|
205
|
+
case 'MATCH_WORD_TO_PICTURE': {
|
|
206
|
+
const data = transformMatchWordToPicture(questions);
|
|
207
|
+
return (_jsx(MoversMatchWordToPictureRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, wordBank: data.wordBank, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
208
|
+
}
|
|
193
209
|
case 'WORD_FILL_STRUCTURED_FORM': {
|
|
194
210
|
const data = transformWordFillStructuredForm(questions);
|
|
195
211
|
return (_jsx(MoversWordFillStructuredFormRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl || part.audioUrl, groupTitle: data.groupTitle, groupContent: data.groupContent, groupImageUrl: data.groupImageUrl, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function getChooseBestAnswerOptionImageSrc(option: {
|
|
2
|
+
imageUrl?: string;
|
|
3
|
+
text?: string;
|
|
4
|
+
}): string;
|
|
5
|
+
export declare function toChooseBestAnswerImageUrls(value?: string | string[]): string[];
|
|
6
|
+
export declare function ChooseBestAnswerImages({ imageUrl, className, imageClassName, }: {
|
|
7
|
+
imageUrl?: string | string[];
|
|
8
|
+
className?: string;
|
|
9
|
+
imageClassName?: string;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { ZoomIn } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { cn } from '../../../../../shared/lib/utils';
|
|
6
|
+
import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
|
|
7
|
+
export function getChooseBestAnswerOptionImageSrc(option) {
|
|
8
|
+
if (typeof option.imageUrl === 'string' && option.imageUrl.trim() !== '') {
|
|
9
|
+
return option.imageUrl.trim();
|
|
10
|
+
}
|
|
11
|
+
if (typeof option.text === 'string' && option.text.trim() !== '') {
|
|
12
|
+
return option.text.trim();
|
|
13
|
+
}
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
export function toChooseBestAnswerImageUrls(value) {
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
return value
|
|
19
|
+
.filter((url) => typeof url === 'string')
|
|
20
|
+
.map((url) => url.trim())
|
|
21
|
+
.filter((url) => url !== '');
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
24
|
+
return [value.trim()];
|
|
25
|
+
}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
function QuestionImage({ imageUrl, imageClassName, }) {
|
|
29
|
+
const [isZoomed, setIsZoomed] = useState(false);
|
|
30
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative flex items-center justify-center", children: [_jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration", className: cn('cursor-pointer rounded-lg object-contain shadow-md', imageClassName ?? 'max-h-[250px]'), onClick: () => setIsZoomed(true) }), _jsx("button", { type: "button", onClick: () => setIsZoomed(true), className: "absolute right-2 top-2 rounded-full bg-white/80 p-1.5 shadow-md hover:bg-white", "aria-label": "Zoom image", children: _jsx(ZoomIn, { className: "h-4 w-4 text-gray-600" }) })] }), isZoomed && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70", onClick: () => setIsZoomed(false), children: _jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration (zoomed)", className: "max-h-[90vh] max-w-[90vw] rounded-lg object-contain", showLoadingState: false }) }))] }));
|
|
31
|
+
}
|
|
32
|
+
export function ChooseBestAnswerImages({ imageUrl, className, imageClassName, }) {
|
|
33
|
+
const urls = toChooseBestAnswerImageUrls(imageUrl);
|
|
34
|
+
if (urls.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return (_jsx("div", { className: cn('flex flex-wrap justify-center gap-3', className), children: urls.map((url, index) => (_jsx(QuestionImage, { imageUrl: url, imageClassName: imageClassName }, `${url}-${index}`))) }));
|
|
38
|
+
}
|
package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
3
|
-
import { ZoomIn } from 'lucide-react';
|
|
4
|
-
import { useState } from 'react';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
5
3
|
import { cn } from '../../../../../shared/lib/utils';
|
|
6
4
|
import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
|
|
7
5
|
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
8
6
|
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
9
7
|
import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
|
|
8
|
+
import { ChooseBestAnswerImages, getChooseBestAnswerOptionImageSrc } from './ChooseBestAnswerImages';
|
|
10
9
|
function normalizeRenderableText(value) {
|
|
11
10
|
if (typeof value === 'string') {
|
|
12
11
|
return value;
|
|
@@ -41,16 +40,11 @@ function normalizeRenderableText(value) {
|
|
|
41
40
|
}
|
|
42
41
|
return '';
|
|
43
42
|
}
|
|
44
|
-
// Image with zoom functionality
|
|
45
|
-
function QuestionImage({ imageUrl }) {
|
|
46
|
-
const [isZoomed, setIsZoomed] = useState(false);
|
|
47
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative flex items-center justify-center", children: [_jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration", className: "max-h-[250px] cursor-pointer rounded-lg object-contain shadow-md", onClick: () => setIsZoomed(true) }), _jsx("button", { type: "button", onClick: () => setIsZoomed(true), className: "absolute right-2 top-2 rounded-full bg-white/80 p-1.5 shadow-md hover:bg-white", "aria-label": "Zoom image", children: _jsx(ZoomIn, { className: "h-4 w-4 text-gray-600" }) })] }), isZoomed && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70", onClick: () => setIsZoomed(false), children: _jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration (zoomed)", className: "max-h-[90vh] max-w-[90vw] rounded-lg object-contain", showLoadingState: false }) }))] }));
|
|
48
|
-
}
|
|
49
43
|
// Example Section Component
|
|
50
44
|
function ExampleSection({ example }) {
|
|
51
|
-
return (_jsxs("div", { className: "rounded-lg border-2 border-orange-300 bg-white p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-gray-700", children: "Example:" }), _jsxs("p", { className: "mb-3 text-[19px] font-bold text-[#001590]", children: [example.questionNumber, ". ", normalizeRenderableText(example.questionText)] }),
|
|
45
|
+
return (_jsxs("div", { className: "rounded-lg border-2 border-orange-300 bg-white p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-gray-700", children: "Example:" }), _jsxs("p", { className: "mb-3 text-[19px] font-bold text-[#001590]", children: [example.questionNumber, ". ", normalizeRenderableText(example.questionText)] }), _jsx(ChooseBestAnswerImages, { imageUrl: example.imageUrl, className: "mb-3" }), example.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${example.options.length}, minmax(0, 1fr))` }, children: example.options.map((option) => {
|
|
52
46
|
const isCorrect = option.id === example.correctOptionId;
|
|
53
|
-
return (_jsxs("div", { className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${isCorrect ? 'border-green-500 bg-green-50' : 'border-blue-200 bg-white'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: option
|
|
47
|
+
return (_jsxs("div", { className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${isCorrect ? 'border-green-500 bg-green-50' : 'border-blue-200 bg-white'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "w-40 rounded object-contain mb-2" }), _jsx("div", { className: cn("w-7 h-7 rounded-full flex items-center justify-center border-[3px] transition-all mt-auto", isCorrect
|
|
54
48
|
? "bg-green-500 border-green-500 text-white"
|
|
55
49
|
: "border-gray-300 bg-white text-transparent"), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }) })] }, option.id));
|
|
56
50
|
}) })) : (_jsx("div", { className: "flex flex-col gap-2 pl-2", children: example.options.map((option) => {
|
|
@@ -105,7 +99,7 @@ function QuestionItem({ question, selectedOptionIds, onSelect, isReviewMode, id,
|
|
|
105
99
|
}
|
|
106
100
|
return selectedOptionIds.includes(optionId) || correctOptionIds.includes(optionId);
|
|
107
101
|
};
|
|
108
|
-
return (_jsxs("div", { id: id, className: "mb-6", children: [_jsxs("p", { className: "mb-3 text-[19px] font-bold text-[#001590]", children: [question.questionNumber, ". ", normalizeRenderableText(question.questionText)] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })),
|
|
102
|
+
return (_jsxs("div", { id: id, className: "mb-6", children: [_jsxs("p", { className: "mb-3 text-[19px] font-bold text-[#001590]", children: [question.questionNumber, ". ", normalizeRenderableText(question.questionText)] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })), _jsx(ChooseBestAnswerImages, { imageUrl: question.imageUrl, className: "mb-3" }), question.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "w-35 rounded object-contain mb-2" }), _jsx("div", { className: cn("w-7 h-7 rounded-full flex items-center justify-center border-[3px] transition-all mt-auto", showIndicatorMark(option.id)
|
|
109
103
|
? "bg-blue-600 border-blue-600 text-white"
|
|
110
104
|
: "border-gray-300 bg-white text-transparent"), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }) })] }, option.id))) })) : (_jsx("div", { className: "flex flex-col gap-2 pl-2", children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 text-left transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx("span", { className: "flex-1 text-sm text-gray-700", children: normalizeRenderableText(option.text) }), _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 ${isMultipleAnswers ? 'rounded-md' : 'rounded-full'} ${getIndicatorStyle(option.id)}`, children: showIndicatorMark(option.id) && (_jsx("div", { className: `${isMultipleAnswers ? 'h-2.5 w-2.5 rounded-sm' : 'h-2 w-2 rounded-full'} bg-white` })) })] }, option.id))) }))] }));
|
|
111
105
|
}
|
package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FILL_IN_BLANK_GROUP Renderer
|
|
3
|
+
* Shared audio/image on top, then each item: picture + fill blanks.
|
|
4
|
+
*/
|
|
5
|
+
import type { FillInBlankGroupQuestion, FillInBlankGroupItemAnswer, FillInBlankGroupStudentItemAnswer } from '../../utils/question-transformers';
|
|
6
|
+
interface MoversFillInBlankGroupRendererProps {
|
|
7
|
+
partNumber: number;
|
|
8
|
+
partName?: string;
|
|
9
|
+
questionCount: number;
|
|
10
|
+
instruction: string;
|
|
11
|
+
audioUrl?: string;
|
|
12
|
+
imageUrl?: string;
|
|
13
|
+
showHints?: boolean;
|
|
14
|
+
hints?: string[];
|
|
15
|
+
questions: FillInBlankGroupQuestion[];
|
|
16
|
+
answers: Record<string, FillInBlankGroupStudentItemAnswer | undefined>;
|
|
17
|
+
onAnswerChange: (questionId: string, value: FillInBlankGroupStudentItemAnswer) => void;
|
|
18
|
+
isReviewMode?: boolean;
|
|
19
|
+
correctAnswerMap?: Record<string, FillInBlankGroupItemAnswer>;
|
|
20
|
+
}
|
|
21
|
+
export declare function MoversFillInBlankGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, imageUrl, showHints, hints, questions, answers, onAnswerChange, isReviewMode, correctAnswerMap, }: MoversFillInBlankGroupRendererProps): import("react").JSX.Element;
|
|
22
|
+
export {};
|
package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* FILL_IN_BLANK_GROUP Renderer
|
|
4
|
+
* Shared audio/image on top, then each item: picture + fill blanks.
|
|
5
|
+
*/
|
|
6
|
+
import { Fragment } from 'react';
|
|
7
|
+
import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
|
|
8
|
+
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
9
|
+
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
10
|
+
import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
|
|
11
|
+
import { extractPlaceholderIndices, isBlankMatch, padStudentAnswers, splitStem, } from '../../../../questions/types/fill-in-blank-group/blank-utils';
|
|
12
|
+
function emptyAnswer(blankCount) {
|
|
13
|
+
return { answers: Array.from({ length: blankCount }, () => '') };
|
|
14
|
+
}
|
|
15
|
+
function ItemCard({ question, answer, onAnswerChange, isReviewMode, correctAnswer, }) {
|
|
16
|
+
const isDisabled = isReviewMode || question.isExample;
|
|
17
|
+
const current = question.isExample
|
|
18
|
+
? { answers: question.exampleAnswers || emptyAnswer(question.blankCount).answers }
|
|
19
|
+
: { answers: padStudentAnswers(answer, question.blankCount) };
|
|
20
|
+
const stem = splitStem(question.question || '{0}');
|
|
21
|
+
const expected = correctAnswer?.answers || [];
|
|
22
|
+
const blankIndices = extractPlaceholderIndices(question.question || '{0}');
|
|
23
|
+
const allCorrect = isReviewMode
|
|
24
|
+
&& !question.isExample
|
|
25
|
+
&& blankIndices.length > 0
|
|
26
|
+
&& blankIndices.every((index) => isBlankMatch(current.answers[index], expected[index]));
|
|
27
|
+
const anyFilled = current.answers.some((value) => value.trim().length > 0);
|
|
28
|
+
const isWrong = isReviewMode && !question.isExample && anyFilled && !allCorrect;
|
|
29
|
+
const setBlank = (blankIndex, value) => {
|
|
30
|
+
const next = [...current.answers];
|
|
31
|
+
next[blankIndex] = value;
|
|
32
|
+
onAnswerChange(question.id, { answers: next });
|
|
33
|
+
};
|
|
34
|
+
return (_jsxs("div", { id: `question-${question.id}`, className: `rounded-2xl border p-3 sm:p-4 transition-all ${question.isExample
|
|
35
|
+
? 'border-amber-200 bg-amber-50/60'
|
|
36
|
+
: allCorrect
|
|
37
|
+
? 'border-emerald-300 bg-emerald-50/30'
|
|
38
|
+
: isWrong
|
|
39
|
+
? 'border-rose-300 bg-rose-50/30'
|
|
40
|
+
: 'border-slate-200 bg-white'}`, children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [_jsx("span", { className: `flex h-7 w-7 items-center justify-center rounded-full text-xs font-black text-white ${question.isExample ? 'bg-amber-500' : allCorrect ? 'bg-emerald-500' : isWrong ? 'bg-rose-500' : 'bg-blue-600'}`, children: question.questionNumber }), question.isExample && (_jsx("span", { className: "rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-extrabold uppercase text-amber-800", children: "Example" }))] }), question.audioUrl && (_jsx("div", { className: "mb-2", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: question.audioUrl, compact: true, dense: true }) })), _jsxs("div", { className: "flex items-center gap-3", children: [question.imageUrl && (_jsx(ResolvedImage, { src: question.imageUrl, alt: `Question ${question.questionNumber}`, className: "h-24 w-24 flex-shrink-0 rounded-lg object-contain" })), _jsx("div", { className: "min-w-0 flex-1 text-[15px] leading-relaxed text-slate-800", children: stem.map((part, partIndex) => {
|
|
41
|
+
if (part.type === 'text') {
|
|
42
|
+
const lines = part.value.split('\n');
|
|
43
|
+
return (_jsx(Fragment, { children: lines.map((line, lineIndex) => (_jsxs(Fragment, { children: [lineIndex > 0 ? _jsx("br", {}) : null, line] }, lineIndex))) }, partIndex));
|
|
44
|
+
}
|
|
45
|
+
const student = current.answers[part.index] || '';
|
|
46
|
+
const alts = expected[part.index] || [];
|
|
47
|
+
const blankCorrect = isReviewMode && !question.isExample && isBlankMatch(student, alts);
|
|
48
|
+
const blankWrong = isReviewMode && !question.isExample && student.trim().length > 0 && !blankCorrect;
|
|
49
|
+
return (_jsx("input", { type: "text", value: student, disabled: isDisabled, onChange: (event) => setBlank(part.index, event.target.value), placeholder: question.isExample ? '' : '...', className: `mx-1 inline-block w-[4.5rem] rounded-md border-2 px-1 py-1 text-center text-sm font-semibold outline-none sm:w-20 ${question.isExample
|
|
50
|
+
? 'border-amber-300 bg-amber-100/70 text-amber-800'
|
|
51
|
+
: blankCorrect
|
|
52
|
+
? 'border-emerald-400 bg-emerald-50 text-emerald-800'
|
|
53
|
+
: blankWrong
|
|
54
|
+
? 'border-rose-400 bg-rose-50 text-rose-800'
|
|
55
|
+
: 'border-blue-200 bg-blue-50/40 text-blue-900 focus:border-blue-500 focus:bg-white'}` }, partIndex));
|
|
56
|
+
}) })] }), isReviewMode && !question.isExample && expected.length > 0 && (_jsxs("div", { className: "mt-2 rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-2 text-sm text-emerald-800", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", expected.map((alts) => alts.join(' / ') || '(trống)').join(' | ')] }))] }));
|
|
57
|
+
}
|
|
58
|
+
export function MoversFillInBlankGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, imageUrl, showHints = false, hints = [], questions, answers, onAnswerChange, isReviewMode = false, correctAnswerMap, }) {
|
|
59
|
+
return (_jsxs("div", { className: "relative flex h-full flex-col gap-4 overflow-y-auto custom-scrollbar sm:gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2 p-3 sm:gap-3 sm:px-6 sm:pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), (audioUrl || imageUrl) && (_jsxs("div", { className: "sticky top-0 z-20 flex flex-col gap-3 border-b border-gray-100 bg-white/95 p-3 pb-2 shadow-sm backdrop-blur sm:px-6", children: [audioUrl && _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true }), imageUrl && (_jsx("div", { className: "flex justify-center", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Shared illustration", className: "max-h-[180px] rounded-lg object-contain" }) }))] })), showHints && hints.length > 0 && !isReviewMode && (_jsx("div", { className: "flex flex-wrap gap-2 px-3 sm:px-6", children: hints.map((hint) => (_jsx("span", { className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: hint }, hint))) })), _jsx("div", { className: "grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 sm:px-6 lg:grid-cols-3", children: questions.map((question) => (_jsx(ItemCard, { question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerMap?.[question.id] }, question.id))) })] }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MatchWordToPictureQuestion } from '../../utils/question-transformers';
|
|
2
|
+
interface MoversMatchWordToPictureRendererProps {
|
|
3
|
+
partNumber: number;
|
|
4
|
+
partName?: string;
|
|
5
|
+
questionCount: number;
|
|
6
|
+
instruction: string;
|
|
7
|
+
wordBank: string[];
|
|
8
|
+
questions: MatchWordToPictureQuestion[];
|
|
9
|
+
answers: Record<string, string>;
|
|
10
|
+
onAnswerChange: (questionId: string, value: string) => void;
|
|
11
|
+
isReviewMode?: boolean;
|
|
12
|
+
correctAnswerTextMap?: Record<string, string>;
|
|
13
|
+
isCorrectMap?: Record<string, boolean | null>;
|
|
14
|
+
}
|
|
15
|
+
export declare function MoversMatchWordToPictureRenderer({ partNumber, partName, questionCount, instruction, wordBank, questions, answers, onAnswerChange, isReviewMode, correctAnswerTextMap, isCorrectMap, }: MoversMatchWordToPictureRendererProps): import("react").JSX.Element;
|
|
16
|
+
export {};
|
package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MATCH_WORD_TO_PICTURE Renderer
|
|
3
|
+
* Shared word bank chips + picture grid + dropdown (one word used once).
|
|
4
|
+
*/
|
|
5
|
+
'use client';
|
|
6
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
7
|
+
import { Check, ChevronDown, X } from 'lucide-react';
|
|
8
|
+
import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
|
|
9
|
+
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
10
|
+
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
11
|
+
function normalizeWord(value) {
|
|
12
|
+
return value.trim().toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
function WordChip({ word, isUsed }) {
|
|
15
|
+
return (_jsx("span", { className: `rounded-full border px-3 py-1 text-sm font-semibold ${isUsed
|
|
16
|
+
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
|
17
|
+
: 'border-violet-200 bg-white text-slate-700'}`, children: word }));
|
|
18
|
+
}
|
|
19
|
+
function AnswerSelector({ value, onChange, disabled, wordBank, usedWords, }) {
|
|
20
|
+
return (_jsxs("div", { className: "group relative min-w-[160px] sm:min-w-[210px]", children: [_jsxs("select", { value: value, onChange: (event) => onChange(event.target.value), disabled: disabled, className: "h-[48px] w-full appearance-none rounded-2xl border border-slate-200 bg-white px-4 pr-11 text-sm font-semibold text-slate-700 shadow-sm outline-none transition-all duration-200 hover:border-violet-300 focus:border-violet-500 focus:ring-4 focus:ring-violet-100/50 disabled:cursor-not-allowed disabled:opacity-60", "aria-label": "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p", children: [_jsx("option", { value: "", children: "Ch\u1ECDn t\u1EEB" }), wordBank.map((word, index) => {
|
|
21
|
+
const isUsedByOther = usedWords.has(normalizeWord(word)) && normalizeWord(word) !== normalizeWord(value);
|
|
22
|
+
return (_jsx("option", { value: word, disabled: isUsedByOther, children: word }, `${word}-${index}`));
|
|
23
|
+
})] }), _jsx("div", { className: "pointer-events-none absolute right-2.5 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full bg-slate-100 text-slate-500 transition-colors group-focus-within:bg-violet-100 group-focus-within:text-violet-600", children: _jsx(ChevronDown, { className: "h-3.5 w-3.5", strokeWidth: 2.5 }) })] }));
|
|
24
|
+
}
|
|
25
|
+
function ReviewAnswer({ userAnswer, correctAnswer, status, }) {
|
|
26
|
+
const badgeCls = status === 'correct'
|
|
27
|
+
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
|
28
|
+
: status === 'incorrect'
|
|
29
|
+
? 'border-red-200 bg-red-50 text-red-700'
|
|
30
|
+
: 'border-slate-200 bg-slate-50 text-slate-700';
|
|
31
|
+
return (_jsxs("div", { className: `flex w-full items-center justify-center gap-2 rounded-xl border px-5 py-2 text-[15px] shadow-sm sm:w-auto ${badgeCls}`, children: [status === 'correct' && (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-4 w-4 shrink-0", strokeWidth: 3 }), _jsx("span", { className: "font-medium opacity-90", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "ml-1 rounded-md border border-emerald-100 bg-white px-2.5 py-0.5 font-bold shadow-sm", children: userAnswer || 'Trống' })] })), status === 'unanswered' && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-medium opacity-80", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "rounded-md border border-slate-200 bg-white px-2.5 py-0.5 font-bold shadow-sm", children: correctAnswer })] })), status === 'incorrect' && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(X, { className: "h-4 w-4 shrink-0", strokeWidth: 3 }), _jsx("span", { className: "font-medium opacity-90", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "rounded-md border border-red-100/50 bg-white/60 px-2 py-0.5 font-semibold line-through decoration-red-400/80 decoration-2 opacity-70", children: userAnswer || 'Trống' }), _jsx("span", { className: "font-bold text-red-400", children: "\u2192" }), _jsx("span", { className: "rounded-md border border-red-200 bg-white px-2.5 py-0.5 font-bold shadow-sm", children: correctAnswer })] }))] }));
|
|
32
|
+
}
|
|
33
|
+
function PictureCard({ question, rawAnswer, isDisabled, isExampleQ, onAnswerChange, isReviewMode, correctAnswer, answerStatus, wordBank, usedWords, }) {
|
|
34
|
+
return (_jsxs("div", { className: [
|
|
35
|
+
'flex flex-col gap-4 overflow-hidden rounded-[14px] border p-4 shadow-sm transition-all duration-200',
|
|
36
|
+
isExampleQ
|
|
37
|
+
? 'border-amber-200 bg-gradient-to-br from-amber-50 to-orange-50/50 shadow-amber-100/40'
|
|
38
|
+
: 'border-slate-200/80 bg-white hover:-translate-y-0.5 hover:border-violet-200 hover:shadow-md hover:shadow-violet-100/50',
|
|
39
|
+
].join(' '), children: [_jsxs("div", { className: "flex items-start gap-3", children: [_jsx("span", { className: `flex h-[38px] w-[38px] shrink-0 items-center justify-center rounded-[10px] text-[15px] font-extrabold text-white shadow-sm ${isExampleQ
|
|
40
|
+
? 'bg-gradient-to-br from-amber-400 to-orange-500'
|
|
41
|
+
: 'bg-gradient-to-br from-violet-500 to-indigo-600'}`, children: question.questionNumber }), isExampleQ && (_jsx("span", { className: "w-fit rounded-full border border-amber-300 bg-amber-100 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-amber-700", children: "Example" }))] }), question.imageUrl && (_jsx("div", { className: "flex w-full justify-center", children: _jsx(ResolvedImage, { src: question.imageUrl, alt: `Q${question.questionNumber}`, className: "h-32 w-auto min-w-[5rem] rounded-xl border border-slate-100 bg-slate-50 object-contain p-1.5 shadow-sm" }) })), _jsx("div", { className: "mt-1 flex w-full items-center justify-end border-t border-slate-100/80 pt-3", children: isReviewMode && !isExampleQ ? (_jsx(ReviewAnswer, { userAnswer: rawAnswer, correctAnswer: correctAnswer, status: answerStatus ?? 'unanswered' })) : (_jsx("div", { className: "w-full sm:w-auto", children: _jsx(AnswerSelector, { value: rawAnswer, onChange: (value) => onAnswerChange(question.id, value), disabled: isDisabled, wordBank: wordBank, usedWords: usedWords }) })) })] }));
|
|
42
|
+
}
|
|
43
|
+
export function MoversMatchWordToPictureRenderer({ partNumber, partName, questionCount, instruction, wordBank, questions, answers, onAnswerChange, isReviewMode = false, correctAnswerTextMap = {}, isCorrectMap = {}, }) {
|
|
44
|
+
const getAnswerStatus = (questionId) => {
|
|
45
|
+
const userAnswer = answers[questionId];
|
|
46
|
+
if (!userAnswer)
|
|
47
|
+
return 'unanswered';
|
|
48
|
+
const isCorrect = isCorrectMap[questionId];
|
|
49
|
+
if (isCorrect === true)
|
|
50
|
+
return 'correct';
|
|
51
|
+
if (isCorrect === false)
|
|
52
|
+
return 'incorrect';
|
|
53
|
+
return 'unanswered';
|
|
54
|
+
};
|
|
55
|
+
const usedWords = new Set();
|
|
56
|
+
Object.values(answers).forEach((value) => {
|
|
57
|
+
if (typeof value === 'string' && value.trim()) {
|
|
58
|
+
usedWords.add(normalizeWord(value));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
questions.forEach((question) => {
|
|
62
|
+
if (question.isExample && question.exampleAnswer) {
|
|
63
|
+
usedWords.add(normalizeWord(question.exampleAnswer));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const exampleQuestions = questions.filter((question) => question.isExample);
|
|
67
|
+
const actualQuestions = questions.filter((question) => !question.isExample);
|
|
68
|
+
return (_jsxs("div", { className: "custom-scrollbar flex h-full flex-col gap-5 overflow-y-auto sm:gap-6", children: [_jsxs("div", { className: "flex flex-col gap-4 p-3 sm:gap-5 sm:px-6 sm:pt-2", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), wordBank.length > 0 && (_jsxs("div", { className: "rounded-[20px] border border-slate-200/80 bg-gradient-to-br from-white via-white to-slate-50 p-4 shadow-lg shadow-slate-100/70 sm:p-5", children: [_jsx("h3", { className: "mb-4 text-sm font-bold uppercase tracking-[0.18em] text-slate-500", children: "Word bank" }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => (_jsx(WordChip, { word: word, isUsed: usedWords.has(normalizeWord(word)) }, `${word}-${index}`))) })] })), exampleQuestions.length > 0 && (_jsx("div", { className: "mt-2 grid grid-cols-1 gap-4 lg:grid-cols-2", children: exampleQuestions.map((question) => {
|
|
69
|
+
const rawAnswer = answers[question.id] || question.exampleAnswer || '';
|
|
70
|
+
return (_jsx("div", { id: `question-${question.id}`, className: "w-full", children: _jsx(PictureCard, { question: question, rawAnswer: rawAnswer, isDisabled: true, isExampleQ: true, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerTextMap[question.id], wordBank: wordBank, usedWords: usedWords }) }, question.id));
|
|
71
|
+
}) }))] }), exampleQuestions.length > 0 && actualQuestions.length > 0 && (_jsx("div", { className: "px-3 sm:px-6", children: _jsx("hr", { className: "my-2 border-t border-slate-200/60" }) })), _jsx("div", { className: "grid grid-cols-1 gap-4 p-3 sm:gap-5 sm:px-6 sm:pb-6 lg:grid-cols-2", children: actualQuestions.map((question) => {
|
|
72
|
+
const rawAnswer = answers[question.id] || '';
|
|
73
|
+
return (_jsx("div", { id: `question-${question.id}`, className: "w-full", children: _jsx(PictureCard, { question: question, rawAnswer: rawAnswer, isDisabled: isReviewMode, isExampleQ: false, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerTextMap[question.id], answerStatus: isReviewMode ? getAnswerStatus(question.id) : undefined, wordBank: wordBank, usedWords: usedWords }) }, question.id));
|
|
74
|
+
}) })] }));
|
|
75
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MatchingWithLinesGroupPartData } from '../../utils/question-transformers';
|
|
2
|
+
interface MoversMatchingWithLinesGroupRendererProps {
|
|
3
|
+
partNumber: number;
|
|
4
|
+
partName?: string;
|
|
5
|
+
questionCount: number;
|
|
6
|
+
instruction: string;
|
|
7
|
+
data: MatchingWithLinesGroupPartData;
|
|
8
|
+
answers: Record<string, string | undefined>;
|
|
9
|
+
onAnswerChange: (questionId: string, value: string) => void;
|
|
10
|
+
isReviewMode?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function MoversMatchingWithLinesGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode, }: MoversMatchingWithLinesGroupRendererProps): import("react").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
4
|
+
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
5
|
+
import { MatchingWithLinesGroupClient } from '../../../../questions/types/matching-with-lines-group/MatchingWithLinesGroupClient';
|
|
6
|
+
export function MoversMatchingWithLinesGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode = false, }) {
|
|
7
|
+
const userAnswers = data.questions.map((question) => {
|
|
8
|
+
if (question.isExample)
|
|
9
|
+
return question.correctRightId;
|
|
10
|
+
const raw = answers[question.id];
|
|
11
|
+
return typeof raw === 'string' ? raw : '';
|
|
12
|
+
});
|
|
13
|
+
return (_jsxs("div", { className: "w-full space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx("div", { className: "mt-4", children: _jsx(MatchingWithLinesGroupClient, { questionData: {
|
|
14
|
+
title: data.title,
|
|
15
|
+
instruction: data.instruction,
|
|
16
|
+
rightItems: data.rightItems,
|
|
17
|
+
items: data.questions.map((question) => ({
|
|
18
|
+
imageUrl: question.imageUrl,
|
|
19
|
+
correctAnswer: question.correctRightId,
|
|
20
|
+
isExample: question.isExample,
|
|
21
|
+
points: question.points,
|
|
22
|
+
questionNumber: question.questionNumber,
|
|
23
|
+
})),
|
|
24
|
+
explanation: data.explanation,
|
|
25
|
+
}, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: (index, rightId) => {
|
|
26
|
+
const target = data.questions[index];
|
|
27
|
+
if (target && !target.isExample) {
|
|
28
|
+
onAnswerChange(target.id, rightId);
|
|
29
|
+
}
|
|
30
|
+
} }) })] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRUE_FALSE_CORRECT_GROUP Renderer
|
|
3
|
+
* Tick/Cross each sentence and rewrite false sentences.
|
|
4
|
+
*/
|
|
5
|
+
import type { TrueFalseCorrectGroupQuestion, TrueFalseCorrectItemAnswer } from '../../utils/question-transformers';
|
|
6
|
+
interface MoversTrueFalseCorrectGroupRendererProps {
|
|
7
|
+
partNumber: number;
|
|
8
|
+
partName?: string;
|
|
9
|
+
questionCount: number;
|
|
10
|
+
instruction: string;
|
|
11
|
+
questions: TrueFalseCorrectGroupQuestion[];
|
|
12
|
+
answers: Record<string, TrueFalseCorrectItemAnswer | undefined>;
|
|
13
|
+
onAnswerChange: (questionId: string, value: TrueFalseCorrectItemAnswer) => void;
|
|
14
|
+
isReviewMode?: boolean;
|
|
15
|
+
correctAnswerMap?: Record<string, TrueFalseCorrectItemAnswer>;
|
|
16
|
+
}
|
|
17
|
+
export declare function MoversTrueFalseCorrectGroupRenderer({ partNumber, partName, questionCount, instruction, questions, answers, onAnswerChange, isReviewMode, correctAnswerMap, }: MoversTrueFalseCorrectGroupRendererProps): import("react").JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* TRUE_FALSE_CORRECT_GROUP Renderer
|
|
4
|
+
* Tick/Cross each sentence and rewrite false sentences.
|
|
5
|
+
*/
|
|
6
|
+
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
7
|
+
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
8
|
+
function normalizeText(value) {
|
|
9
|
+
return value.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
10
|
+
}
|
|
11
|
+
function parseCorrectAnswer(raw) {
|
|
12
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const obj = raw;
|
|
16
|
+
if (typeof obj.isTrue !== 'boolean') {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
isTrue: obj.isTrue,
|
|
21
|
+
correction: obj.isTrue ? '' : (typeof obj.correction === 'string' ? obj.correction : ''),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function emptyAnswer() {
|
|
25
|
+
return { isTrue: null, correction: '' };
|
|
26
|
+
}
|
|
27
|
+
function isFullyCorrect(student, correct) {
|
|
28
|
+
if (!student || !correct || student.isTrue === null) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (student.isTrue !== correct.isTrue) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (correct.isTrue) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return normalizeText(student.correction) === normalizeText(correct.correction);
|
|
38
|
+
}
|
|
39
|
+
function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctAnswer, id, }) {
|
|
40
|
+
const isDisabled = isReviewMode || question.isExample;
|
|
41
|
+
const currentAnswer = question.isExample
|
|
42
|
+
? (question.exampleCorrectAnswer || emptyAnswer())
|
|
43
|
+
: (answer || emptyAnswer());
|
|
44
|
+
const expected = question.isExample
|
|
45
|
+
? question.exampleCorrectAnswer
|
|
46
|
+
: parseCorrectAnswer(correctAnswer) || correctAnswer;
|
|
47
|
+
const hasCorrectAnswer = isReviewMode && !!expected && typeof expected.isTrue === 'boolean';
|
|
48
|
+
const isCorrect = hasCorrectAnswer && isFullyCorrect(currentAnswer, expected);
|
|
49
|
+
const isWrong = hasCorrectAnswer && currentAnswer.isTrue !== null && !isCorrect;
|
|
50
|
+
const showCorrection = currentAnswer.isTrue === false || (hasCorrectAnswer && expected?.isTrue === false);
|
|
51
|
+
const setTruth = (isTrue) => {
|
|
52
|
+
onAnswerChange(question.id, {
|
|
53
|
+
isTrue,
|
|
54
|
+
correction: isTrue ? '' : (currentAnswer.correction || ''),
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
return (_jsxs("div", { id: id, className: `w-full rounded-2xl border p-4 sm:p-4.5 transition-all duration-200 ${question.isExample
|
|
58
|
+
? 'border-amber-200 bg-gradient-to-r from-amber-50/70 via-orange-50/30 to-amber-50/50 shadow-sm'
|
|
59
|
+
: isCorrect
|
|
60
|
+
? 'border-emerald-300 bg-emerald-50/30 shadow-sm'
|
|
61
|
+
: isWrong
|
|
62
|
+
? 'border-rose-300 bg-rose-50/30 shadow-sm'
|
|
63
|
+
: currentAnswer.isTrue !== null
|
|
64
|
+
? 'border-blue-200 bg-blue-50/20 shadow-sm'
|
|
65
|
+
: 'border-slate-200/90 bg-white shadow-sm hover:border-blue-200 hover:shadow'}`, children: [_jsxs("div", { className: "flex flex-col md:flex-row md:items-center justify-between gap-3 sm:gap-4", children: [_jsxs("div", { className: "flex flex-1 items-start sm:items-center gap-3", children: [_jsx("span", { className: `flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-xs font-black text-white shadow-sm ${question.isExample
|
|
66
|
+
? 'bg-gradient-to-br from-amber-400 to-orange-500'
|
|
67
|
+
: isCorrect
|
|
68
|
+
? 'bg-emerald-500'
|
|
69
|
+
: isWrong
|
|
70
|
+
? 'bg-rose-500'
|
|
71
|
+
: 'bg-blue-600'}`, children: question.questionNumber }), _jsxs("div", { className: "flex flex-1 flex-col sm:flex-row sm:items-center gap-2 sm:gap-3", children: [question.isExample && (_jsx("span", { className: "self-start sm:self-center rounded-full border border-amber-300 bg-amber-100/90 px-2.5 py-0.5 text-[10.5px] font-extrabold uppercase tracking-wider text-amber-800 flex-shrink-0", children: "EXAMPLE" })), _jsx("p", { className: "text-[15.5px] sm:text-[16.5px] font-medium leading-relaxed text-slate-800", children: question.question })] })] }), _jsxs("div", { className: "flex items-center gap-2.5 flex-shrink-0 self-end md:self-center pl-10 md:pl-0", children: [_jsx("button", { type: "button", disabled: isDisabled, onClick: () => setTruth(true), className: `h-9 min-w-[90px] rounded-xl border-2 px-4 text-sm font-bold transition-all flex items-center justify-center gap-1 select-none ${currentAnswer.isTrue === true
|
|
72
|
+
? isReviewMode && isWrong && !question.isExample
|
|
73
|
+
? 'border-rose-500 bg-rose-50 text-rose-700'
|
|
74
|
+
: 'border-emerald-500 bg-emerald-50 text-emerald-700 shadow-sm'
|
|
75
|
+
: hasCorrectAnswer && expected?.isTrue === true && !question.isExample
|
|
76
|
+
? 'border-emerald-500 bg-emerald-50 text-emerald-700 ring-2 ring-emerald-300'
|
|
77
|
+
: 'border-slate-200 bg-slate-50/80 text-slate-700 hover:border-emerald-400 hover:bg-emerald-50/50 hover:text-emerald-700'} ${isDisabled ? 'cursor-not-allowed opacity-70' : 'cursor-pointer'}`, children: "\u2713 True" }), _jsx("button", { type: "button", disabled: isDisabled, onClick: () => setTruth(false), className: `h-9 min-w-[90px] rounded-xl border-2 px-4 text-sm font-bold transition-all flex items-center justify-center gap-1 select-none ${currentAnswer.isTrue === false
|
|
78
|
+
? isReviewMode && isWrong && !question.isExample
|
|
79
|
+
? 'border-rose-500 bg-rose-50 text-rose-700'
|
|
80
|
+
: 'border-emerald-500 bg-emerald-50 text-emerald-700 shadow-sm'
|
|
81
|
+
: hasCorrectAnswer && expected?.isTrue === false && !question.isExample
|
|
82
|
+
? 'border-emerald-500 bg-emerald-50 text-emerald-700 ring-2 ring-emerald-300'
|
|
83
|
+
: 'border-slate-200 bg-slate-50/80 text-slate-700 hover:border-rose-400 hover:bg-rose-50/50 hover:text-rose-700'} ${isDisabled ? 'cursor-not-allowed opacity-70' : 'cursor-pointer'}`, children: "\u2717 False" })] })] }), showCorrection && (_jsxs("div", { className: "mt-3 border-t border-slate-100 pt-3", children: [_jsx("label", { className: "mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500", children: "Correct the false sentence" }), isDisabled ? (_jsx("p", { className: "rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-800", children: currentAnswer.correction || '(empty)' })) : (_jsx("textarea", { value: currentAnswer.correction, onChange: (event) => onAnswerChange(question.id, {
|
|
84
|
+
isTrue: false,
|
|
85
|
+
correction: event.target.value,
|
|
86
|
+
}), placeholder: "Write the correct sentence...", rows: 2, className: "w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800 outline-none ring-blue-200 focus:border-blue-400 focus:ring-2" }))] })), isWrong && hasCorrectAnswer && !question.isExample && (_jsxs("div", { className: "mt-2.5 flex flex-col gap-1 border-t border-slate-100 pt-2 text-xs font-semibold", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2 text-rose-600", children: [_jsxs("span", { children: ["Student: ", currentAnswer.isTrue === null ? '(empty)' : currentAnswer.isTrue ? 'True' : 'False'] }), _jsx("span", { children: "\u2022" }), _jsxs("span", { className: "text-emerald-700", children: ["Correct: ", expected?.isTrue ? 'True' : 'False'] })] }), expected?.isTrue === false && (_jsxs("div", { className: "text-emerald-700", children: ["Correct sentence: ", expected.correction || '(empty)'] }))] }))] }));
|
|
87
|
+
}
|
|
88
|
+
export function MoversTrueFalseCorrectGroupRenderer({ partNumber, partName, questionCount, instruction, questions, answers, onAnswerChange, isReviewMode = false, correctAnswerMap = {}, }) {
|
|
89
|
+
const exampleQuestions = questions.filter((question) => question.isExample);
|
|
90
|
+
const regularQuestions = questions.filter((question) => !question.isExample);
|
|
91
|
+
return (_jsxs("div", { className: "custom-scrollbar flex h-full flex-col gap-4 overflow-y-auto sm:gap-5 pb-6", children: [_jsxs("div", { className: "flex flex-col gap-3.5 p-3 sm:px-6 sm:pt-2", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), exampleQuestions.length > 0 && (_jsx("div", { className: "mt-1 flex flex-col gap-3", children: exampleQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerMap[question.id] }, question.id))) }))] }), exampleQuestions.length > 0 && regularQuestions.length > 0 && (_jsx("div", { className: "px-3 sm:px-6", children: _jsx("hr", { className: "border-t border-dashed border-slate-200" }) })), _jsx("div", { className: "flex flex-col gap-3 px-3 sm:px-6", children: regularQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerMap[question.id] }, question.id))) })] }));
|
|
92
|
+
}
|
|
@@ -11,10 +11,11 @@ interface MoversWritingRendererProps {
|
|
|
11
11
|
onAnswerChange: (value: string) => void;
|
|
12
12
|
isReviewMode?: boolean;
|
|
13
13
|
guideImages?: string[];
|
|
14
|
+
passage?: string;
|
|
14
15
|
teacherScore?: number | null;
|
|
15
16
|
maxScore?: number;
|
|
16
17
|
teacherFeedback?: string | null;
|
|
17
18
|
writingAssessment?: WritingAssessment | null;
|
|
18
19
|
}
|
|
19
|
-
export declare function MoversWritingRenderer({ partNumber, partName, instruction, prompt, requirements, minWords, maxWords, answer, onAnswerChange, isReviewMode, guideImages, teacherScore, maxScore, teacherFeedback, writingAssessment, }: MoversWritingRendererProps): import("react").JSX.Element;
|
|
20
|
+
export declare function MoversWritingRenderer({ partNumber, partName, instruction, prompt, requirements, minWords, maxWords, answer, onAnswerChange, isReviewMode, guideImages, passage, teacherScore, maxScore, teacherFeedback, writingAssessment, }: MoversWritingRendererProps): import("react").JSX.Element;
|
|
20
21
|
export {};
|
|
@@ -5,14 +5,14 @@ import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
|
|
|
5
5
|
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
6
6
|
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
7
7
|
import { HtmlOrText } from '../../../../../shared/lib/html-content';
|
|
8
|
-
export function MoversWritingRenderer({ partNumber, partName, instruction, prompt, requirements, minWords, maxWords, answer, onAnswerChange, isReviewMode = false, guideImages, teacherScore, maxScore, teacherFeedback, writingAssessment, }) {
|
|
8
|
+
export function MoversWritingRenderer({ partNumber, partName, instruction, prompt, requirements, minWords, maxWords, answer, onAnswerChange, isReviewMode = false, guideImages, passage, teacherScore, maxScore, teacherFeedback, writingAssessment, }) {
|
|
9
9
|
// Count words by splitting on whitespace and filtering empty strings
|
|
10
10
|
const wordCount = useMemo(() => {
|
|
11
11
|
if (!answer || !answer.trim())
|
|
12
12
|
return 0;
|
|
13
13
|
return answer.trim().split(/\s+/).filter(Boolean).length;
|
|
14
14
|
}, [answer]);
|
|
15
|
-
return (_jsxs("div", { className: "flex flex-col h-full gap-4 sm:gap-6 overflow-y-auto custom-scrollbar relative", children: [_jsxs("div", { className: "flex flex-col gap-2 sm:gap-3 p-3 sm:px-6 sm:pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: 1, partName: partName, isReviewMode: isReviewMode }), instruction && _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), _jsx("div", { className: "flex flex-col gap-4 p-3 sm:px-6 sm:pt-0 sticky top-0 z-20 bg-white/95 backdrop-blur pb-2 shadow-sm border-b border-gray-100", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("div", { className: "rounded-2xl border border-slate-200 bg-white p-5 shadow-sm", children: _jsxs("div", { className: "flex flex-col gap-4 text-base leading-relaxed text-gray-800", children: [_jsxs("div", { className: "space-y-3", children: [_jsx("p", { className: "text-lg font-semibold text-slate-900", children: "Writing task" }), _jsx(HtmlOrText, { value: prompt, className: "text-base leading-relaxed text-gray-800" })] }), requirements && requirements.length > 0 && (_jsxs("div", { className: "rounded-xl bg-slate-50 p-4", children: [_jsx("p", { className: "text-sm font-bold uppercase tracking-wide text-slate-700", children: "Include" }), _jsx("ul", { className: "mt-3 space-y-2", children: requirements.map((req, index) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-slate-700", children: [_jsx("span", { className: "mt-1.5 h-2 w-2 rounded-full bg-blue-500" }), _jsx(HtmlOrText, { value: req, as: "span" })] }, index))) })] })), (minWords || maxWords) && (_jsxs("div", { className: "flex flex-wrap gap-2", children: [minWords && (_jsxs("div", { className: "inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700", children: ["Minimum: ", minWords, " words"] })), maxWords && (_jsxs("div", { className: "inline-flex items-center rounded-full border border-slate-200 bg-white px-3 py-1 text-sm font-medium text-slate-600", children: ["Suggested max: ", maxWords, " words"] }))] }))] }) }), guideImages && guideImages.length > 0 && (_jsxs("div", { className: "rounded-2xl border border-slate-200 bg-white p-4 shadow-sm", children: [_jsxs("div", { className: "mb-3 flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-sm font-semibold uppercase tracking-wide text-slate-600", children: "Story pictures" }), _jsx("p", { className: "text-xs text-slate-400", children: "Read the sequence from left to right" })] }), _jsx("div", { className: `grid gap-4 ${guideImages.length === 1 ? 'grid-cols-1' : 'grid-cols-1 sm:grid-cols-2'}`, children: guideImages.map((src, idx) => (_jsxs("div", { className: "rounded-xl border border-slate-200 bg-slate-50/80 p-2 shadow-sm", children: [_jsx(ResolvedImage, { src: src, alt: `Picture ${idx + 1}`, className: `w-full rounded-lg bg-white object-contain ${guideImages.length === 1 ? 'max-h-[400px]' : 'aspect-[4/3]'}` }), _jsx("div", { className: "pt-2 text-center text-[13px] font-medium text-slate-500", children: guideImages.length > 1 ? `Picture ${idx + 1}` : 'Reference Image' })] }, idx))) })] }))] }) }), _jsxs("div", { className: "flex flex-col gap-3 p-3 sm:px-6 sm:pb-6", children: [_jsxs("div", { className: "rounded-2xl border border-slate-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-slate-100 px-5 py-3 bg-slate-50/50 rounded-t-2xl", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-semibold uppercase tracking-wide text-slate-500", children: "Your answer" }), _jsx("p", { className: "text-xs text-slate-400", children: "Write clearly and organise your ideas in order." })] }), _jsxs("div", { className: `rounded-full px-3 py-1 text-sm font-semibold ${minWords && wordCount < minWords
|
|
15
|
+
return (_jsxs("div", { className: "flex flex-col h-full gap-4 sm:gap-6 overflow-y-auto custom-scrollbar relative", children: [_jsxs("div", { className: "flex flex-col gap-2 sm:gap-3 p-3 sm:px-6 sm:pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: 1, partName: partName, isReviewMode: isReviewMode }), instruction && _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), _jsx("div", { className: "flex flex-col gap-4 p-3 sm:px-6 sm:pt-0 sticky top-0 z-20 bg-white/95 backdrop-blur pb-2 shadow-sm border-b border-gray-100", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("div", { className: "rounded-2xl border border-slate-200 bg-white p-5 shadow-sm", children: _jsxs("div", { className: "flex flex-col gap-4 text-base leading-relaxed text-gray-800", children: [_jsxs("div", { className: "space-y-3", children: [_jsx("p", { className: "text-lg font-semibold text-slate-900", children: "Writing task" }), _jsx(HtmlOrText, { value: prompt, className: "text-base leading-relaxed text-gray-800" })] }), passage?.trim() && (_jsxs("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50/60 p-4", children: [_jsx("p", { className: "text-sm font-bold uppercase tracking-wide text-indigo-800", children: "Read" }), _jsx(HtmlOrText, { value: passage, className: "mt-3 text-sm leading-relaxed text-slate-800" })] })), requirements && requirements.length > 0 && (_jsxs("div", { className: "rounded-xl bg-slate-50 p-4", children: [_jsx("p", { className: "text-sm font-bold uppercase tracking-wide text-slate-700", children: "Include" }), _jsx("ul", { className: "mt-3 space-y-2", children: requirements.map((req, index) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-slate-700", children: [_jsx("span", { className: "mt-1.5 h-2 w-2 rounded-full bg-blue-500" }), _jsx(HtmlOrText, { value: req, as: "span" })] }, index))) })] })), (minWords || maxWords) && (_jsxs("div", { className: "flex flex-wrap gap-2", children: [minWords && (_jsxs("div", { className: "inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700", children: ["Minimum: ", minWords, " words"] })), maxWords && (_jsxs("div", { className: "inline-flex items-center rounded-full border border-slate-200 bg-white px-3 py-1 text-sm font-medium text-slate-600", children: ["Suggested max: ", maxWords, " words"] }))] }))] }) }), guideImages && guideImages.length > 0 && (_jsxs("div", { className: "rounded-2xl border border-slate-200 bg-white p-4 shadow-sm", children: [_jsxs("div", { className: "mb-3 flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-sm font-semibold uppercase tracking-wide text-slate-600", children: "Story pictures" }), _jsx("p", { className: "text-xs text-slate-400", children: "Read the sequence from left to right" })] }), _jsx("div", { className: `grid gap-4 ${guideImages.length === 1 ? 'grid-cols-1' : 'grid-cols-1 sm:grid-cols-2'}`, children: guideImages.map((src, idx) => (_jsxs("div", { className: "rounded-xl border border-slate-200 bg-slate-50/80 p-2 shadow-sm", children: [_jsx(ResolvedImage, { src: src, alt: `Picture ${idx + 1}`, className: `w-full rounded-lg bg-white object-contain ${guideImages.length === 1 ? 'max-h-[400px]' : 'aspect-[4/3]'}` }), _jsx("div", { className: "pt-2 text-center text-[13px] font-medium text-slate-500", children: guideImages.length > 1 ? `Picture ${idx + 1}` : 'Reference Image' })] }, idx))) })] }))] }) }), _jsxs("div", { className: "flex flex-col gap-3 p-3 sm:px-6 sm:pb-6", children: [_jsxs("div", { className: "rounded-2xl border border-slate-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-slate-100 px-5 py-3 bg-slate-50/50 rounded-t-2xl", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-semibold uppercase tracking-wide text-slate-500", children: "Your answer" }), _jsx("p", { className: "text-xs text-slate-400", children: "Write clearly and organise your ideas in order." })] }), _jsxs("div", { className: `rounded-full px-3 py-1 text-sm font-semibold ${minWords && wordCount < minWords
|
|
16
16
|
? 'border border-amber-200 bg-amber-50 text-amber-700'
|
|
17
17
|
: 'border border-emerald-200 bg-emerald-50 text-emerald-700'}`, children: [wordCount, " words"] })] }), _jsxs("div", { className: "p-4", children: [_jsx("textarea", { id: "writing-answer", name: "writing-answer", value: answer, onChange: (e) => {
|
|
18
18
|
const newValue = e.target.value;
|