@tinyweb_dev/oe-exam-sdk 0.2.9 → 0.2.10
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 +7 -3
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.d.ts +13 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.js +30 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/utils/answer-transformers.js +8 -1
- package/dist/components/exams/take/utils/question-transformers.d.ts +16 -1
- package/dist/components/exams/take/utils/question-transformers.js +47 -2
- package/dist/components/questions/_shared/config/question-types.config.js +18 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/index.d.ts +1 -0
- package/dist/components/questions/_shared/types/index.js +1 -0
- package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.d.ts +56 -0
- package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.js +2 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +35 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +2 -0
- package/dist/components/questions/_shared/types/word-ordering-group.type.d.ts +34 -0
- package/dist/components/questions/_shared/types/word-ordering-group.type.js +2 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +3 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
- package/dist/components/questions/creator/question-type-registry.js +6 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +1 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +5 -3
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +16 -2
- package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +74 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.js +222 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/index.d.ts +5 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/index.js +5 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.d.ts +8 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.js +199 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/register.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/register.js +36 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/transform.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/transform.js +66 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +65 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +95 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +6 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +16 -0
- package/dist/components/questions/types/match-word-to-picture-group/index.d.ts +5 -0
- package/dist/components/questions/types/match-word-to-picture-group/index.js +5 -0
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.d.ts +4 -0
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +77 -0
- package/dist/components/questions/types/match-word-to-picture-group/register.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/register.js +36 -0
- package/dist/components/questions/types/match-word-to-picture-group/transform.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/transform.js +36 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +25 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.js +106 -0
- package/dist/components/questions/types/word-ordering-group/index.d.ts +5 -0
- package/dist/components/questions/types/word-ordering-group/index.js +5 -0
- package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.d.ts +4 -0
- package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.js +74 -0
- package/dist/components/questions/types/word-ordering-group/register.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/register.js +36 -0
- package/dist/components/questions/types/word-ordering-group/transform.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/transform.js +33 -0
- package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.d.ts +8 -0
- package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.js +49 -0
- package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +2 -2
- package/dist/components/questions/viewer/QuestionViewer.js +6 -0
- package/dist/components/questions/viewer/WordOrderingGroupViewer.d.ts +7 -0
- package/dist/components/questions/viewer/WordOrderingGroupViewer.js +29 -0
- package/dist/components/questions/viewer/index.d.ts +2 -0
- package/dist/components/questions/viewer/index.js +2 -0
- package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
- package/dist/components/results/renderers/review-question-body-movers.js +6 -2
- package/dist/components/results/review-data.js +13 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +6 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +111 -2
- package/dist/shared/lib/utils/question-transform.js +6 -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/index.d.ts +3 -0
- package/dist/shared/types/questions/index.js +6 -0
- package/dist/shared/types/questions/match-columns-to-make-sentences.d.ts +50 -0
- package/dist/shared/types/questions/match-columns-to-make-sentences.js +1 -0
- package/dist/shared/types/questions/match-word-to-picture-group.d.ts +31 -0
- package/dist/shared/types/questions/match-word-to-picture-group.js +1 -0
- package/dist/shared/types/questions/word-ordering-group.d.ts +28 -0
- package/dist/shared/types/questions/word-ordering-group.js +1 -0
- package/package.json +1 -1
|
@@ -637,6 +637,12 @@ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (a
|
|
|
637
637
|
map.set(`${question.id}-item-${index}`, question.id);
|
|
638
638
|
});
|
|
639
639
|
}
|
|
640
|
+
if (part.questionType === 'WORD_ORDERING_GROUP' &&
|
|
641
|
+
Array.isArray(content?.items)) {
|
|
642
|
+
content.items.forEach((_, index) => {
|
|
643
|
+
map.set(`${question.id}-item-${index}`, question.id);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
640
646
|
});
|
|
641
647
|
});
|
|
642
648
|
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, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, 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, MoversWordOrderingGroupRenderer, 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, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, 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, transformWordOrderingGroupTake, 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, }) {
|
|
@@ -55,7 +55,7 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
55
55
|
}
|
|
56
56
|
case 'CHOOSE_THE_CORRECT_ANSWER_GROUP': {
|
|
57
57
|
const data = transformChooseTheCorrectAnswerGroup(questions);
|
|
58
|
-
return (_jsx(MoversChooseBestAnswerRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl || part.audioUrl, example: data.example, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
58
|
+
return (_jsx(MoversChooseBestAnswerRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl || part.audioUrl, imageUrl: data.imageUrl, example: data.example, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
59
59
|
}
|
|
60
60
|
case 'CHOOSE_CORRECT_ADJECTIVE': {
|
|
61
61
|
const data = transformChooseCorrectAdjective(questions);
|
|
@@ -181,6 +181,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
181
181
|
const data = transformTrueFalse(questions);
|
|
182
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 }));
|
|
183
183
|
}
|
|
184
|
+
case 'WORD_ORDERING_GROUP': {
|
|
185
|
+
const data = transformWordOrderingGroupTake(questions);
|
|
186
|
+
return (_jsx(MoversWordOrderingGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: data.questions.filter((q) => !q.isExample).length, instruction: part.instructions || data.instruction, data: data, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
187
|
+
}
|
|
184
188
|
case 'WORD_ORDERING': {
|
|
185
189
|
const data = transformWordOrdering(questions);
|
|
186
190
|
return (_jsx(MoversWordOrderingRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WordOrderingGroupPartData } from '../../utils/question-transformers';
|
|
2
|
+
interface MoversWordOrderingGroupRendererProps {
|
|
3
|
+
partNumber: number;
|
|
4
|
+
partName?: string;
|
|
5
|
+
questionCount: number;
|
|
6
|
+
instruction: string;
|
|
7
|
+
data: WordOrderingGroupPartData;
|
|
8
|
+
answers: Record<string, string[] | undefined>;
|
|
9
|
+
onAnswerChange: (questionId: string, value: string[]) => void;
|
|
10
|
+
isReviewMode?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function MoversWordOrderingGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode, }: MoversWordOrderingGroupRendererProps): import("react").JSX.Element;
|
|
13
|
+
export {};
|
package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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 { WordOrderingGroupClient } from '../../../../questions/types/word-ordering-group/WordOrderingGroupClient';
|
|
6
|
+
export function MoversWordOrderingGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode = false, }) {
|
|
7
|
+
const userAnswers = data.questions.map((question) => {
|
|
8
|
+
if (question.isExample)
|
|
9
|
+
return question.answer;
|
|
10
|
+
return Array.isArray(answers[question.id]) ? answers[question.id] || [] : [];
|
|
11
|
+
});
|
|
12
|
+
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(WordOrderingGroupClient, { questionData: {
|
|
13
|
+
instruction: data.instruction,
|
|
14
|
+
items: data.questions.map((question) => ({
|
|
15
|
+
words: question.words,
|
|
16
|
+
answer: question.answer,
|
|
17
|
+
isExample: question.isExample,
|
|
18
|
+
points: question.points,
|
|
19
|
+
questionNumber: question.questionNumber,
|
|
20
|
+
})),
|
|
21
|
+
explanation: data.explanation,
|
|
22
|
+
}, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: (next) => {
|
|
23
|
+
next.forEach((value, index) => {
|
|
24
|
+
const target = data.questions[index];
|
|
25
|
+
if (target && !target.isExample) {
|
|
26
|
+
onAnswerChange(target.id, value);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
} }) })] }));
|
|
30
|
+
}
|
|
@@ -16,6 +16,7 @@ export { MoversCrossOutWordGroupRenderer } from './MoversCrossOutWordGroupRender
|
|
|
16
16
|
export { MoversAnswerTheQuestionRenderer } from './MoversAnswerTheQuestionRenderer';
|
|
17
17
|
export { MoversWriteSentencesRenderer } from './MoversWriteSentencesRenderer';
|
|
18
18
|
export { MoversWordOrderingRenderer } from './MoversWordOrderingRenderer';
|
|
19
|
+
export { MoversWordOrderingGroupRenderer } from './MoversWordOrderingGroupRenderer';
|
|
19
20
|
export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRenderer';
|
|
20
21
|
export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
|
|
21
22
|
export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
|
|
@@ -20,6 +20,7 @@ export { MoversCrossOutWordGroupRenderer } from './MoversCrossOutWordGroupRender
|
|
|
20
20
|
export { MoversAnswerTheQuestionRenderer } from './MoversAnswerTheQuestionRenderer';
|
|
21
21
|
export { MoversWriteSentencesRenderer } from './MoversWriteSentencesRenderer';
|
|
22
22
|
export { MoversWordOrderingRenderer } from './MoversWordOrderingRenderer';
|
|
23
|
+
export { MoversWordOrderingGroupRenderer } from './MoversWordOrderingGroupRenderer';
|
|
23
24
|
export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRenderer';
|
|
24
25
|
export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
|
|
25
26
|
export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
|
|
@@ -92,7 +92,8 @@ function findParentQuestion(subQuestionId, parts) {
|
|
|
92
92
|
part.questionType === 'MATCHING_WITH_LINES_GROUP' ||
|
|
93
93
|
part.questionType === 'WORD_ORDER_AND_MATCH_GROUP' ||
|
|
94
94
|
part.questionType === 'SPONTANEOUS_QA_GROUP' ||
|
|
95
|
-
part.questionType === 'ANSWER_THE_QUESTION_GROUP'
|
|
95
|
+
part.questionType === 'ANSWER_THE_QUESTION_GROUP' ||
|
|
96
|
+
part.questionType === 'WORD_ORDERING_GROUP') {
|
|
96
97
|
const parent = part.questions.find((question) => subQuestionId.startsWith(`${question.id}-item-`));
|
|
97
98
|
if (parent) {
|
|
98
99
|
return {
|
|
@@ -397,6 +398,12 @@ export function transformAnswersForSubmission(answers, parts) {
|
|
|
397
398
|
}
|
|
398
399
|
return { answers: [] };
|
|
399
400
|
}
|
|
401
|
+
if (parentType === 'WORD_ORDERING_GROUP') {
|
|
402
|
+
if (Array.isArray(raw)) {
|
|
403
|
+
return raw.filter((token) => typeof token === 'string');
|
|
404
|
+
}
|
|
405
|
+
return [];
|
|
406
|
+
}
|
|
400
407
|
if (parentType === 'WORD_ORDER_AND_MATCH_GROUP') {
|
|
401
408
|
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
402
409
|
const obj = raw;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Each transformer handles specific question type data structure.
|
|
6
6
|
*/
|
|
7
7
|
import type { FillBlankQuestion, ChooseBestAnswerQuestion, PictureChooseSubQuestion, PictureFillBlankSubQuestion, ReadingPassageQuestion, LabelThePictureLabel, FillWordHintSubQuestion, ColoringHintItem, ColoringRegion } from '../types';
|
|
8
|
-
export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
|
|
8
|
+
export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_ORDERING_GROUP' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
|
|
9
9
|
export interface ApiQuestion {
|
|
10
10
|
id: string;
|
|
11
11
|
content: any;
|
|
@@ -162,6 +162,7 @@ export declare function transformChooseTheCorrectAnswerGroup(questions: ApiQuest
|
|
|
162
162
|
example?: import('../types').ChooseBestAnswerExample;
|
|
163
163
|
instruction: string;
|
|
164
164
|
audioUrl?: string;
|
|
165
|
+
imageUrl?: string;
|
|
165
166
|
};
|
|
166
167
|
export interface ChooseAdjectiveOption {
|
|
167
168
|
id: string;
|
|
@@ -550,3 +551,17 @@ export interface MatchingWithLinesGroupPartData {
|
|
|
550
551
|
explanation?: string;
|
|
551
552
|
}
|
|
552
553
|
export declare function transformMatchingWithLinesGroup(questions: ApiQuestion[]): MatchingWithLinesGroupPartData;
|
|
554
|
+
export interface WordOrderingGroupQuestionItem {
|
|
555
|
+
id: string;
|
|
556
|
+
questionNumber: number;
|
|
557
|
+
words: string[];
|
|
558
|
+
answer: string[];
|
|
559
|
+
isExample: boolean;
|
|
560
|
+
points: number;
|
|
561
|
+
}
|
|
562
|
+
export interface WordOrderingGroupPartData {
|
|
563
|
+
questions: WordOrderingGroupQuestionItem[];
|
|
564
|
+
instruction: string;
|
|
565
|
+
explanation?: string;
|
|
566
|
+
}
|
|
567
|
+
export declare function transformWordOrderingGroupTake(questions: ApiQuestion[]): WordOrderingGroupPartData;
|
|
@@ -376,6 +376,16 @@ export function transformChooseCorrectAnswer(questions, optionsConfig) {
|
|
|
376
376
|
instruction: firstContent?.instruction || 'CHOOSE THE CORRECT ANSWER',
|
|
377
377
|
};
|
|
378
378
|
}
|
|
379
|
+
function asChooseAnswerOptionId(value) {
|
|
380
|
+
if (typeof value === 'string') {
|
|
381
|
+
return value.trim();
|
|
382
|
+
}
|
|
383
|
+
if (Array.isArray(value)) {
|
|
384
|
+
const first = value.find((item) => typeof item === 'string' && item.trim() !== '');
|
|
385
|
+
return typeof first === 'string' ? first.trim() : '';
|
|
386
|
+
}
|
|
387
|
+
return '';
|
|
388
|
+
}
|
|
379
389
|
export function transformChooseTheCorrectAnswerGroup(questions) {
|
|
380
390
|
const parent = questions[0];
|
|
381
391
|
const content = parent?.content;
|
|
@@ -416,11 +426,18 @@ export function transformChooseTheCorrectAnswerGroup(questions) {
|
|
|
416
426
|
return trimmed !== '' ? trimmed : undefined;
|
|
417
427
|
};
|
|
418
428
|
const groupAudioUrl = toAudioUrl(content?.meta?.audioUrl);
|
|
429
|
+
const groupImageUrlRaw = toQuestionImageUrl(content?.meta?.imageUrl);
|
|
430
|
+
const groupImageUrl = Array.isArray(groupImageUrlRaw)
|
|
431
|
+
? groupImageUrlRaw[0]
|
|
432
|
+
: groupImageUrlRaw;
|
|
419
433
|
let example;
|
|
420
434
|
if (exampleItem) {
|
|
421
435
|
const exampleIndex = items.indexOf(exampleItem);
|
|
422
436
|
const exOptions = mapOptions(exampleItem.content?.options, `${parent.id}-ex`);
|
|
423
|
-
const exCorrect = exampleItem.correctAnswer?.answer
|
|
437
|
+
const exCorrect = asChooseAnswerOptionId(exampleItem.correctAnswer?.answer)
|
|
438
|
+
|| asChooseAnswerOptionId(parentCorrect[exampleIndex])
|
|
439
|
+
|| exOptions[0]?.id
|
|
440
|
+
|| '';
|
|
424
441
|
const exampleImageUrl = toQuestionImageUrl(exampleItem.content?.imageUrl);
|
|
425
442
|
const exampleAudioUrl = toAudioUrl(exampleItem.content?.audioUrl);
|
|
426
443
|
example = {
|
|
@@ -436,7 +453,9 @@ export function transformChooseTheCorrectAnswerGroup(questions) {
|
|
|
436
453
|
const transformed = regularItems.map((item) => {
|
|
437
454
|
const itemIndex = items.indexOf(item);
|
|
438
455
|
const options = mapOptions(item.content?.options, `${parent.id}-item-${itemIndex}`);
|
|
439
|
-
const correctId = item.correctAnswer?.answer
|
|
456
|
+
const correctId = asChooseAnswerOptionId(item.correctAnswer?.answer)
|
|
457
|
+
|| asChooseAnswerOptionId(parentCorrect[itemIndex])
|
|
458
|
+
|| '';
|
|
440
459
|
const itemImageUrl = toQuestionImageUrl(item.content?.imageUrl);
|
|
441
460
|
const itemAudioUrl = toAudioUrl(item.content?.audioUrl);
|
|
442
461
|
return {
|
|
@@ -456,6 +475,7 @@ export function transformChooseTheCorrectAnswerGroup(questions) {
|
|
|
456
475
|
example,
|
|
457
476
|
instruction: content?.meta?.instruction || 'Choose the correct answer.',
|
|
458
477
|
...(groupAudioUrl ? { audioUrl: groupAudioUrl } : {}),
|
|
478
|
+
...(groupImageUrl ? { imageUrl: groupImageUrl } : {}),
|
|
459
479
|
};
|
|
460
480
|
}
|
|
461
481
|
export function transformChooseCorrectAdjective(questions) {
|
|
@@ -1392,3 +1412,28 @@ export function transformMatchingWithLinesGroup(questions) {
|
|
|
1392
1412
|
explanation: parent?.explanation,
|
|
1393
1413
|
};
|
|
1394
1414
|
}
|
|
1415
|
+
export function transformWordOrderingGroupTake(questions) {
|
|
1416
|
+
const parent = questions[0];
|
|
1417
|
+
const content = (parent?.content || {});
|
|
1418
|
+
const items = Array.isArray(content.items) ? content.items : [];
|
|
1419
|
+
const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
|
|
1420
|
+
? parent.correct_answer.answer
|
|
1421
|
+
: [];
|
|
1422
|
+
return {
|
|
1423
|
+
questions: items.map((item, index) => {
|
|
1424
|
+
const isExample = item.isExample === true;
|
|
1425
|
+
const fromItem = Array.isArray(item.correctAnswer?.answer) ? item.correctAnswer.answer : [];
|
|
1426
|
+
const fromParent = Array.isArray(parentCorrect[index]) ? parentCorrect[index] : [];
|
|
1427
|
+
return {
|
|
1428
|
+
id: `${parent?.id || 'q'}-item-${index}`,
|
|
1429
|
+
questionNumber: item.questionNumber || index + 1,
|
|
1430
|
+
words: Array.isArray(item.content?.words) ? item.content.words : [],
|
|
1431
|
+
answer: fromItem.length ? fromItem : fromParent,
|
|
1432
|
+
isExample,
|
|
1433
|
+
points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
1434
|
+
};
|
|
1435
|
+
}),
|
|
1436
|
+
instruction: content.meta?.instruction || 'Order the words.',
|
|
1437
|
+
explanation: parent?.explanation,
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
@@ -347,6 +347,21 @@ export const QUESTION_TYPE_CONFIG = {
|
|
|
347
347
|
requiresContent: true,
|
|
348
348
|
hasDedicatedComponent: true,
|
|
349
349
|
},
|
|
350
|
+
MATCH_COLUMNS_TO_MAKE_SENTENCES: {
|
|
351
|
+
label: 'Ghép cột thành câu',
|
|
352
|
+
requiresContent: true,
|
|
353
|
+
hasDedicatedComponent: true,
|
|
354
|
+
},
|
|
355
|
+
MATCH_WORD_TO_PICTURE_GROUP: {
|
|
356
|
+
label: 'Nối từ với tranh (nhóm)',
|
|
357
|
+
requiresContent: true,
|
|
358
|
+
hasDedicatedComponent: true,
|
|
359
|
+
},
|
|
360
|
+
WORD_ORDERING_GROUP: {
|
|
361
|
+
label: 'Sắp xếp từ (nhóm)',
|
|
362
|
+
requiresContent: true,
|
|
363
|
+
hasDedicatedComponent: true,
|
|
364
|
+
},
|
|
350
365
|
};
|
|
351
366
|
export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
|
|
352
367
|
'FILL_IN_BLANK',
|
|
@@ -398,6 +413,9 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
|
|
|
398
413
|
'WORD_FILL_STRUCTURED_FORM',
|
|
399
414
|
'CROSSWORD_PUZZLE',
|
|
400
415
|
'FIND_WORDS_IN_MATRIX',
|
|
416
|
+
'MATCH_COLUMNS_TO_MAKE_SENTENCES',
|
|
417
|
+
'MATCH_WORD_TO_PICTURE_GROUP',
|
|
418
|
+
'WORD_ORDERING_GROUP',
|
|
401
419
|
];
|
|
402
420
|
export function getQuestionTypeLabel(type) {
|
|
403
421
|
return QUESTION_TYPE_CONFIG[type]?.label || type;
|
|
@@ -24,6 +24,7 @@ export interface ChooseTheCorrectAnswerGroupData {
|
|
|
24
24
|
instruction: string;
|
|
25
25
|
passage?: string;
|
|
26
26
|
audioUrl?: string;
|
|
27
|
+
imageUrl?: ChooseTheCorrectAnswerGroupImageUrl;
|
|
27
28
|
items: ChooseTheCorrectAnswerGroupItemData[];
|
|
28
29
|
explanation?: string;
|
|
29
30
|
points?: number;
|
|
@@ -15,4 +15,5 @@ export * from './true-false-correct-group.type';
|
|
|
15
15
|
export * from './fill-in-blank-group.type';
|
|
16
16
|
export * from './match-word-to-picture.type';
|
|
17
17
|
export * from './matching-with-lines-group.type';
|
|
18
|
+
export * from './match-columns-to-make-sentences.type';
|
|
18
19
|
export * from './spontaneous-qa-group.type';
|
|
@@ -15,4 +15,5 @@ export * from './true-false-correct-group.type';
|
|
|
15
15
|
export * from './fill-in-blank-group.type';
|
|
16
16
|
export * from './match-word-to-picture.type';
|
|
17
17
|
export * from './matching-with-lines-group.type';
|
|
18
|
+
export * from './match-columns-to-make-sentences.type';
|
|
18
19
|
export * from './spontaneous-qa-group.type';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const MATCH_COLUMNS_TO_MAKE_SENTENCE_ITEM_TYPE: "MATCH_COLUMNS_TO_MAKE_SENTENCE";
|
|
3
|
+
export declare const MATCH_COLUMNS_DEFAULT_INSTRUCTION = "Match the information to make sentences. You can use the same words more than once.";
|
|
4
|
+
export type MatchColumnsColumnRole = 'stem' | 'choice';
|
|
5
|
+
export interface MatchColumnsFragment {
|
|
6
|
+
id: string;
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
export interface MatchColumnsColumn {
|
|
10
|
+
id: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
role: MatchColumnsColumnRole;
|
|
13
|
+
fragments: MatchColumnsFragment[];
|
|
14
|
+
}
|
|
15
|
+
export interface MatchColumnsItemAnswer {
|
|
16
|
+
fragmentIds: string[];
|
|
17
|
+
sentence: string;
|
|
18
|
+
}
|
|
19
|
+
export interface MatchColumnsStudentItemAnswer {
|
|
20
|
+
fragmentIds: string[];
|
|
21
|
+
sentence: string;
|
|
22
|
+
}
|
|
23
|
+
export interface MatchColumnsGroupItemData {
|
|
24
|
+
stemFragmentId: string;
|
|
25
|
+
fragmentIds: string[];
|
|
26
|
+
sentence: string;
|
|
27
|
+
isExample?: boolean;
|
|
28
|
+
points: number;
|
|
29
|
+
questionNumber: number;
|
|
30
|
+
}
|
|
31
|
+
export interface MatchColumnsGroupData {
|
|
32
|
+
title?: string;
|
|
33
|
+
instruction: string;
|
|
34
|
+
columns: MatchColumnsColumn[];
|
|
35
|
+
items: MatchColumnsGroupItemData[];
|
|
36
|
+
explanation?: string;
|
|
37
|
+
points?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface MatchColumnsGroupCreatorProps {
|
|
40
|
+
initialData?: MatchColumnsGroupData;
|
|
41
|
+
onSave?: (data: MatchColumnsGroupData) => void;
|
|
42
|
+
onCancel?: () => void;
|
|
43
|
+
onChange?: (data: MatchColumnsGroupData) => void;
|
|
44
|
+
externalErrors?: string[];
|
|
45
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
46
|
+
validationRef?: MutableRefObject<{
|
|
47
|
+
triggerValidation: () => Promise<boolean>;
|
|
48
|
+
getFormData?: () => MatchColumnsGroupData;
|
|
49
|
+
} | null>;
|
|
50
|
+
}
|
|
51
|
+
export interface MatchColumnsGroupClientProps {
|
|
52
|
+
questionData: MatchColumnsGroupData;
|
|
53
|
+
isReviewMode?: boolean;
|
|
54
|
+
userAnswers?: MatchColumnsStudentItemAnswer[];
|
|
55
|
+
onAnswerChange?: (answers: MatchColumnsStudentItemAnswer[]) => void;
|
|
56
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION = "Look and complete.";
|
|
3
|
+
export declare const MATCH_WORD_TO_PICTURE_ITEM_TYPE: "MATCH_WORD_TO_PICTURE";
|
|
4
|
+
export interface MatchWordToPictureGroupItemData {
|
|
5
|
+
imageUrl: string;
|
|
6
|
+
correctAnswer: string;
|
|
7
|
+
isExample?: boolean;
|
|
8
|
+
points: number;
|
|
9
|
+
questionNumber: number;
|
|
10
|
+
}
|
|
11
|
+
export interface MatchWordToPictureGroupData {
|
|
12
|
+
instruction: string;
|
|
13
|
+
wordBank: string[];
|
|
14
|
+
items: MatchWordToPictureGroupItemData[];
|
|
15
|
+
explanation?: string;
|
|
16
|
+
points?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface MatchWordToPictureGroupCreatorProps {
|
|
19
|
+
initialData?: MatchWordToPictureGroupData;
|
|
20
|
+
onSave?: (data: MatchWordToPictureGroupData) => void;
|
|
21
|
+
onCancel?: () => void;
|
|
22
|
+
onChange?: (data: MatchWordToPictureGroupData) => void;
|
|
23
|
+
externalErrors?: string[];
|
|
24
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
25
|
+
validationRef?: MutableRefObject<{
|
|
26
|
+
triggerValidation: () => Promise<boolean>;
|
|
27
|
+
getFormData?: () => MatchWordToPictureGroupData;
|
|
28
|
+
} | null>;
|
|
29
|
+
}
|
|
30
|
+
export interface MatchWordToPictureGroupClientProps {
|
|
31
|
+
questionData: MatchWordToPictureGroupData;
|
|
32
|
+
isReviewMode?: boolean;
|
|
33
|
+
userAnswers?: string[];
|
|
34
|
+
onAnswerChange?: (answers: string[]) => void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION = "Order the words.";
|
|
3
|
+
export declare const WORD_ORDERING_ITEM_TYPE: "WORD_ORDERING";
|
|
4
|
+
export interface WordOrderingGroupItemData {
|
|
5
|
+
words: string[];
|
|
6
|
+
answer: string[];
|
|
7
|
+
isExample?: boolean;
|
|
8
|
+
points: number;
|
|
9
|
+
questionNumber: number;
|
|
10
|
+
}
|
|
11
|
+
export interface WordOrderingGroupData {
|
|
12
|
+
instruction: string;
|
|
13
|
+
items: WordOrderingGroupItemData[];
|
|
14
|
+
explanation?: string;
|
|
15
|
+
points?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface WordOrderingGroupCreatorProps {
|
|
18
|
+
initialData?: WordOrderingGroupData;
|
|
19
|
+
onSave?: (data: WordOrderingGroupData) => void;
|
|
20
|
+
onCancel?: () => void;
|
|
21
|
+
onChange?: (data: WordOrderingGroupData) => void;
|
|
22
|
+
externalErrors?: string[];
|
|
23
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
24
|
+
validationRef?: MutableRefObject<{
|
|
25
|
+
triggerValidation: () => Promise<boolean>;
|
|
26
|
+
getFormData?: () => WordOrderingGroupData;
|
|
27
|
+
} | null>;
|
|
28
|
+
}
|
|
29
|
+
export interface WordOrderingGroupClientProps {
|
|
30
|
+
questionData: WordOrderingGroupData;
|
|
31
|
+
isReviewMode?: boolean;
|
|
32
|
+
userAnswers?: string[][];
|
|
33
|
+
onAnswerChange?: (answers: string[][]) => void;
|
|
34
|
+
}
|
|
@@ -68,6 +68,9 @@ const getQuestionTypeName = (type) => {
|
|
|
68
68
|
'READ_PASSAGE_AND_ANSWER_QUESTIONS': 'Đọc hiểu',
|
|
69
69
|
'MATCH_WORD_TO_PICTURE': 'Nối từ với tranh',
|
|
70
70
|
'MATCHING_WITH_LINES_GROUP': 'Nối bằng đường kẻ (nhóm)',
|
|
71
|
+
'MATCH_COLUMNS_TO_MAKE_SENTENCES': 'Ghép cột thành câu',
|
|
72
|
+
'MATCH_WORD_TO_PICTURE_GROUP': 'Nối từ với tranh (nhóm)',
|
|
73
|
+
'WORD_ORDERING_GROUP': 'Sắp xếp từ (nhóm)',
|
|
71
74
|
};
|
|
72
75
|
return type ? typeMap[type] || type.replace(/_/g, ' ').toLowerCase() : 'Chưa xác định';
|
|
73
76
|
};
|
|
@@ -28,6 +28,9 @@ export function QuestionTypeSelector({ value, onValueChange, disabled, }) {
|
|
|
28
28
|
'MATCH_BY_WRITING_ANSWER',
|
|
29
29
|
'MATCH_WORD_TO_PICTURE',
|
|
30
30
|
'MATCHING_WITH_LINES_GROUP',
|
|
31
|
+
'MATCH_COLUMNS_TO_MAKE_SENTENCES',
|
|
32
|
+
'MATCH_WORD_TO_PICTURE_GROUP',
|
|
33
|
+
'WORD_ORDERING_GROUP',
|
|
31
34
|
'WRITE_SENTENCES',
|
|
32
35
|
'FILL_IN_BLANK_GROUP',
|
|
33
36
|
];
|
|
@@ -49,6 +49,9 @@ import { wordFillStructuredFormRegistration } from '../types/word-fill-structure
|
|
|
49
49
|
import { wordOrderAndMatchGroupRegistration } from '../types/word-order-and-match-group/register';
|
|
50
50
|
import { crosswordPuzzleRegistration } from '../types/crossword-puzzle/register';
|
|
51
51
|
import { findWordsInMatrixRegistration } from '../types/find-words-in-matrix/register';
|
|
52
|
+
import { matchColumnsToMakeSentencesRegistration } from '../types/match-columns-to-make-sentences/register';
|
|
53
|
+
import { matchWordToPictureGroupRegistration } from '../types/match-word-to-picture-group/register';
|
|
54
|
+
import { wordOrderingGroupRegistration } from '../types/word-ordering-group/register';
|
|
52
55
|
// ─── Registry map ───────────────────────────────────────────────────────────
|
|
53
56
|
export const questionTypeRegistry = {
|
|
54
57
|
CHOOSE_THE_CORRECT_ANSWER: chooseTheCorrectAnswerRegistration,
|
|
@@ -101,4 +104,7 @@ export const questionTypeRegistry = {
|
|
|
101
104
|
WORD_FILL_STRUCTURED_FORM: wordFillStructuredFormRegistration,
|
|
102
105
|
CROSSWORD_PUZZLE: crosswordPuzzleRegistration,
|
|
103
106
|
FIND_WORDS_IN_MATRIX: findWordsInMatrixRegistration,
|
|
107
|
+
MATCH_COLUMNS_TO_MAKE_SENTENCES: matchColumnsToMakeSentencesRegistration,
|
|
108
|
+
MATCH_WORD_TO_PICTURE_GROUP: matchWordToPictureGroupRegistration,
|
|
109
|
+
WORD_ORDERING_GROUP: wordOrderingGroupRegistration,
|
|
104
110
|
};
|
|
@@ -34,7 +34,7 @@ export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode =
|
|
|
34
34
|
next[itemIndex] = optionId;
|
|
35
35
|
onAnswerChange(next);
|
|
36
36
|
};
|
|
37
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), questionData.audioUrl && _jsx(ClientAudio, { url: questionData.audioUrl }), questionData.passage && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none text-gray-800 leading-relaxed", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
|
|
37
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), questionData.audioUrl && _jsx(ClientAudio, { url: questionData.audioUrl }), toChooseAnswerGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh nhóm (${imageIndex + 1})` }, `group-image-${imageIndex}`))) })), questionData.passage && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none text-gray-800 leading-relaxed", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
|
|
38
38
|
const selectedId = userAnswers[itemIndex];
|
|
39
39
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
40
40
|
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), item.audioUrl && _jsx(ClientAudio, { url: item.audioUrl }), toChooseAnswerGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), item.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: item.options.map((option, optionIndex) => {
|
|
@@ -70,6 +70,7 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
70
70
|
const [instruction, setInstruction] = useState(initialData?.instruction || 'Choose the correct answer.');
|
|
71
71
|
const [passage, setPassage] = useState(initialData?.passage || '');
|
|
72
72
|
const [audioUrl, setAudioUrl] = useState(initialData?.audioUrl || '');
|
|
73
|
+
const [imageUrl, setImageUrl] = useState(toChooseAnswerGroupImageUrls(initialData?.imageUrl)[0] || '');
|
|
73
74
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
74
75
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
75
76
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
@@ -80,10 +81,11 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
80
81
|
onChangeRef.current = onChange;
|
|
81
82
|
}, [onChange]);
|
|
82
83
|
const isContentEmpty = (html) => !html || html === '<p></p>' || html === '<p><br></p>' || html.trim() === '';
|
|
83
|
-
const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation, nextAudioUrl = audioUrl) => ({
|
|
84
|
+
const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation, nextAudioUrl = audioUrl, nextImageUrl = imageUrl) => ({
|
|
84
85
|
instruction: nextInstruction,
|
|
85
86
|
...(!isContentEmpty(nextPassage) ? { passage: nextPassage.trim() } : {}),
|
|
86
87
|
...(nextAudioUrl.trim() ? { audioUrl: nextAudioUrl.trim() } : {}),
|
|
88
|
+
...(nextImageUrl.trim() ? { imageUrl: nextImageUrl.trim() } : {}),
|
|
87
89
|
items: nextItems,
|
|
88
90
|
explanation: nextExplanation,
|
|
89
91
|
points: sumPoints(nextItems),
|
|
@@ -100,7 +102,7 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
100
102
|
previousPayloadRef.current = serialized;
|
|
101
103
|
onUnsavedChangesChange?.(true);
|
|
102
104
|
debouncedOnChange(payload);
|
|
103
|
-
}, [instruction, passage, audioUrl, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
105
|
+
}, [instruction, passage, audioUrl, imageUrl, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
104
106
|
const validate = () => {
|
|
105
107
|
const nextErrors = [];
|
|
106
108
|
if (!instruction.trim()) {
|
|
@@ -165,7 +167,7 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
165
167
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Ch\u1EC9nh s\u1EEDa"] }) }), _jsx(ChooseTheCorrectAnswerGroupClient, { questionData: buildPayload(), isReviewMode: true })] }));
|
|
166
168
|
}
|
|
167
169
|
const allErrors = [...errors, ...(externalErrors || [])];
|
|
168
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(true), children: [_jsx(Eye, { className: "mr-2 h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] }) }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Nh\u00F3m ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "group-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "group-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: "Choose the correct answer." })] }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: "choose-answer-group-audio", label: "Audio chung (tu\u1EF3 ch\u1ECDn)", accept: "audio/*", value: audioUrl, onChange: setAudioUrl, maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage - Tu\u1EF3 ch\u1ECDn)" }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] }), _jsxs("div", { className: "rounded-xl border border-gray-200 bg-gray-50 p-4", children: [_jsx(PointsInput, { value: sumPoints(items), allowZero: true, readOnly: true }), _jsx("p", { className: "mt-2 text-xs text-gray-500", children: "T\u1ED5ng \u0111i\u1EC3m \u0111\u01B0\u1EE3c c\u1ED9ng t\u1EEB c\u00E1c c\u00E2u kh\u00F4ng ph\u1EA3i example." })] })] })] }), items.map((item, itemIndex) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0", children: [_jsxs(CardTitle, { className: "text-base", children: ["C\u00E2u ", itemIndex + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeItem(itemIndex), disabled: items.length <= 1, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg border border-amber-100 bg-amber-50 px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-amber-800", children: [_jsx(Star, { className: "h-4 w-4" }), "C\u00E2u v\u00ED d\u1EE5"] }), _jsx(Switch, { checked: item.isExample === true, onCheckedChange: (checked) => updateItem(itemIndex, {
|
|
170
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(true), children: [_jsx(Eye, { className: "mr-2 h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] }) }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Nh\u00F3m ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "group-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "group-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: "Choose the correct answer." })] }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: "choose-answer-group-audio", label: "Audio chung (tu\u1EF3 ch\u1ECDn)", accept: "audio/*", value: audioUrl, onChange: setAudioUrl, maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: "choose-answer-group-image", label: "H\u00ECnh \u1EA3nh chung (tu\u1EF3 ch\u1ECDn)", accept: "image/*", value: imageUrl, onChange: setImageUrl, maxSize: 5, placeholder: "Upload \u1EA3nh nh\u00F3m ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }) }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage - Tu\u1EF3 ch\u1ECDn)" }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] }), _jsxs("div", { className: "rounded-xl border border-gray-200 bg-gray-50 p-4", children: [_jsx(PointsInput, { value: sumPoints(items), allowZero: true, readOnly: true }), _jsx("p", { className: "mt-2 text-xs text-gray-500", children: "T\u1ED5ng \u0111i\u1EC3m \u0111\u01B0\u1EE3c c\u1ED9ng t\u1EEB c\u00E1c c\u00E2u kh\u00F4ng ph\u1EA3i example." })] })] })] }), items.map((item, itemIndex) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0", children: [_jsxs(CardTitle, { className: "text-base", children: ["C\u00E2u ", itemIndex + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeItem(itemIndex), disabled: items.length <= 1, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg border border-amber-100 bg-amber-50 px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-amber-800", children: [_jsx(Star, { className: "h-4 w-4" }), "C\u00E2u v\u00ED d\u1EE5"] }), _jsx(Switch, { checked: item.isExample === true, onCheckedChange: (checked) => updateItem(itemIndex, {
|
|
169
171
|
isExample: checked,
|
|
170
172
|
points: checked ? 0 : (item.points || 1),
|
|
171
173
|
}) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "C\u00E2u h\u1ECFi" }), _jsx(Textarea, { value: item.question, onChange: (event) => updateItem(itemIndex, { question: event.target.value }), rows: 2, placeholder: "A place for someone to stay or live is called" })] }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: `choose-answer-item-audio-${itemIndex}`, label: "Audio c\u00E2u h\u1ECFi (tu\u1EF3 ch\u1ECDn)", accept: "audio/*", value: item.audioUrl || '', onChange: (url) => updateItem(itemIndex, { audioUrl: url }), maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { className: "flex items-center gap-2", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-teal-500" }), "H\u00ECnh \u1EA3nh (tu\u1EF3 ch\u1ECDn)"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
|