@tinyweb_dev/oe-exam-sdk 0.2.3 → 0.2.5
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/components/QuestionRenderer.js +11 -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/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/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 +2 -0
- package/dist/components/exams/take/components/question-renderers/index.js +2 -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 +12 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +29 -1
- package/dist/components/exams/take/utils/question-transformers.js +98 -0
- package/dist/components/questions/_shared/config/question-types.config.js +12 -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/index.d.ts +2 -0
- package/dist/components/questions/_shared/types/index.js +2 -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/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 +2 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +1 -0
- package/dist/components/questions/creator/question-type-registry.js +4 -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/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/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 +27 -3
- 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/AnswerTheQuestionGroupViewer.d.ts +8 -0
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +11 -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/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/QuestionViewer.js +58 -9
- 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 +4 -0
- package/dist/components/questions/viewer/index.js +4 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
- package/dist/components/results/renderers/review-question-body-movers.js +39 -3
- package/dist/components/results/review-data.js +22 -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 +4 -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 +111 -2
- package/dist/shared/lib/utils/question-transform.js +4 -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 +2 -0
- package/dist/shared/types/questions/index.js +4 -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/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
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
3
|
-
import { Check, X
|
|
4
|
-
import { useState } from 'react';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, X } from 'lucide-react';
|
|
5
4
|
import { ResolvedImage } from '../../../components/common/ResolvedImage';
|
|
6
5
|
import CambridgeYlePartBanner from '../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
7
6
|
import CambridgeYleInstructionBanner from '../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
8
7
|
import CambridgeYleAudioPlayer from '../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
|
|
9
|
-
|
|
10
|
-
function QuestionImage({ imageUrl }) {
|
|
11
|
-
const [isZoomed, setIsZoomed] = useState(false);
|
|
12
|
-
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 }) }))] }));
|
|
13
|
-
}
|
|
8
|
+
import { ChooseBestAnswerImages, getChooseBestAnswerOptionImageSrc } from '../../../components/exams/take/components/question-renderers/ChooseBestAnswerImages';
|
|
14
9
|
// Single Question Component for Review Mode
|
|
15
10
|
function ReviewQuestionItem({ question, selectedOptionIds, isCorrect, correctOptionIds, }) {
|
|
16
11
|
const hasStudentAnswered = selectedOptionIds.length > 0;
|
|
@@ -48,7 +43,7 @@ function ReviewQuestionItem({ question, selectedOptionIds, isCorrect, correctOpt
|
|
|
48
43
|
}
|
|
49
44
|
return _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 border-gray-300 bg-white ${shapeClass}` });
|
|
50
45
|
};
|
|
51
|
-
return (_jsxs("div", { className: "mb-4", children: [_jsxs("p", { className: "mb-2 text-lg font-bold text-gray-900", children: [question.questionNumber, ". ", 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" })),
|
|
46
|
+
return (_jsxs("div", { className: "mb-4", children: [_jsxs("p", { className: "mb-2 text-lg font-bold text-gray-900", children: [question.questionNumber, ". ", 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" }), isImageOptions ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("div", { className: `relative flex flex-col items-center gap-1 rounded-lg border-2 p-2 cursor-default ${getOptionStyle(option.id)}`, children: [_jsx("span", { className: "text-xs font-medium text-gray-600", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "h-28 w-28 rounded object-contain" }), renderIndicator(option.id)] }, option.id))) })) : (
|
|
52
47
|
/* Options — text variant */
|
|
53
48
|
_jsx("div", { className: "flex flex-col gap-2 pl-2", children: question.options.map((option) => (_jsxs("div", { className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 cursor-default ${getOptionStyle(option.id)}`, 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: option.text }), renderIndicator(option.id)] }, option.id))) }))] }));
|
|
54
49
|
}
|
|
@@ -4,8 +4,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
4
4
|
* Movers fallback review cases (isReviewMode=true).
|
|
5
5
|
* Source: oe-exam-fe ReviewQuestionRenderer.tsx
|
|
6
6
|
*/
|
|
7
|
-
import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, } from '../../../components/exams/take/components/question-renderers';
|
|
8
|
-
import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, } from '../../../components/exams/take/utils/question-transformers';
|
|
7
|
+
import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, } from '../../../components/exams/take/components/question-renderers';
|
|
8
|
+
import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, } from '../../../components/exams/take/utils/question-transformers';
|
|
9
9
|
import { normalizeCrossOutWordGroupAnswer, transformCrossOutWordGroup, } from '../../../components/exams/take/utils/cross-out-word-group.transformer';
|
|
10
10
|
function asApiQuestions(questions) {
|
|
11
11
|
return questions;
|
|
@@ -26,7 +26,7 @@ export function renderMoversReviewBody(ctx) {
|
|
|
26
26
|
const data = transformWriteShortLetter(firstQuestion);
|
|
27
27
|
const writingAnswer = answers[firstQuestion.id] || '';
|
|
28
28
|
const gradingInfo = essayGradingMap[firstQuestion.id];
|
|
29
|
-
return (_jsx(MoversWritingRenderer, { partNumber: partNo, partName: part.name, prompt: data.prompt, requirements: data.requirements, minWords: data.minWords, maxWords: data.maxWords, answer: writingAnswer, onAnswerChange: handleAnswerChange, isReviewMode: true, teacherScore: gradingInfo?.score, maxScore: gradingInfo?.maxScore, teacherFeedback: gradingInfo?.teacherFeedback, writingAssessment: gradingInfo?.writingAssessment }));
|
|
29
|
+
return (_jsx(MoversWritingRenderer, { partNumber: partNo, partName: part.name, prompt: data.prompt, requirements: data.requirements, minWords: data.minWords, maxWords: data.maxWords, answer: writingAnswer, onAnswerChange: handleAnswerChange, isReviewMode: true, guideImages: data.guideImages, passage: data.passage, teacherScore: gradingInfo?.score, maxScore: gradingInfo?.maxScore, teacherFeedback: gradingInfo?.teacherFeedback, writingAssessment: gradingInfo?.writingAssessment }));
|
|
30
30
|
}
|
|
31
31
|
case 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER': {
|
|
32
32
|
const data = transformPictureChoose(apiQuestions);
|
|
@@ -110,6 +110,38 @@ export function renderMoversReviewBody(ctx) {
|
|
|
110
110
|
});
|
|
111
111
|
return (_jsx(MoversCrossOutWordGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerMap: correctAnswerMap }));
|
|
112
112
|
}
|
|
113
|
+
case 'TRUE_FALSE_CORRECT_GROUP': {
|
|
114
|
+
const data = transformTrueFalseCorrectGroup(apiQuestions);
|
|
115
|
+
const correctAnswerMap = {};
|
|
116
|
+
data.questions.forEach((question) => {
|
|
117
|
+
if (question.exampleCorrectAnswer) {
|
|
118
|
+
correctAnswerMap[question.id] = question.exampleCorrectAnswer;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const parent = apiQuestions[0];
|
|
122
|
+
const rawCorrect = parent?.id ? rawCorrectAnswerMap[parent.id] : undefined;
|
|
123
|
+
const rawCorrectRecord = rawCorrect && typeof rawCorrect === 'object' && !Array.isArray(rawCorrect)
|
|
124
|
+
? rawCorrect
|
|
125
|
+
: undefined;
|
|
126
|
+
const correctArr = Array.isArray(rawCorrectRecord?.answer)
|
|
127
|
+
? rawCorrectRecord.answer
|
|
128
|
+
: Array.isArray(parent?.correct_answer?.answer)
|
|
129
|
+
? parent.correct_answer.answer
|
|
130
|
+
: [];
|
|
131
|
+
data.questions.forEach((question, index) => {
|
|
132
|
+
if (!question.isExample && correctArr[index] && typeof correctArr[index] === 'object') {
|
|
133
|
+
const obj = correctArr[index];
|
|
134
|
+
const isTrue = obj.isTrue === true ? true : obj.isTrue === false ? false : null;
|
|
135
|
+
if (isTrue !== null) {
|
|
136
|
+
correctAnswerMap[question.id] = {
|
|
137
|
+
isTrue,
|
|
138
|
+
correction: typeof obj.correction === 'string' ? obj.correction : '',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return (_jsx(MoversTrueFalseCorrectGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerMap: correctAnswerMap }));
|
|
144
|
+
}
|
|
113
145
|
case 'TRUE_FALSE': {
|
|
114
146
|
const data = transformTrueFalse(apiQuestions);
|
|
115
147
|
return (_jsx(MoversTrueFalseRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, imageUrl: data.imageUrl, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerTextMap: correctAnswerTextMap }));
|
|
@@ -118,6 +150,10 @@ export function renderMoversReviewBody(ctx) {
|
|
|
118
150
|
const data = transformMatchByWritingAnswer(apiQuestions);
|
|
119
151
|
return (_jsx(MoversMatchByWritingAnswerRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, audioUrl: part.audioUrl, options: data.options, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, viewMode: data.viewMode, groupImageUrl: data.groupImageUrl, correctAnswerTextMap: correctAnswerTextMap, isCorrectMap: isCorrectMap }));
|
|
120
152
|
}
|
|
153
|
+
case 'MATCH_WORD_TO_PICTURE': {
|
|
154
|
+
const data = transformMatchWordToPicture(apiQuestions);
|
|
155
|
+
return (_jsx(MoversMatchWordToPictureRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, wordBank: data.wordBank, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerTextMap: correctAnswerTextMap, isCorrectMap: isCorrectMap }));
|
|
156
|
+
}
|
|
121
157
|
case 'WORD_ORDERING': {
|
|
122
158
|
const data = transformWordOrdering(apiQuestions);
|
|
123
159
|
return (_jsx(MoversWordOrderingRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, rawCorrectAnswerMap: rawCorrectAnswerMap, isCorrectMap: isCorrectMap }));
|
|
@@ -8,6 +8,7 @@ const FLATTENED_ANSWER_TYPES = new Set([
|
|
|
8
8
|
'CHOOSE_THE_CORRECT_ANSWER_GROUP',
|
|
9
9
|
'ANSWER_THE_QUESTION_GROUP',
|
|
10
10
|
'CROSS_OUT_WORD_GROUP',
|
|
11
|
+
'TRUE_FALSE_CORRECT_GROUP',
|
|
11
12
|
]);
|
|
12
13
|
export function transformResultAnswersForReview(answers, templateParts = []) {
|
|
13
14
|
const groups = groupAnswers(answers);
|
|
@@ -183,6 +184,27 @@ function transformStudentAnswer(questionId, questionType, value) {
|
|
|
183
184
|
normalizeCrossOutWordGroupAnswer(item),
|
|
184
185
|
]));
|
|
185
186
|
}
|
|
187
|
+
if (questionType === 'TRUE_FALSE_CORRECT_GROUP') {
|
|
188
|
+
const answer = Array.isArray(record.answer)
|
|
189
|
+
? record.answer
|
|
190
|
+
: Array.isArray(value)
|
|
191
|
+
? value
|
|
192
|
+
: [];
|
|
193
|
+
return Object.fromEntries(answer.map((item, index) => {
|
|
194
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
195
|
+
const obj = item;
|
|
196
|
+
const isTrue = obj.isTrue === true ? true : obj.isTrue === false ? false : null;
|
|
197
|
+
return [
|
|
198
|
+
`${questionId}-item-${index}`,
|
|
199
|
+
{
|
|
200
|
+
isTrue,
|
|
201
|
+
correction: typeof obj.correction === 'string' ? obj.correction : '',
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
return [`${questionId}-item-${index}`, { isTrue: null, correction: '' }];
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
186
208
|
if (questionType === 'CHOOSE_THE_CORRECT_ANSWER' || questionType === 'CHOOSE_CORRECT_ADJECTIVE') {
|
|
187
209
|
const rawAns = record.answer ?? record.optionId ?? record.optionIds;
|
|
188
210
|
const ids = getOptionIds(questionId, rawAns);
|
|
@@ -46,7 +46,7 @@ export function EcWritingTask({ part, answers, onAnswerChange, isReviewMode = fa
|
|
|
46
46
|
if (!firstQuestion || !data) {
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
|
-
const { prompt, requirements, guideImages } = data;
|
|
49
|
+
const { prompt, requirements, guideImages, passage } = data;
|
|
50
50
|
const meetsMin = minWords ? wordCount >= minWords : true;
|
|
51
51
|
const withinMax = maxWords ? wordCount <= maxWords : true;
|
|
52
52
|
const isValid = meetsMin && withinMax;
|
|
@@ -54,7 +54,7 @@ export function EcWritingTask({ part, answers, onAnswerChange, isReviewMode = fa
|
|
|
54
54
|
const progressPct = progressTarget
|
|
55
55
|
? Math.min(100, Math.round((wordCount / progressTarget) * 100))
|
|
56
56
|
: 0;
|
|
57
|
-
return (_jsxs("div", { id: englishCertQuestionDomId(firstQuestion.id), className: "scroll-mt-24 flex flex-col gap-5", children: [_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "overflow-hidden rounded-2xl border border-[#cfe0f7] bg-white shadow-[0_18px_40px_rgba(13,71,161,0.10)]", children: [prompt && (_jsx(HtmlOrText, { value: prompt, as: "div", className: "whitespace-pre-line px-5 pb-4 pt-5 text-base leading-relaxed text-[#0f1730]" })), requirements && requirements.length > 0 && (_jsx("div", { className: cn('px-5 pb-4', !prompt && 'pt-5'), children: _jsxs("div", { className: "rounded-xl border border-[#e3edfb] bg-[#f6faff] p-4", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-wider text-[#0b57d0]", children: [_jsx(ListChecks, { className: "h-3.5 w-3.5" }), "Requirements"] }), _jsx("ul", { className: "mt-2 list-inside list-disc space-y-1.5 text-sm text-[#0f1730]", children: requirements.map((req, idx) => (_jsx("li", { children: _jsx(HtmlOrText, { value: req, as: "span" }) }, idx))) })] }) })), minWords && (_jsxs("div", { className: "border-t border-[#e3edfb] bg-[#f9fbfe] px-5 py-3 text-sm font-semibold text-[#0f1730]", children: ["Write at least ", minWords, " words."] }))] }), guideImages && guideImages.length > 0 && (_jsx("div", { className: "flex flex-col items-center justify-center gap-4 rounded-2xl border border-[#d9e4f2] bg-white/60 p-4", children: guideImages.map((img, idx) => (_jsx(ResolvedImage, { src: img, alt: `Guide ${idx + 1}`, className: "w-full max-w-3xl rounded-xl bg-white object-contain shadow-sm" }, idx))) }))] }), _jsxs("div", { className: "overflow-hidden rounded-2xl border border-[#cfe0f7] bg-white shadow-[0_18px_40px_rgba(13,71,161,0.10)]", children: [_jsxs("div", { className: "flex items-center gap-1.5 border-b border-[#e3edfb] bg-[#f6faff] px-4 py-2.5 text-xs font-bold uppercase tracking-wider text-[#0b57d0]", children: [_jsx(PenLine, { className: "h-3.5 w-3.5" }), "Your answer"] }), _jsx("textarea", { value: answer, disabled: isReviewMode, placeholder: "Write your essay here\u2026", onChange: (e) => onAnswerChange(firstQuestion.id, e.target.value), rows: 14, className: cn('w-full resize-y border-0 bg-white px-5 py-4 text-sm leading-relaxed text-[#0f1730] outline-none transition-all placeholder:text-[#9aa9c2] focus:bg-[#fbfdff]', isReviewMode && 'cursor-not-allowed opacity-80') }), _jsxs("div", { className: "flex flex-col gap-2 border-t border-[#e3edfb] bg-[#f9fbfe] px-4 py-3", children: [_jsxs("div", { className: "flex items-center justify-between text-xs", children: [_jsxs("span", { className: cn('inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 font-semibold', isValid
|
|
57
|
+
return (_jsxs("div", { id: englishCertQuestionDomId(firstQuestion.id), className: "scroll-mt-24 flex flex-col gap-5", children: [_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "overflow-hidden rounded-2xl border border-[#cfe0f7] bg-white shadow-[0_18px_40px_rgba(13,71,161,0.10)]", children: [prompt && (_jsx(HtmlOrText, { value: prompt, as: "div", className: "whitespace-pre-line px-5 pb-4 pt-5 text-base leading-relaxed text-[#0f1730]" })), passage?.trim() && (_jsx("div", { className: "px-5 pb-4", children: _jsxs("div", { className: "rounded-xl border border-[#e3edfb] bg-[#f6faff] p-4", children: [_jsx("p", { className: "text-[11px] font-bold uppercase tracking-wider text-[#0b57d0]", children: "Read" }), _jsx(HtmlOrText, { value: passage, as: "div", className: "mt-2 text-sm leading-relaxed text-[#0f1730]" })] }) })), requirements && requirements.length > 0 && (_jsx("div", { className: cn('px-5 pb-4', !prompt && 'pt-5'), children: _jsxs("div", { className: "rounded-xl border border-[#e3edfb] bg-[#f6faff] p-4", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-wider text-[#0b57d0]", children: [_jsx(ListChecks, { className: "h-3.5 w-3.5" }), "Requirements"] }), _jsx("ul", { className: "mt-2 list-inside list-disc space-y-1.5 text-sm text-[#0f1730]", children: requirements.map((req, idx) => (_jsx("li", { children: _jsx(HtmlOrText, { value: req, as: "span" }) }, idx))) })] }) })), minWords && (_jsxs("div", { className: "border-t border-[#e3edfb] bg-[#f9fbfe] px-5 py-3 text-sm font-semibold text-[#0f1730]", children: ["Write at least ", minWords, " words."] }))] }), guideImages && guideImages.length > 0 && (_jsx("div", { className: "flex flex-col items-center justify-center gap-4 rounded-2xl border border-[#d9e4f2] bg-white/60 p-4", children: guideImages.map((img, idx) => (_jsx(ResolvedImage, { src: img, alt: `Guide ${idx + 1}`, className: "w-full max-w-3xl rounded-xl bg-white object-contain shadow-sm" }, idx))) }))] }), _jsxs("div", { className: "overflow-hidden rounded-2xl border border-[#cfe0f7] bg-white shadow-[0_18px_40px_rgba(13,71,161,0.10)]", children: [_jsxs("div", { className: "flex items-center gap-1.5 border-b border-[#e3edfb] bg-[#f6faff] px-4 py-2.5 text-xs font-bold uppercase tracking-wider text-[#0b57d0]", children: [_jsx(PenLine, { className: "h-3.5 w-3.5" }), "Your answer"] }), _jsx("textarea", { value: answer, disabled: isReviewMode, placeholder: "Write your essay here\u2026", onChange: (e) => onAnswerChange(firstQuestion.id, e.target.value), rows: 14, className: cn('w-full resize-y border-0 bg-white px-5 py-4 text-sm leading-relaxed text-[#0f1730] outline-none transition-all placeholder:text-[#9aa9c2] focus:bg-[#fbfdff]', isReviewMode && 'cursor-not-allowed opacity-80') }), _jsxs("div", { className: "flex flex-col gap-2 border-t border-[#e3edfb] bg-[#f9fbfe] px-4 py-3", children: [_jsxs("div", { className: "flex items-center justify-between text-xs", children: [_jsxs("span", { className: cn('inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 font-semibold', isValid
|
|
58
58
|
? 'bg-[#ddf8e7] text-[#15803d]'
|
|
59
59
|
: 'bg-[#fde8e8] text-[#ef4444]'), children: [wordCount, " words"] }), (minWords || maxWords) && (_jsxs("span", { className: "font-medium text-[#64748b]", children: [minWords ? `Min ${minWords}` : '', minWords && maxWords ? ' · ' : '', maxWords ? `Max ${maxWords}` : ''] }))] }), progressTarget > 0 && (_jsx("div", { className: "h-1.5 w-full overflow-hidden rounded-full bg-[#e3edfb]", children: _jsx("div", { className: cn('h-full rounded-full transition-all', isValid ? 'bg-[#15803d]' : 'bg-[#1167e8]'), style: { width: `${progressPct}%` } }) })), statusMessage && (_jsxs("div", { className: cn('flex items-center gap-1.5 text-xs font-medium', statusMessage.ok ? 'text-[#15803d]' : 'text-[#64748b]'), children: [statusMessage.ok ? (_jsx(CheckCircle2, { className: "h-3.5 w-3.5 shrink-0" })) : (_jsx(AlertCircle, { className: "h-3.5 w-3.5 shrink-0" })), statusMessage.text] }))] })] })] }));
|
|
60
60
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -26,4 +26,4 @@ export { validateQuestion } from './components/questions/_shared/utils/question-
|
|
|
26
26
|
export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
|
|
27
27
|
export type { QuestionFormState } from './components/questions/_shared/hooks/useQuestionFormState';
|
|
28
28
|
export * from './components/questions/_shared/types';
|
|
29
|
-
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, } from './components/questions/viewer';
|
|
29
|
+
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, } from './components/questions/viewer';
|
package/dist/index.js
CHANGED
|
@@ -21,4 +21,4 @@ export { QUESTION_TYPE_CONFIG, QUESTION_TYPES_WITH_DEDICATED_COMPONENTS } from '
|
|
|
21
21
|
export { validateQuestion } from './components/questions/_shared/utils/question-validation';
|
|
22
22
|
export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
|
|
23
23
|
export * from './components/questions/_shared/types';
|
|
24
|
-
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, } from './components/questions/viewer';
|
|
24
|
+
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, } from './components/questions/viewer';
|
|
@@ -52,6 +52,7 @@ export const QUESTION_TYPE_SKILLS = {
|
|
|
52
52
|
'WORD_ORDERING': SkillEnum.READING,
|
|
53
53
|
'WORD_FILL_PARAGRAPH': SkillEnum.READING,
|
|
54
54
|
'MATCH_BY_WRITING_ANSWER': SkillEnum.READING,
|
|
55
|
+
'MATCH_WORD_TO_PICTURE': SkillEnum.READING,
|
|
55
56
|
// Visual-based reading questions
|
|
56
57
|
'LABEL_THE_PICTURE': SkillEnum.READING,
|
|
57
58
|
'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER': SkillEnum.READING,
|
|
@@ -62,6 +63,7 @@ export const QUESTION_TYPE_SKILLS = {
|
|
|
62
63
|
// Other reading types
|
|
63
64
|
'TRUE_FALSE': SkillEnum.READING,
|
|
64
65
|
'TRUE_FALSE_GROUP': SkillEnum.READING,
|
|
66
|
+
'TRUE_FALSE_CORRECT_GROUP': SkillEnum.READING,
|
|
65
67
|
'CROSS_OUT_WORD_GROUP': SkillEnum.READING,
|
|
66
68
|
'SPONTANEOUS_QA_GROUP': SkillEnum.SPEAKING,
|
|
67
69
|
'WORD_ORDER_AND_MATCH_GROUP': SkillEnum.READING,
|
|
@@ -185,6 +187,7 @@ export const QUESTION_TYPE_LABELS = {
|
|
|
185
187
|
'IMAGE_OBJECT_MATCHING': 'Ghép hình ảnh - Đối tượng',
|
|
186
188
|
'TRUE_FALSE': 'Đúng/Sai',
|
|
187
189
|
'TRUE_FALSE_GROUP': 'Đúng/Sai (nhóm)',
|
|
190
|
+
'TRUE_FALSE_CORRECT_GROUP': 'Đúng/Sai + sửa câu (nhóm)',
|
|
188
191
|
'CROSS_OUT_WORD_GROUP': 'Gạch từ / sửa từ (nhóm)',
|
|
189
192
|
'SPONTANEOUS_QA_GROUP': 'Hỏi đáp tự phát (nhóm)',
|
|
190
193
|
'WORD_ORDER_AND_MATCH_GROUP': 'Sắp xếp từ và nối nhãn (nhóm)',
|
|
@@ -205,6 +208,7 @@ export const QUESTION_TYPE_LABELS = {
|
|
|
205
208
|
'WORD_ORDERING': 'Sắp xếp từ',
|
|
206
209
|
'WORD_FILL_PARAGRAPH': 'Điền từ vào đoạn văn',
|
|
207
210
|
'MATCH_BY_WRITING_ANSWER': 'Ghép đáp án bằng cách viết',
|
|
211
|
+
'MATCH_WORD_TO_PICTURE': 'Nối từ với tranh',
|
|
208
212
|
'WORD_FILL_STRUCTURED_FORM': 'Điền từ vào biểu mẫu có cấu trúc',
|
|
209
213
|
'CROSSWORD_PUZZLE': 'Giải ô chữ (Crossword)',
|
|
210
214
|
'FIND_WORDS_IN_MATRIX': 'Tìm từ trong ma trận (Word Search)',
|
|
@@ -9,7 +9,9 @@ export interface PresignedUrlResponse {
|
|
|
9
9
|
* - Empty/undefined → empty string.
|
|
10
10
|
* - Already a URL (http/https/blob/data) → returned unchanged.
|
|
11
11
|
* - An S3 storage key (e.g. `exams/:id/audio/x.wav`) → resolved to a presigned
|
|
12
|
-
* URL via the host API
|
|
12
|
+
* URL via the host API. Tries `/api/v1/exam-storage/:key` (oe-class proxy)
|
|
13
|
+
* then `/api/v1/storage/:key` (oe-exam-api). Override with
|
|
14
|
+
* `NEXT_PUBLIC_EXAM_STORAGE_PATH`.
|
|
13
15
|
*/
|
|
14
16
|
export declare function usePresignedFileUrl(fileKey: string | undefined): {
|
|
15
17
|
previewUrl: string;
|
|
@@ -6,7 +6,14 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
6
6
|
* owns them. Reads NEXT_PUBLIC_API_URL — inlined at build time in the host.
|
|
7
7
|
*/
|
|
8
8
|
const HOST_API_BASE_URL = (process.env.NEXT_PUBLIC_API_URL ?? '').replace(/\/+$/, '');
|
|
9
|
-
const
|
|
9
|
+
const DEFAULT_STORAGE_PATHS = ['/api/v1/exam-storage', '/api/v1/storage'];
|
|
10
|
+
function getStoragePaths() {
|
|
11
|
+
const configured = process.env.NEXT_PUBLIC_EXAM_STORAGE_PATH;
|
|
12
|
+
const paths = configured
|
|
13
|
+
? [configured, ...DEFAULT_STORAGE_PATHS]
|
|
14
|
+
: DEFAULT_STORAGE_PATHS;
|
|
15
|
+
return [...new Set(paths.map((path) => path.replace(/\/+$/, '')).filter(Boolean))];
|
|
16
|
+
}
|
|
10
17
|
/** A resolved key is displayable as-is when it's already a URL or inline data. */
|
|
11
18
|
function isDisplayableUrl(value) {
|
|
12
19
|
return (value.startsWith('http://') ||
|
|
@@ -33,17 +40,26 @@ async function resolvePresignedUrl(key) {
|
|
|
33
40
|
// caller at least doesn't crash. It won't load, but nothing throws.
|
|
34
41
|
return key;
|
|
35
42
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
let lastError = null;
|
|
44
|
+
for (const storagePath of getStoragePaths()) {
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(`${HOST_API_BASE_URL}${storagePath}/${encodeURIComponent(key)}`, { credentials: 'include' });
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`Failed to resolve file URL (${response.status})`);
|
|
49
|
+
}
|
|
50
|
+
const body = (await response.json());
|
|
51
|
+
const resolved = 'data' in body ? body.data?.url : body.url;
|
|
52
|
+
if (!resolved) {
|
|
53
|
+
throw new Error('Presigned URL missing in response');
|
|
54
|
+
}
|
|
55
|
+
urlCache.set(key, resolved);
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
60
|
+
}
|
|
44
61
|
}
|
|
45
|
-
|
|
46
|
-
return resolved;
|
|
62
|
+
throw lastError ?? new Error('Failed to resolve file URL');
|
|
47
63
|
})().finally(() => {
|
|
48
64
|
inFlight.delete(key);
|
|
49
65
|
});
|
|
@@ -56,7 +72,9 @@ async function resolvePresignedUrl(key) {
|
|
|
56
72
|
* - Empty/undefined → empty string.
|
|
57
73
|
* - Already a URL (http/https/blob/data) → returned unchanged.
|
|
58
74
|
* - An S3 storage key (e.g. `exams/:id/audio/x.wav`) → resolved to a presigned
|
|
59
|
-
* URL via the host API
|
|
75
|
+
* URL via the host API. Tries `/api/v1/exam-storage/:key` (oe-class proxy)
|
|
76
|
+
* then `/api/v1/storage/:key` (oe-exam-api). Override with
|
|
77
|
+
* `NEXT_PUBLIC_EXAM_STORAGE_PATH`.
|
|
60
78
|
*/
|
|
61
79
|
export function usePresignedFileUrl(fileKey) {
|
|
62
80
|
const safeKey = typeof fileKey === 'string' ? fileKey : '';
|
|
@@ -145,6 +145,23 @@ const reverseGroupPayload = (apiQuestion) => {
|
|
|
145
145
|
},
|
|
146
146
|
};
|
|
147
147
|
};
|
|
148
|
+
function normalizeChooseAnswerGroupImageUrl(value) {
|
|
149
|
+
if (typeof value === 'string') {
|
|
150
|
+
const trimmed = value.trim();
|
|
151
|
+
return trimmed !== '' ? trimmed : undefined;
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(value)) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const urls = value
|
|
157
|
+
.filter((url) => typeof url === 'string')
|
|
158
|
+
.map((url) => url.trim())
|
|
159
|
+
.filter((url) => url !== '');
|
|
160
|
+
if (urls.length === 0) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return urls.length === 1 ? urls[0] : urls;
|
|
164
|
+
}
|
|
148
165
|
const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
|
|
149
166
|
const content = asRecord(apiQuestion.content);
|
|
150
167
|
const answer = readAnswer(apiQuestion);
|
|
@@ -155,12 +172,15 @@ const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
|
|
|
155
172
|
const itemAnswer = asRecord(item.correctAnswer);
|
|
156
173
|
const options = asArray(itemContent.options).map((rawOption, optionIndex) => {
|
|
157
174
|
const option = asRecord(rawOption);
|
|
175
|
+
const imageUrl = asString(option.imageUrl);
|
|
158
176
|
return {
|
|
159
177
|
id: asString(option.id, `opt-${optionIndex + 1}`),
|
|
160
178
|
text: asString(option.text),
|
|
179
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
161
180
|
};
|
|
162
181
|
});
|
|
163
182
|
const isExample = item.isExample === true;
|
|
183
|
+
const imageUrl = normalizeChooseAnswerGroupImageUrl(itemContent.imageUrl);
|
|
164
184
|
return {
|
|
165
185
|
question: asString(itemContent.question),
|
|
166
186
|
optionType: asString(itemContent.optionType, 'text') === 'image' ? 'image' : 'text',
|
|
@@ -173,6 +193,7 @@ const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
|
|
|
173
193
|
isExample,
|
|
174
194
|
points: isExample ? 0 : asNumber(item.points, 1),
|
|
175
195
|
questionNumber: asNumber(item.questionNumber, index + 1),
|
|
196
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
176
197
|
};
|
|
177
198
|
});
|
|
178
199
|
return {
|
|
@@ -256,6 +277,37 @@ const reverseTrueFalseGroup = (apiQuestion) => {
|
|
|
256
277
|
},
|
|
257
278
|
};
|
|
258
279
|
};
|
|
280
|
+
const reverseTrueFalseCorrectGroup = (apiQuestion) => {
|
|
281
|
+
const content = asRecord(apiQuestion.content);
|
|
282
|
+
const answer = readAnswer(apiQuestion);
|
|
283
|
+
const fallbackAnswers = asArray(answer.answer);
|
|
284
|
+
const items = asArray(content.items).map((rawItem, index) => {
|
|
285
|
+
const item = asRecord(rawItem);
|
|
286
|
+
const itemContent = asRecord(item.content);
|
|
287
|
+
const itemAnswer = asRecord(asRecord(item.correctAnswer).answer);
|
|
288
|
+
const fallback = asRecord(fallbackAnswers[index]);
|
|
289
|
+
const rawAnswer = Object.keys(itemAnswer).length > 0 ? itemAnswer : fallback;
|
|
290
|
+
const isExample = item.isExample === true;
|
|
291
|
+
const isTrue = rawAnswer.isTrue === true;
|
|
292
|
+
return {
|
|
293
|
+
question: asString(itemContent.question),
|
|
294
|
+
isTrue,
|
|
295
|
+
correction: isTrue ? '' : asString(rawAnswer.correction),
|
|
296
|
+
isExample,
|
|
297
|
+
points: isExample ? 0 : asNumber(item.points, 1),
|
|
298
|
+
questionNumber: asNumber(item.questionNumber, index + 1),
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
return {
|
|
302
|
+
content: '',
|
|
303
|
+
answer: {
|
|
304
|
+
instruction: asString(asRecord(content.meta).instruction, 'Tick (✓) the true and cross (✗) the false sentences. Correct the false sentences.'),
|
|
305
|
+
items,
|
|
306
|
+
explanation: asString(apiQuestion.explanation),
|
|
307
|
+
points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
};
|
|
259
311
|
const reverseCrossOutWordGroup = (apiQuestion) => {
|
|
260
312
|
const content = asRecord(apiQuestion.content);
|
|
261
313
|
const answer = readAnswer(apiQuestion);
|
|
@@ -407,6 +459,7 @@ const handlers = {
|
|
|
407
459
|
CHOOSE_THE_CORRECT_ANSWER_GROUP: reverseChooseTheCorrectAnswerGroup,
|
|
408
460
|
ANSWER_THE_QUESTION_GROUP: reverseAnswerTheQuestionGroup,
|
|
409
461
|
TRUE_FALSE_GROUP: reverseTrueFalseGroup,
|
|
462
|
+
TRUE_FALSE_CORRECT_GROUP: reverseTrueFalseCorrectGroup,
|
|
410
463
|
CROSS_OUT_WORD_GROUP: reverseCrossOutWordGroup,
|
|
411
464
|
WORD_ORDER_AND_MATCH_GROUP: reverseWordOrderAndMatchGroup,
|
|
412
465
|
WORD_ORDER_AND_MATCH: reverseWordOrderAndMatchGroup,
|
|
@@ -424,8 +477,9 @@ const handlers = {
|
|
|
424
477
|
WORD_ORDERING: reverseSimpleAnswer,
|
|
425
478
|
WORD_FILL_PARAGRAPH: reverseSimpleAnswer,
|
|
426
479
|
MATCH_BY_WRITING_ANSWER: reverseGroupPayload,
|
|
427
|
-
|
|
428
|
-
|
|
480
|
+
MATCH_WORD_TO_PICTURE: reverseGroupPayload,
|
|
481
|
+
WRITE_SHORT_PARAGRAPH: reverseWriteShortParagraph,
|
|
482
|
+
WRITE_A_SHORT_LETTER: reverseWriteAShortLetter,
|
|
429
483
|
WRITE_CORRECT_VERB_FORM: reverseSimpleAnswer,
|
|
430
484
|
CHOOSE_CORRECT_ADJECTIVE: reverseSimpleAnswer,
|
|
431
485
|
WORD_FILL_STRUCTURED_FORM: reverseGroupPayload,
|
|
@@ -449,6 +503,61 @@ const handlers = {
|
|
|
449
503
|
CROSSWORD_PUZZLE: reverseCrosswordPuzzle,
|
|
450
504
|
FIND_WORDS_IN_MATRIX: reverseFindWordsInMatrix,
|
|
451
505
|
};
|
|
506
|
+
function reverseWriteAShortLetter(apiQuestion) {
|
|
507
|
+
const content = asRecord(apiQuestion.content);
|
|
508
|
+
const answer = readAnswer(apiQuestion);
|
|
509
|
+
const informations = asArray(content.informations ?? answer.informations).map((item) => asString(item)).filter(Boolean);
|
|
510
|
+
const guideImages = asArray(content.guideImages ?? answer.guideImages).map((item) => asString(item)).filter(Boolean);
|
|
511
|
+
const passage = asString(content.passage ?? answer.passage);
|
|
512
|
+
const contexts = asArray(content.contexts ?? answer.contexts);
|
|
513
|
+
const instructions = isRecord(content.instructions)
|
|
514
|
+
? content.instructions
|
|
515
|
+
: isRecord(answer.instructions)
|
|
516
|
+
? answer.instructions
|
|
517
|
+
: { mainInstruction: asString(content.instruction ?? content.prompt ?? answer.instruction ?? answer.prompt) };
|
|
518
|
+
const minWords = asNumber(content.minWords ?? answer.minWords, 20);
|
|
519
|
+
const maxWords = content.maxWords !== undefined && content.maxWords !== null
|
|
520
|
+
? asNumber(content.maxWords, 0) || null
|
|
521
|
+
: answer.maxWords !== undefined && answer.maxWords !== null
|
|
522
|
+
? asNumber(answer.maxWords, 0) || null
|
|
523
|
+
: null;
|
|
524
|
+
const resolved = {
|
|
525
|
+
instructions,
|
|
526
|
+
instruction: asString(instructions.mainInstruction || instructions.instruction),
|
|
527
|
+
informations,
|
|
528
|
+
guideImages,
|
|
529
|
+
...(passage ? { passage } : {}),
|
|
530
|
+
contexts,
|
|
531
|
+
minWords,
|
|
532
|
+
maxWords,
|
|
533
|
+
};
|
|
534
|
+
return {
|
|
535
|
+
content: resolved,
|
|
536
|
+
answer: resolved,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function reverseWriteShortParagraph(apiQuestion) {
|
|
540
|
+
const content = asRecord(apiQuestion.content);
|
|
541
|
+
const answer = readAnswer(apiQuestion);
|
|
542
|
+
const contexts = asArray(content.contexts ?? answer.contexts);
|
|
543
|
+
const instruction = asString(content.instruction ?? content.instructions?.mainInstruction ?? answer.instruction);
|
|
544
|
+
const minWords = asNumber(content.minWords ?? answer.minWords, 20);
|
|
545
|
+
const maxWords = content.maxWords !== undefined && content.maxWords !== null
|
|
546
|
+
? asNumber(content.maxWords, 0) || null
|
|
547
|
+
: answer.maxWords !== undefined && answer.maxWords !== null
|
|
548
|
+
? asNumber(answer.maxWords, 0) || null
|
|
549
|
+
: null;
|
|
550
|
+
const resolved = {
|
|
551
|
+
instruction,
|
|
552
|
+
contexts,
|
|
553
|
+
minWords,
|
|
554
|
+
maxWords,
|
|
555
|
+
};
|
|
556
|
+
return {
|
|
557
|
+
content: resolved,
|
|
558
|
+
answer: resolved,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
452
561
|
function reverseCrosswordPuzzle(apiQuestion) {
|
|
453
562
|
const apiContent = (apiQuestion.content || {});
|
|
454
563
|
const apiAnswer = (apiQuestion.correctAnswer || apiQuestion.correct_answer || apiQuestion.answer || {});
|
|
@@ -13,6 +13,7 @@ import { transformLabelThePicture } from '../../../components/questions/types/la
|
|
|
13
13
|
import { transformChooseTheCorrectAnswer } from '../../../components/questions/types/choose-the-correct-answer/transform';
|
|
14
14
|
import { transformChooseTheCorrectAnswerGroup } from '../../../components/questions/types/choose-the-correct-answer-group/transform';
|
|
15
15
|
import { transformCrossOutWordGroup } from '../../../components/questions/types/cross-out-word-group/transform';
|
|
16
|
+
import { transformTrueFalseCorrectGroup } from '../../../components/questions/types/true-false-correct-group/transform';
|
|
16
17
|
import { transformFillInBlank } from '../../../components/questions/types/fill-in-blank/transform';
|
|
17
18
|
import { transformWriteCorrectVerbForm } from '../../../components/questions/types/write-correct-verb-form/transform';
|
|
18
19
|
import { transformChooseCorrectAdjective } from '../../../components/questions/types/choose-correct-adjective/transform';
|
|
@@ -22,6 +23,7 @@ import { transformTrueFalse } from '../../../components/questions/types/true-fal
|
|
|
22
23
|
import { transformWordOrdering } from '../../../components/questions/types/word-ordering/transform';
|
|
23
24
|
import { transformWordFillParagraph } from '../../../components/questions/types/word-fill-paragraph/transform';
|
|
24
25
|
import { transformMatchByWritingAnswer } from '../../../components/questions/types/match-by-writing-answer/transform';
|
|
26
|
+
import { transformMatchWordToPicture } from '../../../components/questions/types/match-word-to-picture/transform';
|
|
25
27
|
import { transformWriteSentences } from '../../../components/questions/types/write-sentences/transform';
|
|
26
28
|
import { transformListenAndChooseFromAnswerGroup } from '../../../components/questions/types/listen-and-choose-from-answer-group/transform';
|
|
27
29
|
import { transformListenAndChooseObjectsInScene } from '../../../components/questions/types/listen-and-choose-objects-in-scene/transform';
|
|
@@ -62,6 +64,7 @@ const questionTransformHandlers = {
|
|
|
62
64
|
CHOOSE_THE_CORRECT_ANSWER: transformChooseTheCorrectAnswer,
|
|
63
65
|
CHOOSE_THE_CORRECT_ANSWER_GROUP: transformChooseTheCorrectAnswerGroup,
|
|
64
66
|
CROSS_OUT_WORD_GROUP: transformCrossOutWordGroup,
|
|
67
|
+
TRUE_FALSE_CORRECT_GROUP: transformTrueFalseCorrectGroup,
|
|
65
68
|
FILL_IN_BLANK: transformFillInBlank,
|
|
66
69
|
WRITE_CORRECT_VERB_FORM: transformWriteCorrectVerbForm,
|
|
67
70
|
CHOOSE_CORRECT_ADJECTIVE: transformChooseCorrectAdjective,
|
|
@@ -71,6 +74,7 @@ const questionTransformHandlers = {
|
|
|
71
74
|
WORD_ORDERING: transformWordOrdering,
|
|
72
75
|
WORD_FILL_PARAGRAPH: transformWordFillParagraph,
|
|
73
76
|
MATCH_BY_WRITING_ANSWER: transformMatchByWritingAnswer,
|
|
77
|
+
MATCH_WORD_TO_PICTURE: transformMatchWordToPicture,
|
|
74
78
|
WRITE_SENTENCES: transformWriteSentences,
|
|
75
79
|
LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP: transformListenAndChooseFromAnswerGroup,
|
|
76
80
|
LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE: transformListenAndChooseObjectsInScene,
|
|
@@ -29,7 +29,7 @@ export declare enum StudentSelectionType {
|
|
|
29
29
|
}
|
|
30
30
|
export type ResultStatus = 'PENDING' | 'PARTIAL_GRADED' | 'GRADED';
|
|
31
31
|
export type StudentStatus = 'NOT_STARTED' | 'IN_PROGRESS' | 'SUBMITTED';
|
|
32
|
-
export type QuestionType = 'FILL_IN_BLANK' | 'TRUE_FALSE' | 'MATCHING' | 'ORDERING' | 'LISTENING' | 'COLORING' | 'ESSAY' | 'LISTEN_AND_FILL_IN_THE_BLANK_WITH_IMAGE' | 'LISTEN_AND_TICK_ANSWER' | 'LISTEN_AND_FILL_NAME_NUMBER' | 'LISTENING_FILL_BLANK' | 'LISTENING_COLOR' | 'TICK_TRUE_OR_FALSE' | 'TICK_YES_OR_NO' | 'READING_TICK_CROSS' | 'ARRANGE_LETTERS_INTO_WORDS' | 'READ_PASSAGE_AND_COMPLETE_STORY' | 'CLOZE_TEST_WITH_TICK_BOX' | 'READ_AND_CHOOSE_APPROPRIATE_WORD' | 'WRITE_SHORT_PARAGRAPH' | 'LABEL_THE_PICTURE' | 'READING_LETTER_ARRANGE' | 'READING_PASSAGE_FILL' | 'FILL_BLANK' | 'FILL_MISSING_WORDS_IN_GRID' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'LABEL_THE_PICTURE' | 'READ_AND_COLOR_OBJECTS' | 'IMAGE_OBJECT_MATCHING' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'TRUE_FALSE_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'SPONTANEOUS_QA_GROUP' | 'WORD_ORDER_AND_MATCH_GROUP' | 'WRITE_A_SHORT_LETTER' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_CORRECT_ADJECTIVE' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'WRITE_SENTENCES' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX';
|
|
32
|
+
export type QuestionType = 'FILL_IN_BLANK' | 'TRUE_FALSE' | 'MATCHING' | 'ORDERING' | 'LISTENING' | 'COLORING' | 'ESSAY' | 'LISTEN_AND_FILL_IN_THE_BLANK_WITH_IMAGE' | 'LISTEN_AND_TICK_ANSWER' | 'LISTEN_AND_FILL_NAME_NUMBER' | 'LISTENING_FILL_BLANK' | 'LISTENING_COLOR' | 'TICK_TRUE_OR_FALSE' | 'TICK_YES_OR_NO' | 'READING_TICK_CROSS' | 'ARRANGE_LETTERS_INTO_WORDS' | 'READ_PASSAGE_AND_COMPLETE_STORY' | 'CLOZE_TEST_WITH_TICK_BOX' | 'READ_AND_CHOOSE_APPROPRIATE_WORD' | 'WRITE_SHORT_PARAGRAPH' | 'LABEL_THE_PICTURE' | 'READING_LETTER_ARRANGE' | 'READING_PASSAGE_FILL' | 'FILL_BLANK' | 'FILL_MISSING_WORDS_IN_GRID' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'LABEL_THE_PICTURE' | 'READ_AND_COLOR_OBJECTS' | 'IMAGE_OBJECT_MATCHING' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'SPONTANEOUS_QA_GROUP' | 'WORD_ORDER_AND_MATCH_GROUP' | 'WRITE_A_SHORT_LETTER' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_CORRECT_ADJECTIVE' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX';
|
|
33
33
|
export type Level = 'Pre-A1' | 'A1' | 'A2' | 'B1' | 'B2' | 'C1' | 'C2';
|
|
34
34
|
export type Difficulty = 'EASY' | 'MEDIUM' | 'HARD';
|
|
35
35
|
export type Skill = 'LISTENING' | 'READING' | 'WRITING' | 'SPEAKING';
|
|
@@ -7,6 +7,7 @@ export interface ChooseTheCorrectAnswerGroupItemContent {
|
|
|
7
7
|
question: string;
|
|
8
8
|
options: ChooseTheCorrectAnswerGroupOption[];
|
|
9
9
|
optionType?: 'text' | 'image';
|
|
10
|
+
imageUrl?: string | string[];
|
|
10
11
|
}
|
|
11
12
|
export interface ChooseTheCorrectAnswerGroupItem {
|
|
12
13
|
questionType?: 'CHOOSE_THE_CORRECT_ANSWER';
|
|
@@ -50,3 +50,7 @@ export * from './word-order-and-match-group';
|
|
|
50
50
|
export * from './crossword-puzzle';
|
|
51
51
|
// FIND_WORDS_IN_MATRIX
|
|
52
52
|
export * from './find-words-in-matrix';
|
|
53
|
+
// MATCH_WORD_TO_PICTURE
|
|
54
|
+
export * from './match-word-to-picture';
|
|
55
|
+
// TRUE_FALSE_CORRECT_GROUP
|
|
56
|
+
export * from './true-false-correct-group';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Types for MATCH_WORD_TO_PICTURE question type
|
|
3
|
+
* Reference: .agents/templates/MATCH_WORD_TO_PICTURE.sample.json
|
|
4
|
+
*/
|
|
5
|
+
import { QuestionBankSettings } from './choose-the-correct-answer';
|
|
6
|
+
export interface MatchWordToPictureContent {
|
|
7
|
+
imageUrl: string;
|
|
8
|
+
}
|
|
9
|
+
export interface MatchWordToPictureCorrectAnswer {
|
|
10
|
+
answer: string;
|
|
11
|
+
}
|
|
12
|
+
export interface MatchWordToPictureGroupPayload {
|
|
13
|
+
wordBank: string[];
|
|
14
|
+
instruction?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface MatchWordToPictureAPIPayload {
|
|
17
|
+
id?: string;
|
|
18
|
+
questionType: 'MATCH_WORD_TO_PICTURE';
|
|
19
|
+
content: MatchWordToPictureContent;
|
|
20
|
+
correctAnswer: MatchWordToPictureCorrectAnswer;
|
|
21
|
+
isGroup: true;
|
|
22
|
+
isExample?: boolean;
|
|
23
|
+
questionGroup: {
|
|
24
|
+
groupNumber: number;
|
|
25
|
+
payload: MatchWordToPictureGroupPayload;
|
|
26
|
+
};
|
|
27
|
+
points: number;
|
|
28
|
+
explanation?: string;
|
|
29
|
+
questionNumber: number;
|
|
30
|
+
partId?: string;
|
|
31
|
+
partNo?: number;
|
|
32
|
+
addToQuestionBank?: boolean;
|
|
33
|
+
questionBank?: QuestionBankSettings;
|
|
34
|
+
}
|
|
35
|
+
export type MatchWordToPictureLocalStorage = Omit<MatchWordToPictureAPIPayload, 'addToQuestionBank' | 'questionBank'>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const TRUE_FALSE_CORRECT_ITEM_TYPE: "TRUE_FALSE_CORRECT";
|
|
2
|
+
export interface TrueFalseCorrectItemAnswer {
|
|
3
|
+
isTrue: boolean;
|
|
4
|
+
correction: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TrueFalseCorrectGroupItemContent {
|
|
7
|
+
question: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TrueFalseCorrectGroupItem {
|
|
10
|
+
questionType?: typeof TRUE_FALSE_CORRECT_ITEM_TYPE;
|
|
11
|
+
isExample?: boolean;
|
|
12
|
+
content: TrueFalseCorrectGroupItemContent;
|
|
13
|
+
correctAnswer?: {
|
|
14
|
+
answer: TrueFalseCorrectItemAnswer;
|
|
15
|
+
};
|
|
16
|
+
points?: number;
|
|
17
|
+
questionNumber?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface TrueFalseCorrectGroupContent {
|
|
20
|
+
meta?: {
|
|
21
|
+
instruction?: string;
|
|
22
|
+
};
|
|
23
|
+
items: TrueFalseCorrectGroupItem[];
|
|
24
|
+
}
|
|
25
|
+
export interface TrueFalseCorrectGroupCorrectAnswer {
|
|
26
|
+
answer: TrueFalseCorrectItemAnswer[];
|
|
27
|
+
}
|
|
28
|
+
export interface TrueFalseCorrectGroupAPIPayload {
|
|
29
|
+
id?: string;
|
|
30
|
+
questionType: 'TRUE_FALSE_CORRECT_GROUP';
|
|
31
|
+
content: TrueFalseCorrectGroupContent;
|
|
32
|
+
correctAnswer: TrueFalseCorrectGroupCorrectAnswer;
|
|
33
|
+
points: number;
|
|
34
|
+
explanation?: string;
|
|
35
|
+
questionNumber: number;
|
|
36
|
+
partId?: string;
|
|
37
|
+
partNo?: number;
|
|
38
|
+
}
|
|
39
|
+
export type TrueFalseCorrectGroupLocalStorage = TrueFalseCorrectGroupAPIPayload;
|