@tinyweb_dev/oe-exam-sdk 0.2.12 → 0.2.14
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 +13 -3
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.d.ts +14 -0
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +32 -0
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.d.ts +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +10 -2
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.d.ts +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -2
- package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.js +23 -0
- 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/utils/answer-transformers.js +34 -1
- package/dist/components/exams/take/utils/question-transformers.d.ts +43 -4
- package/dist/components/exams/take/utils/question-transformers.js +71 -1
- package/dist/components/questions/_shared/config/question-types.config.js +12 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +7 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.js +19 -1
- package/dist/components/questions/_shared/types/answer-the-question.type.d.ts +3 -1
- package/dist/components/questions/_shared/types/answer-the-question.type.js +19 -1
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +47 -0
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.js +2 -0
- package/dist/components/questions/_shared/types/fill-in-blank-group.type.d.ts +14 -2
- 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-group.type.d.ts +10 -1
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +19 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +9 -2
- package/dist/components/questions/_shared/types/sort-words-into-categories.type.d.ts +43 -0
- package/dist/components/questions/_shared/types/sort-words-into-categories.type.js +1 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +2 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +2 -0
- package/dist/components/questions/creator/question-type-registry.js +4 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +30 -6
- package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.js +56 -0
- package/dist/components/questions/types/answer-the-question/transform.js +9 -3
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +47 -3
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +8 -2
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +21 -2
- package/dist/components/questions/types/answer-the-question-group/transform.js +10 -1
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +2 -0
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +34 -0
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +174 -0
- package/dist/components/questions/types/choose-then-answer-group/index.d.ts +5 -0
- package/dist/components/questions/types/choose-then-answer-group/index.js +5 -0
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +11 -0
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +127 -0
- package/dist/components/questions/types/choose-then-answer-group/register.d.ts +3 -0
- package/dist/components/questions/types/choose-then-answer-group/register.js +37 -0
- package/dist/components/questions/types/choose-then-answer-group/transform.d.ts +2 -0
- package/dist/components/questions/types/choose-then-answer-group/transform.js +37 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +51 -16
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +52 -20
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +23 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +89 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +24 -6
- package/dist/components/questions/types/fill-in-blank-group/transform.js +18 -3
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +41 -12
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +23 -8
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +11 -2
- package/dist/components/questions/types/match-word-to-picture-group/transform.js +25 -9
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts +4 -2
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +4 -4
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +13 -2
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +25 -13
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +19 -2
- package/dist/components/questions/types/matching-with-lines-group/transform.js +29 -13
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.d.ts +12 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.js +74 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.d.ts +2 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +24 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.d.ts +2 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.js +171 -0
- package/dist/components/questions/types/sort-words-into-categories/index.d.ts +4 -0
- package/dist/components/questions/types/sort-words-into-categories/index.js +4 -0
- package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.d.ts +11 -0
- package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.js +153 -0
- package/dist/components/questions/types/sort-words-into-categories/register.d.ts +3 -0
- package/dist/components/questions/types/sort-words-into-categories/register.js +37 -0
- package/dist/components/questions/types/sort-words-into-categories/transform.d.ts +2 -0
- package/dist/components/questions/types/sort-words-into-categories/transform.js +33 -0
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +18 -0
- package/dist/components/questions/viewer/AnswerTheQuestionViewer.d.ts +9 -0
- package/dist/components/questions/viewer/AnswerTheQuestionViewer.js +8 -0
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +10 -0
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +39 -0
- package/dist/components/questions/viewer/QuestionViewer.js +13 -2
- package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.d.ts +10 -0
- package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.js +27 -0
- package/dist/components/questions/viewer/index.d.ts +3 -0
- package/dist/components/questions/viewer/index.js +3 -0
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +2 -1
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +12 -3
- package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
- package/dist/components/results/renderers/review-question-body-movers.js +24 -2
- package/dist/components/results/review-data.js +28 -0
- 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/utils/question-reverse-transform.js +126 -6
- package/dist/shared/lib/utils/question-transform-types.d.ts +1 -1
- 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/answer-the-question-group.d.ts +3 -0
- package/dist/shared/types/questions/choose-then-answer-group.d.ts +46 -0
- package/dist/shared/types/questions/choose-then-answer-group.js +1 -0
- package/dist/shared/types/questions/fill-in-blank-group.d.ts +9 -2
- package/dist/shared/types/questions/index.d.ts +2 -0
- package/dist/shared/types/questions/index.js +3 -0
- package/dist/shared/types/questions/match-word-to-picture-group.d.ts +7 -1
- package/dist/shared/types/questions/matching-with-lines-group.d.ts +7 -2
- package/dist/shared/types/questions/sort-words-into-categories.d.ts +37 -0
- package/dist/shared/types/questions/sort-words-into-categories.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { ChooseThenAnswerGroupClient } from '../types/choose-then-answer-group/ChooseThenAnswerGroupClient';
|
|
5
|
+
import { mapQuestionToChooseThenAnswerGroupData, parseStudentAnswer, } from '../types/choose-then-answer-group/map-choose-then-answer-group-data';
|
|
6
|
+
function readStudentAnswers(value, itemCount) {
|
|
7
|
+
const record = value && typeof value === 'object' && !Array.isArray(value)
|
|
8
|
+
? value
|
|
9
|
+
: null;
|
|
10
|
+
const raw = Array.isArray(record?.answer)
|
|
11
|
+
? record.answer
|
|
12
|
+
: Array.isArray(value)
|
|
13
|
+
? value
|
|
14
|
+
: null;
|
|
15
|
+
if (!raw)
|
|
16
|
+
return undefined;
|
|
17
|
+
return Array.from({ length: itemCount }, (_, index) => parseStudentAnswer(raw[index]));
|
|
18
|
+
}
|
|
19
|
+
export function ChooseThenAnswerGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
20
|
+
const questionData = mapQuestionToChooseThenAnswerGroupData(question);
|
|
21
|
+
const seed = questionData.items.map((item) => ({
|
|
22
|
+
optionId: item.isExample ? item.optionId : '',
|
|
23
|
+
text: '',
|
|
24
|
+
}));
|
|
25
|
+
const parsedUser = readStudentAnswers(userAnswer, questionData.items.length);
|
|
26
|
+
const questionKey = `${question.id}:${question.type}:${questionData.items.length}`;
|
|
27
|
+
const [cachedKey, setCachedKey] = useState(questionKey);
|
|
28
|
+
const [localAnswers, setLocalAnswers] = useState(() => parsedUser ?? seed);
|
|
29
|
+
if (cachedKey !== questionKey) {
|
|
30
|
+
setCachedKey(questionKey);
|
|
31
|
+
setLocalAnswers(parsedUser ?? seed);
|
|
32
|
+
}
|
|
33
|
+
const handleChange = (index, next) => {
|
|
34
|
+
const updated = localAnswers.map((item, itemIndex) => itemIndex === index ? next : item);
|
|
35
|
+
setLocalAnswers(updated);
|
|
36
|
+
onSubmitAnswer?.({ answer: updated });
|
|
37
|
+
};
|
|
38
|
+
return (_jsx(ChooseThenAnswerGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: isReviewMode && !parsedUser ? undefined : (parsedUser ?? localAnswers), onAnswerChange: isReviewMode ? undefined : handleChange }));
|
|
39
|
+
}
|
|
@@ -21,6 +21,8 @@ import { FillMissingWordsInGridClient } from '../types/fill-missing-words-in-gri
|
|
|
21
21
|
import { WordFillStructuredFormClient } from '../types/word-fill-structured-form/WordFillStructuredFormClient';
|
|
22
22
|
import { CrosswordPuzzleClient } from '../types/crossword-puzzle/CrosswordPuzzleClient';
|
|
23
23
|
import { FindWordsInMatrixClient } from '../types/find-words-in-matrix/FindWordsInMatrixClient';
|
|
24
|
+
import { SortWordsIntoCategoriesViewer } from './SortWordsIntoCategoriesViewer';
|
|
25
|
+
import { ChooseThenAnswerGroupViewer } from './ChooseThenAnswerGroupViewer';
|
|
24
26
|
import { parseFillBlankStemLine } from '../_shared/utils/parseFillBlankStem';
|
|
25
27
|
import { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
|
|
26
28
|
import { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
|
|
@@ -31,6 +33,7 @@ import { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
|
|
|
31
33
|
import { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
|
|
32
34
|
import { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
|
|
33
35
|
import { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
36
|
+
import { AnswerTheQuestionViewer } from './AnswerTheQuestionViewer';
|
|
34
37
|
import { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
35
38
|
import { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
36
39
|
import { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|
|
@@ -647,6 +650,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
647
650
|
return (_jsx(MatchColumnsToMakeSentencesViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
648
651
|
case 'CROSS_OUT_WORD_GROUP':
|
|
649
652
|
return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode }));
|
|
653
|
+
case 'ANSWER_THE_QUESTION':
|
|
654
|
+
return (_jsx(AnswerTheQuestionViewer, { question: question, isReviewMode: isReviewMode, userAnswer: typeof userAnswer === 'string' ? userAnswer : undefined, onSubmitAnswer: onSubmitAnswer }));
|
|
650
655
|
case 'ANSWER_THE_QUESTION_GROUP':
|
|
651
656
|
return (_jsx(AnswerTheQuestionGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
652
657
|
case 'SPONTANEOUS_QA_GROUP':
|
|
@@ -703,6 +708,10 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
703
708
|
return (_jsx(CrosswordPuzzleClient, { content: question.content, correctAnswer: question.answer, isReviewMode: isReviewMode, userAnswers: userAnswer }));
|
|
704
709
|
case 'FIND_WORDS_IN_MATRIX':
|
|
705
710
|
return (_jsx(FindWordsInMatrixClient, { content: question.content, correctAnswer: question.answer, showSolution: isReviewMode }));
|
|
711
|
+
case 'SORT_WORDS_INTO_CATEGORIES':
|
|
712
|
+
return (_jsx(SortWordsIntoCategoriesViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
713
|
+
case 'CHOOSE_THEN_ANSWER_GROUP':
|
|
714
|
+
return (_jsx(ChooseThenAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
706
715
|
default:
|
|
707
716
|
return _jsxs("div", { children: ["Lo\u1EA1i c\u00E2u h\u1ECFi kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3: ", question.type] });
|
|
708
717
|
}
|
|
@@ -716,7 +725,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
716
725
|
// WFSF and Crossword show title/instruction inside the client — no outer card label.
|
|
717
726
|
if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
|
|
718
727
|
question.type === 'CROSSWORD_PUZZLE' ||
|
|
719
|
-
question.type === 'MATCH_WORD_TO_PICTURE'
|
|
728
|
+
question.type === 'MATCH_WORD_TO_PICTURE' ||
|
|
729
|
+
question.type === 'ANSWER_THE_QUESTION') {
|
|
720
730
|
return null;
|
|
721
731
|
}
|
|
722
732
|
// Modern fill stems live entirely in the body; title is a short label.
|
|
@@ -741,7 +751,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
741
751
|
// WFSF and Crossword already have their own bordered shell — avoid double Card border.
|
|
742
752
|
if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
|
|
743
753
|
question.type === 'CROSSWORD_PUZZLE' ||
|
|
744
|
-
question.type === 'MATCH_WORD_TO_PICTURE'
|
|
754
|
+
question.type === 'MATCH_WORD_TO_PICTURE' ||
|
|
755
|
+
question.type === 'ANSWER_THE_QUESTION') {
|
|
745
756
|
return (_jsx("div", { className: "space-y-4", children: renderQuestion() }));
|
|
746
757
|
}
|
|
747
758
|
return (_jsxs(Card, { children: [cardTitle ? (_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center justify-between", children: [_jsx("span", { className: "min-w-0 pr-2 text-base font-semibold leading-snug", children: cardTitle }), _jsxs("span", { className: "shrink-0 text-sm font-normal text-gray-500", children: [question.points, " \u0111i\u1EC3m"] })] }) })) : null, _jsx(CardContent, { className: cardTitle ? 'space-y-4' : 'space-y-4 pt-4', children: renderQuestion() })] }));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Question } from '../../../shared/types/entities/question.types';
|
|
2
|
+
import type { SortWordsPlacement } from '../_shared/types/sort-words-into-categories.type';
|
|
3
|
+
export declare function SortWordsIntoCategoriesViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
|
|
4
|
+
question: Question;
|
|
5
|
+
isReviewMode?: boolean;
|
|
6
|
+
userAnswer?: unknown;
|
|
7
|
+
onSubmitAnswer?: (answer: {
|
|
8
|
+
placements: SortWordsPlacement[];
|
|
9
|
+
}) => void;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { SortWordsIntoCategoriesClient } from '../types/sort-words-into-categories/SortWordsIntoCategoriesClient';
|
|
5
|
+
import { mapQuestionToSortWordsIntoCategoriesData, readPlacements, } from '../types/sort-words-into-categories/map-sort-words-into-categories-data';
|
|
6
|
+
export function SortWordsIntoCategoriesViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
7
|
+
const questionData = mapQuestionToSortWordsIntoCategoriesData(question);
|
|
8
|
+
const parsedUser = readPlacements(userAnswer);
|
|
9
|
+
const questionKey = `${question.id}:${question.type}:${questionData.words.map((word) => word.id).join(',')}`;
|
|
10
|
+
const [cachedKey, setCachedKey] = useState(questionKey);
|
|
11
|
+
const [localPlacements, setLocalPlacements] = useState(() => parsedUser ?? questionData.words.map((word) => ({
|
|
12
|
+
wordId: word.id,
|
|
13
|
+
categoryId: word.isExample ? word.categoryId : '',
|
|
14
|
+
})));
|
|
15
|
+
if (cachedKey !== questionKey) {
|
|
16
|
+
setCachedKey(questionKey);
|
|
17
|
+
setLocalPlacements(parsedUser ?? questionData.words.map((word) => ({
|
|
18
|
+
wordId: word.id,
|
|
19
|
+
categoryId: word.isExample ? word.categoryId : '',
|
|
20
|
+
})));
|
|
21
|
+
}
|
|
22
|
+
const handleChange = (next) => {
|
|
23
|
+
setLocalPlacements(next);
|
|
24
|
+
onSubmitAnswer?.({ placements: next });
|
|
25
|
+
};
|
|
26
|
+
return (_jsx(SortWordsIntoCategoriesClient, { questionData: questionData, isReviewMode: isReviewMode, userPlacements: isReviewMode && !parsedUser ? undefined : (parsedUser ?? localPlacements), onPlacementsChange: isReviewMode ? undefined : handleChange }));
|
|
27
|
+
}
|
|
@@ -9,8 +9,11 @@ export { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
|
|
|
9
9
|
export { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
|
|
10
10
|
export { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
|
|
11
11
|
export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
12
|
+
export { AnswerTheQuestionViewer } from './AnswerTheQuestionViewer';
|
|
12
13
|
export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
13
14
|
export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
14
15
|
export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|
|
15
16
|
export { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
|
|
16
17
|
export { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
|
|
18
|
+
export { SortWordsIntoCategoriesViewer } from './SortWordsIntoCategoriesViewer';
|
|
19
|
+
export { ChooseThenAnswerGroupViewer } from './ChooseThenAnswerGroupViewer';
|
|
@@ -9,8 +9,11 @@ export { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
|
|
|
9
9
|
export { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
|
|
10
10
|
export { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
|
|
11
11
|
export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
12
|
+
export { AnswerTheQuestionViewer } from './AnswerTheQuestionViewer';
|
|
12
13
|
export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
13
14
|
export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
14
15
|
export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|
|
15
16
|
export { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
|
|
16
17
|
export { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
|
|
18
|
+
export { SortWordsIntoCategoriesViewer } from './SortWordsIntoCategoriesViewer';
|
|
19
|
+
export { ChooseThenAnswerGroupViewer } from './ChooseThenAnswerGroupViewer';
|
|
@@ -11,9 +11,10 @@ interface ReviewAnswerTheQuestionGroupRendererProps {
|
|
|
11
11
|
questionCount: number;
|
|
12
12
|
instruction: string;
|
|
13
13
|
groupContent?: string;
|
|
14
|
+
imageUrl?: string | string[];
|
|
14
15
|
questions: AnswerTheQuestionQuestion[];
|
|
15
16
|
answers: Record<string, string>;
|
|
16
17
|
itemMeta: Record<string, ReviewAnswerTheQuestionGroupItemMeta>;
|
|
17
18
|
}
|
|
18
|
-
export declare function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, questions, answers, itemMeta, }: ReviewAnswerTheQuestionGroupRendererProps): import("react").JSX.Element;
|
|
19
|
+
export declare function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, imageUrl, questions, answers, itemMeta, }: ReviewAnswerTheQuestionGroupRendererProps): import("react").JSX.Element;
|
|
19
20
|
export {};
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { BookOpen, Check, Sparkles, Star, X } from 'lucide-react';
|
|
4
|
+
import { ResolvedImage } from '../../../components/common/ResolvedImage';
|
|
4
5
|
import CambridgeYlePartBanner from '../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
5
6
|
import CambridgeYleInstructionBanner from '../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
import { toAnswerTheQuestionImageUrls } from '../../questions/_shared/types/answer-the-question.type';
|
|
8
|
+
function ReviewImages({ imageUrl, alt, }) {
|
|
9
|
+
const urls = toAnswerTheQuestionImageUrls(imageUrl);
|
|
10
|
+
if (urls.length === 0) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: urls.map((url, imageIndex) => (_jsx(ResolvedImage, { src: url, alt: `${alt} ${imageIndex + 1}`, className: "max-h-56 w-auto rounded-lg object-contain" }, `${alt}-${imageIndex}`))) }));
|
|
14
|
+
}
|
|
15
|
+
export function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, imageUrl, questions, answers, itemMeta, }) {
|
|
16
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx(ReviewImages, { imageUrl: imageUrl, alt: "Shared illustration" }), groupContent && (_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 leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: groupContent } })] })), questions.map((question) => {
|
|
8
17
|
const meta = itemMeta[question.id];
|
|
9
18
|
const studentAnswer = answers[question.id] || '';
|
|
10
19
|
const expectedAnswers = meta?.expectedAnswers || [];
|
|
@@ -16,7 +25,7 @@ export function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, que
|
|
|
16
25
|
? 'bg-green-500'
|
|
17
26
|
: isCorrect === false
|
|
18
27
|
? 'bg-red-500'
|
|
19
|
-
: 'bg-blue-500'}`, children: question.questionNumber }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", question.questionNumber] }), question.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: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] })] }), isCorrect === true && _jsx(Check, { className: "h-4 w-4 text-green-600" }), isCorrect === false && _jsx(X, { className: "h-4 w-4 text-red-600" })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: question.question }), _jsx("div", { className: `rounded-lg border px-3 py-2 text-sm ${isCorrect === true
|
|
28
|
+
: 'bg-blue-500'}`, children: question.questionNumber }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", question.questionNumber] }), question.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: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] })] }), isCorrect === true && _jsx(Check, { className: "h-4 w-4 text-green-600" }), isCorrect === false && _jsx(X, { className: "h-4 w-4 text-red-600" })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx(ReviewImages, { imageUrl: question.imageUrl, alt: `Hình ảnh câu ${question.questionNumber}` }), _jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: question.question }), _jsx("div", { className: `rounded-lg border px-3 py-2 text-sm ${isCorrect === true
|
|
20
29
|
? 'border-green-200 bg-green-50 text-green-800'
|
|
21
30
|
: isCorrect === false
|
|
22
31
|
? 'border-red-200 bg-red-50 text-red-800'
|
|
@@ -115,7 +115,7 @@ export function renderDedicatedReviewBody(ctx) {
|
|
|
115
115
|
feedbackVi: typeof feedback.vi === 'string' ? feedback.vi : undefined,
|
|
116
116
|
}];
|
|
117
117
|
}));
|
|
118
|
-
return (_jsx(ReviewAnswerTheQuestionGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, groupContent: data.groupContent, questions: data.questions, answers: answers, itemMeta: itemMeta }));
|
|
118
|
+
return (_jsx(ReviewAnswerTheQuestionGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, groupContent: data.groupContent, imageUrl: data.imageUrl, questions: data.questions, answers: answers, itemMeta: itemMeta }));
|
|
119
119
|
}
|
|
120
120
|
case 'SPONTANEOUS_QA_GROUP': {
|
|
121
121
|
const parent = questions[0];
|
|
@@ -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, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, } from '../../../components/exams/take/components/question-renderers';
|
|
8
|
-
import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformMatchingWithLinesGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWordOrderingGroupTake, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, } from '../../../components/exams/take/utils/question-transformers';
|
|
7
|
+
import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, ChooseThenAnswerGroupRenderer, SortWordsIntoCategoriesRenderer, } from '../../../components/exams/take/components/question-renderers';
|
|
8
|
+
import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformMatchingWithLinesGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWordOrderingGroupTake, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, transformChooseThenAnswerGroup, transformSortWordsIntoCategories, } 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;
|
|
@@ -110,6 +110,28 @@ 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 'CHOOSE_THEN_ANSWER_GROUP': {
|
|
114
|
+
const data = transformChooseThenAnswerGroup(apiQuestions);
|
|
115
|
+
const gradableCount = data.questions.filter((item) => !item.isExample).length;
|
|
116
|
+
return (_jsx(ChooseThenAnswerGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: gradableCount || questionCount, instruction: part.instructions || data.instruction, data: data, answers: answers, onAnswerChange: () => undefined, isReviewMode: true }));
|
|
117
|
+
}
|
|
118
|
+
case 'SORT_WORDS_INTO_CATEGORIES': {
|
|
119
|
+
const data = transformSortWordsIntoCategories(apiQuestions);
|
|
120
|
+
const firstQuestion = apiQuestions[0];
|
|
121
|
+
const stored = firstQuestion?.id ? answers[firstQuestion.id] : undefined;
|
|
122
|
+
const storedRecord = stored && typeof stored === 'object' && !Array.isArray(stored)
|
|
123
|
+
? stored
|
|
124
|
+
: undefined;
|
|
125
|
+
const storedPlacements = Array.isArray(storedRecord?.placements)
|
|
126
|
+
? storedRecord.placements
|
|
127
|
+
: Array.isArray(stored)
|
|
128
|
+
? stored
|
|
129
|
+
: [];
|
|
130
|
+
const gradableCount = data.words.filter((word) => !word.isExample).length;
|
|
131
|
+
return (_jsx(SortWordsIntoCategoriesRenderer, { partNumber: partNo, partName: part.name, questionCount: gradableCount || questionCount, instruction: part.instructions || data.instruction, data: data, answers: {
|
|
132
|
+
[data.questionId]: { placements: storedPlacements },
|
|
133
|
+
}, onAnswerChange: () => undefined, isReviewMode: true }));
|
|
134
|
+
}
|
|
113
135
|
case 'TRUE_FALSE_CORRECT_GROUP': {
|
|
114
136
|
const data = transformTrueFalseCorrectGroup(apiQuestions);
|
|
115
137
|
const correctAnswerMap = {};
|
|
@@ -9,6 +9,7 @@ const FLATTENED_ANSWER_TYPES = new Set([
|
|
|
9
9
|
'ANSWER_THE_QUESTION_GROUP',
|
|
10
10
|
'CROSS_OUT_WORD_GROUP',
|
|
11
11
|
'TRUE_FALSE_CORRECT_GROUP',
|
|
12
|
+
'CHOOSE_THEN_ANSWER_GROUP',
|
|
12
13
|
'FILL_IN_BLANK_GROUP',
|
|
13
14
|
'MATCHING_WITH_LINES_GROUP',
|
|
14
15
|
]);
|
|
@@ -186,6 +187,26 @@ function transformStudentAnswer(questionId, questionType, value) {
|
|
|
186
187
|
normalizeCrossOutWordGroupAnswer(item),
|
|
187
188
|
]));
|
|
188
189
|
}
|
|
190
|
+
if (questionType === 'CHOOSE_THEN_ANSWER_GROUP') {
|
|
191
|
+
const answer = Array.isArray(record.answer)
|
|
192
|
+
? record.answer
|
|
193
|
+
: Array.isArray(value)
|
|
194
|
+
? value
|
|
195
|
+
: [];
|
|
196
|
+
return Object.fromEntries(answer.map((item, index) => {
|
|
197
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
198
|
+
const obj = item;
|
|
199
|
+
return [
|
|
200
|
+
`${questionId}-item-${index}`,
|
|
201
|
+
{
|
|
202
|
+
optionId: typeof obj.optionId === 'string' ? obj.optionId : '',
|
|
203
|
+
text: typeof obj.text === 'string' ? obj.text : '',
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
return [`${questionId}-item-${index}`, { optionId: '', text: '' }];
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
189
210
|
if (questionType === 'TRUE_FALSE_CORRECT_GROUP') {
|
|
190
211
|
const answer = Array.isArray(record.answer)
|
|
191
212
|
? record.answer
|
|
@@ -282,6 +303,13 @@ function transformStudentAnswer(questionId, questionType, value) {
|
|
|
282
303
|
}
|
|
283
304
|
if (questionType === 'READ_AND_COLOR_OBJECTS' || questionType === 'IMAGE_OBJECT_MATCHING' || questionType === 'CROSSWORD_PUZZLE')
|
|
284
305
|
return record.cells ?? record.answers ?? value;
|
|
306
|
+
if (questionType === 'SORT_WORDS_INTO_CATEGORIES') {
|
|
307
|
+
if (Array.isArray(record.placements))
|
|
308
|
+
return record;
|
|
309
|
+
if (Array.isArray(value))
|
|
310
|
+
return { placements: value };
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
285
313
|
if (questionType === 'WRITE_A_SHORT_LETTER')
|
|
286
314
|
return typeof value === 'string' ? value : record.answer ?? '';
|
|
287
315
|
return record.answer ?? value;
|
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, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, } from './components/questions/viewer';
|
|
29
|
+
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, SortWordsIntoCategoriesViewer, ChooseThenAnswerGroupViewer, } 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, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, } from './components/questions/viewer';
|
|
24
|
+
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, SortWordsIntoCategoriesViewer, ChooseThenAnswerGroupViewer, } from './components/questions/viewer';
|
|
@@ -84,6 +84,8 @@ export const QUESTION_TYPE_SKILLS = {
|
|
|
84
84
|
'WORD_FILL_STRUCTURED_FORM': SkillEnum.READING,
|
|
85
85
|
'CROSSWORD_PUZZLE': SkillEnum.READING,
|
|
86
86
|
'FIND_WORDS_IN_MATRIX': SkillEnum.READING,
|
|
87
|
+
'SORT_WORDS_INTO_CATEGORIES': SkillEnum.READING,
|
|
88
|
+
'CHOOSE_THEN_ANSWER_GROUP': SkillEnum.READING,
|
|
87
89
|
'MATCH_COLUMNS_TO_MAKE_SENTENCES': SkillEnum.READING,
|
|
88
90
|
'MATCH_WORD_TO_PICTURE_GROUP': SkillEnum.READING,
|
|
89
91
|
'WORD_ORDERING_GROUP': SkillEnum.READING,
|
|
@@ -219,6 +221,8 @@ export const QUESTION_TYPE_LABELS = {
|
|
|
219
221
|
'WORD_FILL_STRUCTURED_FORM': 'Điền từ vào biểu mẫu có cấu trúc',
|
|
220
222
|
'CROSSWORD_PUZZLE': 'Giải ô chữ (Crossword)',
|
|
221
223
|
'FIND_WORDS_IN_MATRIX': 'Tìm từ trong ma trận (Word Search)',
|
|
224
|
+
'SORT_WORDS_INTO_CATEGORIES': 'Sắp xếp từ vào nhóm',
|
|
225
|
+
'CHOOSE_THEN_ANSWER_GROUP': 'Chọn đáp án rồi trả lời',
|
|
222
226
|
'MATCH_COLUMNS_TO_MAKE_SENTENCES': 'Ghép cột thành câu',
|
|
223
227
|
'MATCH_WORD_TO_PICTURE_GROUP': 'Nối từ với tranh (nhóm)',
|
|
224
228
|
'WORD_ORDERING_GROUP': 'Sắp xếp từ (nhóm)',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { fromFillInBlankImageUrls, toFillInBlankImageUrls, } from '../../../components/questions/types/fill-in-blank-group/blank-utils';
|
|
2
|
+
import { fromMatchWordToPictureImageUrls } from '../../../components/questions/_shared/types/match-word-to-picture-group.type';
|
|
1
3
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2
4
|
const asRecord = (value) => isRecord(value) ? value : {};
|
|
3
5
|
const asArray = (value) => Array.isArray(value) ? value : [];
|
|
@@ -301,19 +303,30 @@ const reverseAnswerTheQuestionGroup = (apiQuestion) => {
|
|
|
301
303
|
.filter((value) => value !== '');
|
|
302
304
|
const expectedAnswers = nested.length > 0 ? nested : (fallbackExpected.length > 0 ? fallbackExpected : ['']);
|
|
303
305
|
const isExample = item.isExample === true;
|
|
306
|
+
const imageUrl = normalizeChooseAnswerGroupImageUrl(itemContent.imageUrl);
|
|
304
307
|
return {
|
|
305
308
|
question: asString(itemContent.question),
|
|
306
309
|
expectedAnswers,
|
|
307
310
|
isExample,
|
|
308
311
|
points: isExample ? 0 : asNumber(item.points, 1),
|
|
309
312
|
questionNumber: asNumber(item.questionNumber, index + 1),
|
|
313
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
310
314
|
};
|
|
311
315
|
});
|
|
316
|
+
const meta = asRecord(content.meta);
|
|
317
|
+
const audioUrl = asString(meta.audioUrl).trim();
|
|
318
|
+
const imageUrl = asArray(meta.imageUrl)
|
|
319
|
+
.filter((value) => typeof value === 'string')
|
|
320
|
+
.map((value) => value.trim())
|
|
321
|
+
.filter((value) => value !== '');
|
|
322
|
+
const singleImage = asString(meta.imageUrl).trim();
|
|
312
323
|
return {
|
|
313
324
|
content: '',
|
|
314
325
|
answer: {
|
|
315
|
-
instruction: asString(
|
|
316
|
-
passage: asString(
|
|
326
|
+
instruction: asString(meta.instruction, 'Read the text again and answer the questions.'),
|
|
327
|
+
passage: asString(meta.passage),
|
|
328
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
329
|
+
...(imageUrl.length ? { imageUrl } : (singleImage ? { imageUrl: [singleImage] } : {})),
|
|
317
330
|
items,
|
|
318
331
|
explanation: asString(apiQuestion.explanation),
|
|
319
332
|
points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
|
|
@@ -383,6 +396,45 @@ const reverseTrueFalseCorrectGroup = (apiQuestion) => {
|
|
|
383
396
|
},
|
|
384
397
|
};
|
|
385
398
|
};
|
|
399
|
+
const reverseChooseThenAnswerGroup = (apiQuestion) => {
|
|
400
|
+
const content = asRecord(apiQuestion.content);
|
|
401
|
+
const answer = readAnswer(apiQuestion);
|
|
402
|
+
const fallbackAnswers = asArray(answer.answer);
|
|
403
|
+
const meta = asRecord(content.meta);
|
|
404
|
+
const items = asArray(content.items).map((rawItem, index) => {
|
|
405
|
+
const item = asRecord(rawItem);
|
|
406
|
+
const itemContent = asRecord(item.content);
|
|
407
|
+
const itemAnswer = asRecord(asRecord(item.correctAnswer).answer);
|
|
408
|
+
const fallback = asRecord(fallbackAnswers[index]);
|
|
409
|
+
const rawAnswer = Object.keys(itemAnswer).length > 0 ? itemAnswer : fallback;
|
|
410
|
+
const isExample = item.isExample === true;
|
|
411
|
+
const options = asArray(itemContent.options).map((rawOption, optionIndex) => {
|
|
412
|
+
const option = asRecord(rawOption);
|
|
413
|
+
return {
|
|
414
|
+
id: asString(option.id, `opt-${optionIndex + 1}`),
|
|
415
|
+
text: asString(option.text),
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
return {
|
|
419
|
+
question: asString(itemContent.question),
|
|
420
|
+
options,
|
|
421
|
+
optionId: asString(rawAnswer.optionId) || options[0]?.id || '',
|
|
422
|
+
isExample,
|
|
423
|
+
points: isExample ? 0 : asNumber(item.points, 1),
|
|
424
|
+
questionNumber: asNumber(item.questionNumber, index + 1),
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
return {
|
|
428
|
+
content: '',
|
|
429
|
+
answer: {
|
|
430
|
+
title: asString(meta.title),
|
|
431
|
+
instruction: asString(meta.instruction, 'Read and choose. Then answer for you.'),
|
|
432
|
+
items,
|
|
433
|
+
explanation: asString(apiQuestion.explanation),
|
|
434
|
+
points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
};
|
|
386
438
|
const reverseFillInBlankGroup = (apiQuestion) => {
|
|
387
439
|
const content = asRecord(apiQuestion.content);
|
|
388
440
|
const answer = readAnswer(apiQuestion);
|
|
@@ -402,27 +454,34 @@ const reverseFillInBlankGroup = (apiQuestion) => {
|
|
|
402
454
|
: [String(blank ?? '').trim()].filter(Boolean);
|
|
403
455
|
return alts.length > 0 ? alts : [''];
|
|
404
456
|
});
|
|
457
|
+
const wordBank = asArray(itemContent.wordBank);
|
|
405
458
|
return {
|
|
406
459
|
question,
|
|
407
460
|
answers: answers.length > 0 ? answers : [['']],
|
|
408
461
|
caseSensitive: itemAnswer.caseSensitive === true || fallback.caseSensitive === true,
|
|
409
|
-
imageUrl:
|
|
462
|
+
imageUrl: fromFillInBlankImageUrls(toFillInBlankImageUrls(itemContent.imageUrl)),
|
|
410
463
|
audioUrl: asString(itemContent.audioUrl),
|
|
411
464
|
showHints: itemContent.showHints === true,
|
|
412
465
|
hints: asArray(itemContent.hints).map((hint) => String(hint ?? '').trim()).filter(Boolean),
|
|
466
|
+
...(wordBank.length > 0 ? { wordBank } : {}),
|
|
413
467
|
isExample,
|
|
414
468
|
points: isExample ? 0 : asNumber(item.points, 1),
|
|
415
469
|
questionNumber: asNumber(item.questionNumber, index + 1),
|
|
416
470
|
};
|
|
417
471
|
});
|
|
472
|
+
const groupWordBank = asArray(meta.wordBank);
|
|
473
|
+
const viewMode = asString(meta.viewMode) === 'WITH_OPTIONS' ? 'WITH_OPTIONS' : undefined;
|
|
418
474
|
return {
|
|
419
475
|
content: '',
|
|
420
476
|
answer: {
|
|
421
477
|
instruction: asString(meta.instruction, 'Complete the sentences.'),
|
|
478
|
+
passage: asString(meta.passage),
|
|
422
479
|
audioUrl: asString(meta.audioUrl),
|
|
423
|
-
imageUrl:
|
|
480
|
+
imageUrl: fromFillInBlankImageUrls(toFillInBlankImageUrls(meta.imageUrl)),
|
|
424
481
|
showHints: meta.showHints === true,
|
|
425
482
|
hints: asArray(meta.hints).map((hint) => String(hint ?? '').trim()).filter(Boolean),
|
|
483
|
+
...(viewMode ? { viewMode } : {}),
|
|
484
|
+
...(groupWordBank.length > 0 ? { wordBank: groupWordBank } : {}),
|
|
426
485
|
items,
|
|
427
486
|
explanation: asString(apiQuestion.explanation),
|
|
428
487
|
points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
|
|
@@ -436,9 +495,12 @@ const reverseMatchingWithLinesGroup = (apiQuestion) => {
|
|
|
436
495
|
const meta = asRecord(content.meta);
|
|
437
496
|
const rightItems = asArray(content.rightItems).map((raw, index) => {
|
|
438
497
|
const item = asRecord(raw);
|
|
498
|
+
const imageUrl = asString(item.imageUrl).trim();
|
|
499
|
+
const text = asString(item.text).trim();
|
|
439
500
|
return {
|
|
440
501
|
id: asString(item.id, `r${index + 1}`),
|
|
441
|
-
text:
|
|
502
|
+
...(text ? { text } : {}),
|
|
503
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
442
504
|
};
|
|
443
505
|
});
|
|
444
506
|
const items = asArray(content.items).map((rawItem, index) => {
|
|
@@ -449,6 +511,7 @@ const reverseMatchingWithLinesGroup = (apiQuestion) => {
|
|
|
449
511
|
const isExample = item.isExample === true;
|
|
450
512
|
return {
|
|
451
513
|
imageUrl: asString(itemContent.imageUrl),
|
|
514
|
+
text: asString(itemContent.text),
|
|
452
515
|
correctAnswer: asString(itemAnswer.answer, typeof fallback === 'string' ? fallback : ''),
|
|
453
516
|
isExample,
|
|
454
517
|
points: isExample ? 0 : asNumber(item.points, 1),
|
|
@@ -460,6 +523,9 @@ const reverseMatchingWithLinesGroup = (apiQuestion) => {
|
|
|
460
523
|
answer: {
|
|
461
524
|
title: asString(meta.title),
|
|
462
525
|
instruction: asString(meta.instruction, 'Look and match.'),
|
|
526
|
+
...(asString(meta.passage).trim() ? { passage: asString(meta.passage).trim() } : {}),
|
|
527
|
+
...(asString(meta.audioUrl).trim() ? { audioUrl: asString(meta.audioUrl).trim() } : {}),
|
|
528
|
+
...(asString(meta.imageUrl).trim() ? { imageUrl: asString(meta.imageUrl).trim() } : {}),
|
|
463
529
|
rightItems,
|
|
464
530
|
items,
|
|
465
531
|
explanation: asString(apiQuestion.explanation),
|
|
@@ -658,8 +724,9 @@ const reverseMatchWordToPictureGroup = (apiQuestion) => {
|
|
|
658
724
|
const itemContent = asRecord(item.content);
|
|
659
725
|
const itemAnswer = asRecord(item.correctAnswer);
|
|
660
726
|
const isExample = item.isExample === true;
|
|
727
|
+
const itemImageUrl = fromMatchWordToPictureImageUrls(itemContent.imageUrl ?? item.imageUrl);
|
|
661
728
|
return {
|
|
662
|
-
imageUrl:
|
|
729
|
+
...(itemImageUrl ? { imageUrl: itemImageUrl } : {}),
|
|
663
730
|
correctAnswer: asString(itemAnswer.answer) || asString(fallbackAnswers[index]),
|
|
664
731
|
isExample,
|
|
665
732
|
points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
@@ -669,7 +736,13 @@ const reverseMatchWordToPictureGroup = (apiQuestion) => {
|
|
|
669
736
|
return {
|
|
670
737
|
content: '',
|
|
671
738
|
answer: {
|
|
739
|
+
...(asString(meta.title).trim() ? { title: asString(meta.title).trim() } : {}),
|
|
672
740
|
instruction: asString(meta.instruction, 'Look and complete.'),
|
|
741
|
+
...(asString(meta.passage).trim() ? { passage: asString(meta.passage).trim() } : {}),
|
|
742
|
+
...(asString(meta.audioUrl).trim() ? { audioUrl: asString(meta.audioUrl).trim() } : {}),
|
|
743
|
+
...(fromMatchWordToPictureImageUrls(meta.imageUrl)
|
|
744
|
+
? { imageUrl: fromMatchWordToPictureImageUrls(meta.imageUrl) }
|
|
745
|
+
: {}),
|
|
673
746
|
wordBank: asArray(meta.wordBank).map((word) => asString(word)).filter(Boolean),
|
|
674
747
|
items: mappedItems,
|
|
675
748
|
explanation: asString(apiQuestion.explanation),
|
|
@@ -717,6 +790,7 @@ const handlers = {
|
|
|
717
790
|
ANSWER_THE_QUESTION_GROUP: reverseAnswerTheQuestionGroup,
|
|
718
791
|
TRUE_FALSE_GROUP: reverseTrueFalseGroup,
|
|
719
792
|
TRUE_FALSE_CORRECT_GROUP: reverseTrueFalseCorrectGroup,
|
|
793
|
+
CHOOSE_THEN_ANSWER_GROUP: reverseChooseThenAnswerGroup,
|
|
720
794
|
FILL_IN_BLANK_GROUP: reverseFillInBlankGroup,
|
|
721
795
|
MATCHING_WITH_LINES_GROUP: reverseMatchingWithLinesGroup,
|
|
722
796
|
CROSS_OUT_WORD_GROUP: reverseCrossOutWordGroup,
|
|
@@ -761,6 +835,7 @@ const handlers = {
|
|
|
761
835
|
GN_SPEAKING_INTERVIEW: reverseGroupPayload,
|
|
762
836
|
CROSSWORD_PUZZLE: reverseCrosswordPuzzle,
|
|
763
837
|
FIND_WORDS_IN_MATRIX: reverseFindWordsInMatrix,
|
|
838
|
+
SORT_WORDS_INTO_CATEGORIES: reverseSortWordsIntoCategories,
|
|
764
839
|
MATCH_COLUMNS_TO_MAKE_SENTENCES: reverseMatchColumnsToMakeSentences,
|
|
765
840
|
MATCH_WORD_TO_PICTURE_GROUP: reverseMatchWordToPictureGroup,
|
|
766
841
|
WORD_ORDERING_GROUP: reverseWordOrderingGroup,
|
|
@@ -935,6 +1010,51 @@ function reverseFindWordsInMatrix(apiQuestion) {
|
|
|
935
1010
|
},
|
|
936
1011
|
};
|
|
937
1012
|
}
|
|
1013
|
+
function reverseSortWordsIntoCategories(apiQuestion) {
|
|
1014
|
+
const apiContent = asRecord(apiQuestion.content);
|
|
1015
|
+
const apiAnswer = readAnswer(apiQuestion);
|
|
1016
|
+
const meta = asRecord(apiContent.meta);
|
|
1017
|
+
const categories = asArray(apiContent.categories);
|
|
1018
|
+
const words = asArray(apiContent.words);
|
|
1019
|
+
const placements = asArray(apiAnswer.placements);
|
|
1020
|
+
const placementMap = new Map();
|
|
1021
|
+
placements.forEach((item) => {
|
|
1022
|
+
const record = asRecord(item);
|
|
1023
|
+
const wordId = asString(record.wordId);
|
|
1024
|
+
if (wordId && !placementMap.has(wordId)) {
|
|
1025
|
+
placementMap.set(wordId, asString(record.categoryId));
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
const mappedWords = words.map((raw, index) => {
|
|
1029
|
+
const item = asRecord(raw);
|
|
1030
|
+
const id = asString(item.id, `w${index + 1}`);
|
|
1031
|
+
const isExample = item.isExample === true;
|
|
1032
|
+
return {
|
|
1033
|
+
id,
|
|
1034
|
+
text: asString(item.text),
|
|
1035
|
+
categoryId: placementMap.get(id) || '',
|
|
1036
|
+
isExample,
|
|
1037
|
+
points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
1038
|
+
};
|
|
1039
|
+
});
|
|
1040
|
+
return {
|
|
1041
|
+
content: '',
|
|
1042
|
+
answer: {
|
|
1043
|
+
title: asString(meta.title),
|
|
1044
|
+
instruction: asString(meta.instruction, 'Read and sort the words.'),
|
|
1045
|
+
categories: categories.map((raw, index) => {
|
|
1046
|
+
const item = asRecord(raw);
|
|
1047
|
+
return {
|
|
1048
|
+
id: asString(item.id, `cat_${index + 1}`),
|
|
1049
|
+
name: asString(item.name),
|
|
1050
|
+
};
|
|
1051
|
+
}),
|
|
1052
|
+
words: mappedWords,
|
|
1053
|
+
explanation: asString(apiQuestion.explanation),
|
|
1054
|
+
points: mappedWords.reduce((total, word) => (word.isExample ? total : total + (word.points || 0)), 0),
|
|
1055
|
+
},
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
938
1058
|
export function transformApiQuestionToFrontend(apiQuestion) {
|
|
939
1059
|
const questionType = String(apiQuestion.questionType ?? apiQuestion.question_type ?? apiQuestion.type ?? '');
|
|
940
1060
|
const handlerResult = (handlers[questionType] ?? defaultReverse)(apiQuestion);
|
|
@@ -312,7 +312,7 @@ export interface AnswerTheQuestionAnswerData {
|
|
|
312
312
|
groupNumber?: number;
|
|
313
313
|
isExample?: boolean;
|
|
314
314
|
suggestedAnswers?: string[];
|
|
315
|
-
imageUrl?: string;
|
|
315
|
+
imageUrl?: string | string[];
|
|
316
316
|
title?: string;
|
|
317
317
|
subtitle?: string;
|
|
318
318
|
groupContent?: string;
|
|
@@ -47,6 +47,8 @@ import { transformGNSpeakingInterview } from '../../../components/questions/type
|
|
|
47
47
|
import { transformWordFillStructuredForm } from '../../../components/questions/types/word-fill-structured-form/transform';
|
|
48
48
|
import { transformCrosswordPuzzle } from '../../../components/questions/types/crossword-puzzle/transform';
|
|
49
49
|
import { transformFindWordsInMatrix } from '../../../components/questions/types/find-words-in-matrix/transform';
|
|
50
|
+
import { transformSortWordsIntoCategories } from '../../../components/questions/types/sort-words-into-categories/transform';
|
|
51
|
+
import { transformChooseThenAnswerGroup } from '../../../components/questions/types/choose-then-answer-group/transform';
|
|
50
52
|
import { transformMatchColumnsToMakeSentences } from '../../../components/questions/types/match-columns-to-make-sentences/transform';
|
|
51
53
|
import { transformMatchWordToPictureGroup } from '../../../components/questions/types/match-word-to-picture-group/transform';
|
|
52
54
|
import { transformWordOrderingGroup } from '../../../components/questions/types/word-ordering-group/transform';
|
|
@@ -103,6 +105,8 @@ const questionTransformHandlers = {
|
|
|
103
105
|
WORD_FILL_STRUCTURED_FORM: transformWordFillStructuredForm,
|
|
104
106
|
CROSSWORD_PUZZLE: transformCrosswordPuzzle,
|
|
105
107
|
FIND_WORDS_IN_MATRIX: transformFindWordsInMatrix,
|
|
108
|
+
SORT_WORDS_INTO_CATEGORIES: transformSortWordsIntoCategories,
|
|
109
|
+
CHOOSE_THEN_ANSWER_GROUP: transformChooseThenAnswerGroup,
|
|
106
110
|
MATCH_COLUMNS_TO_MAKE_SENTENCES: transformMatchColumnsToMakeSentences,
|
|
107
111
|
MATCH_WORD_TO_PICTURE_GROUP: transformMatchWordToPictureGroup,
|
|
108
112
|
WORD_ORDERING_GROUP: transformWordOrderingGroup,
|