@tinyweb_dev/oe-exam-sdk 0.2.13 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- 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/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 +42 -3
- 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 +5 -1
- package/dist/components/questions/_shared/types/answer-the-question-group.type.js +7 -0
- 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/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 +9 -3
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +19 -0
- 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 +42 -9
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +7 -5
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +18 -4
- package/dist/components/questions/types/answer-the-question-group/transform.js +8 -2
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -2
- 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/match-word-to-picture-group/MatchWordToPictureGroupClient.js +39 -10
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +15 -8
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +8 -3
- package/dist/components/questions/types/match-word-to-picture-group/transform.js +21 -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/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 +98 -2
- 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/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 +6 -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
package/README.md
CHANGED
|
@@ -419,6 +419,18 @@ npm run dev
|
|
|
419
419
|
|
|
420
420
|
## Publish
|
|
421
421
|
|
|
422
|
+
CI release uses `.github/workflows/release_sdk.yml` on tags `sdk-v*` with
|
|
423
|
+
**npm Trusted Publishing (OIDC)** (no long-lived `NPM_TOKEN`).
|
|
424
|
+
|
|
425
|
+
Configure Trusted Publisher on npmjs.com for `@tinyweb_dev/oe-exam-sdk`:
|
|
426
|
+
|
|
427
|
+
- Organization or user: `ocean-edu`
|
|
428
|
+
- Repository: `oe-exam`
|
|
429
|
+
- Workflow filename: `release_sdk.yml`
|
|
430
|
+
- Allowed actions: `npm publish`
|
|
431
|
+
|
|
432
|
+
Manual local publish (maintainer + 2FA):
|
|
433
|
+
|
|
422
434
|
```bash
|
|
423
435
|
cd sdk
|
|
424
436
|
npm version patch
|
|
@@ -643,6 +643,12 @@ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (a
|
|
|
643
643
|
map.set(`${question.id}-item-${index}`, question.id);
|
|
644
644
|
});
|
|
645
645
|
}
|
|
646
|
+
if (part.questionType === 'CHOOSE_THEN_ANSWER_GROUP' &&
|
|
647
|
+
Array.isArray(content?.items)) {
|
|
648
|
+
content.items.forEach((_, index) => {
|
|
649
|
+
map.set(`${question.id}-item-${index}`, question.id);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
646
652
|
});
|
|
647
653
|
});
|
|
648
654
|
return map;
|
|
@@ -8,9 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* 2. Transforms data to renderer-specific props
|
|
9
9
|
* 3. Renders the correct component for each question type
|
|
10
10
|
*/
|
|
11
|
-
import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
|
|
11
|
+
import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, SortWordsIntoCategoriesRenderer, ChooseThenAnswerGroupRenderer, } from './question-renderers';
|
|
12
12
|
import { SpeakingQuestionRenderer } from './speaking/renderers';
|
|
13
|
-
import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordOrderingGroupTake, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, } from '../utils/question-transformers';
|
|
13
|
+
import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordOrderingGroupTake, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, transformSortWordsIntoCategories, transformChooseThenAnswerGroup, } from '../utils/question-transformers';
|
|
14
14
|
import { transformCrossOutWordGroup } from '../utils/cross-out-word-group.transformer';
|
|
15
15
|
import { normalizeColoringAssignments } from '../utils/coloring-assignments';
|
|
16
16
|
export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode = false, onExit, onPartComplete, onPartBack, speakingTheme, initialQuestionIndex, skipTeacherVideo, onTeacherIntroPlayed, currentQuestionIndex, }) {
|
|
@@ -141,6 +141,16 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
141
141
|
const data = transformCrosswordPuzzle(questions);
|
|
142
142
|
return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
143
143
|
}
|
|
144
|
+
case 'SORT_WORDS_INTO_CATEGORIES': {
|
|
145
|
+
const data = transformSortWordsIntoCategories(questions);
|
|
146
|
+
const gradableCount = data.words.filter((word) => !word.isExample).length;
|
|
147
|
+
return (_jsx(SortWordsIntoCategoriesRenderer, { partNumber: partNo, partName: part.name, questionCount: gradableCount || questionCount, instruction: part.instructions || data.instruction, data: data, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
148
|
+
}
|
|
149
|
+
case 'CHOOSE_THEN_ANSWER_GROUP': {
|
|
150
|
+
const data = transformChooseThenAnswerGroup(questions);
|
|
151
|
+
const gradableCount = data.questions.filter((item) => !item.isExample).length;
|
|
152
|
+
return (_jsx(ChooseThenAnswerGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: gradableCount || questionCount, instruction: part.instructions || data.instruction, data: data, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
153
|
+
}
|
|
144
154
|
case 'FIND_WORDS_IN_MATRIX': {
|
|
145
155
|
const data = transformFindWordsInMatrix(questions);
|
|
146
156
|
return (_jsx(FindWordsInMatrixRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, type: data.type, title: data.title, gridSize: data.gridSize, grid: data.grid, categories: data.categories, wordBank: data.wordBank, exampleSelection: data.exampleSelection, correctWords: data.correctWords, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, questionId: questions[0]?.id || 'find-words' }));
|
|
@@ -151,7 +161,7 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
151
161
|
}
|
|
152
162
|
case 'ANSWER_THE_QUESTION_GROUP': {
|
|
153
163
|
const data = transformAnswerTheQuestionGroup(questions);
|
|
154
|
-
return (_jsx(MoversAnswerTheQuestionRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
164
|
+
return (_jsx(MoversAnswerTheQuestionRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
155
165
|
}
|
|
156
166
|
case 'TRUE_FALSE_GROUP': {
|
|
157
167
|
const data = transformTrueFalseGroup(questions);
|
package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChooseThenAnswerGroupPartData } from '../../utils/question-transformers';
|
|
2
|
+
import type { ChooseThenAnswerStudentItemAnswer } from '../../../../questions/_shared/types/choose-then-answer-group.type';
|
|
3
|
+
interface ChooseThenAnswerGroupRendererProps {
|
|
4
|
+
partNumber: number;
|
|
5
|
+
partName?: string;
|
|
6
|
+
questionCount: number;
|
|
7
|
+
instruction: string;
|
|
8
|
+
data: ChooseThenAnswerGroupPartData;
|
|
9
|
+
answers: Record<string, unknown>;
|
|
10
|
+
onAnswerChange: (questionId: string, value: ChooseThenAnswerStudentItemAnswer) => void;
|
|
11
|
+
isReviewMode?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function ChooseThenAnswerGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode, }: ChooseThenAnswerGroupRendererProps): import("react").JSX.Element;
|
|
14
|
+
export {};
|
package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
4
|
+
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
5
|
+
import { ChooseThenAnswerGroupClient } from '../../../../questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient';
|
|
6
|
+
import { parseStudentAnswer } from '../../../../questions/types/choose-then-answer-group/map-choose-then-answer-group-data';
|
|
7
|
+
export function ChooseThenAnswerGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode = false, }) {
|
|
8
|
+
const userAnswers = data.questions.map((item) => {
|
|
9
|
+
if (item.isExample) {
|
|
10
|
+
return { optionId: item.correctOptionId, text: parseStudentAnswer(answers[item.id]).text };
|
|
11
|
+
}
|
|
12
|
+
return parseStudentAnswer(answers[item.id]);
|
|
13
|
+
});
|
|
14
|
+
return (_jsxs("div", { className: "w-full space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx("div", { className: "mt-4", children: _jsx(ChooseThenAnswerGroupClient, { questionData: {
|
|
15
|
+
title: data.title,
|
|
16
|
+
instruction: data.instruction,
|
|
17
|
+
items: data.questions.map((item) => ({
|
|
18
|
+
question: item.question,
|
|
19
|
+
options: item.options,
|
|
20
|
+
optionId: item.correctOptionId,
|
|
21
|
+
isExample: item.isExample,
|
|
22
|
+
points: item.points,
|
|
23
|
+
questionNumber: item.questionNumber,
|
|
24
|
+
})),
|
|
25
|
+
explanation: data.explanation,
|
|
26
|
+
}, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: isReviewMode ? undefined : (index, next) => {
|
|
27
|
+
const item = data.questions[index];
|
|
28
|
+
if (!item || item.isExample)
|
|
29
|
+
return;
|
|
30
|
+
onAnswerChange(item.id, next);
|
|
31
|
+
} }) })] }));
|
|
32
|
+
}
|
package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js
CHANGED
|
@@ -7,6 +7,14 @@ import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
|
|
|
7
7
|
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
8
8
|
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
9
9
|
import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
|
|
10
|
+
import { toAnswerTheQuestionImageUrls } from '../../../../questions/_shared/types/answer-the-question.type';
|
|
11
|
+
function QuestionImages({ imageUrl, alt, className, }) {
|
|
12
|
+
const urls = toAnswerTheQuestionImageUrls(imageUrl);
|
|
13
|
+
if (urls.length === 0) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
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: className }, `${alt}-${imageIndex}`))) }));
|
|
17
|
+
}
|
|
10
18
|
// Single Question Component
|
|
11
19
|
function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctAnswerTextMap, id, }) {
|
|
12
20
|
const isExampleQ = question.isExample;
|
|
@@ -17,7 +25,7 @@ function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctA
|
|
|
17
25
|
const hasCorrectAnswer = isReviewMode && correctAnswer !== undefined;
|
|
18
26
|
const isCorrect = hasCorrectAnswer && userAnswer.trim().toLowerCase() === correctAnswer.trim().toLowerCase();
|
|
19
27
|
const isWrong = hasCorrectAnswer && !!userAnswer && !isCorrect;
|
|
20
|
-
return (_jsxs("div", { id: id, className: `flex items-start gap-3 rounded-lg p-3 ${isExampleQ ? 'border-2 border-orange-200 bg-orange-50' : ''}`, children: [_jsx("span", { className: `flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-sm font-bold text-white ${isExampleQ ? 'bg-orange-400' : isCorrect ? 'bg-green-500' : isWrong ? 'bg-red-500' : 'bg-blue-500'}`, children: question.questionNumber }), _jsxs("div", { className: "flex flex-1 flex-col gap-2", children: [
|
|
28
|
+
return (_jsxs("div", { id: id, className: `flex items-start gap-3 rounded-lg p-3 ${isExampleQ ? 'border-2 border-orange-200 bg-orange-50' : ''}`, children: [_jsx("span", { className: `flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-sm font-bold text-white ${isExampleQ ? 'bg-orange-400' : isCorrect ? 'bg-green-500' : isWrong ? 'bg-red-500' : 'bg-blue-500'}`, children: question.questionNumber }), _jsxs("div", { className: "flex flex-1 flex-col gap-2", children: [_jsx(QuestionImages, { imageUrl: question.imageUrl, alt: `Q${question.questionNumber}`, className: "max-h-[120px] rounded-lg object-contain" }), _jsxs("div", { className: "flex flex-wrap items-baseline gap-2", children: [_jsx("span", { className: "font-medium text-gray-800", children: question.question }), question.prefixText && _jsx("span", { className: "text-gray-600", children: question.prefixText }), _jsx("input", { type: "text", value: displayValue, onChange: (e) => onAnswerChange(question.id, e.target.value), disabled: isDisabled, placeholder: isExampleQ ? '' : 'Type your answer...', className: `min-w-[140px] flex-1 border-b-2 bg-transparent px-1 py-1 text-center outline-none focus:border-blue-500 disabled:cursor-default ${isExampleQ ? 'border-orange-400 font-semibold text-orange-700 disabled:opacity-100' :
|
|
21
29
|
isCorrect ? 'border-green-500 text-green-700' :
|
|
22
30
|
isWrong ? 'border-red-500 text-red-700' :
|
|
23
31
|
'border-gray-400 disabled:opacity-60'}` }), question.suffixText && _jsx("span", { className: "text-gray-600", children: question.suffixText }), isExampleQ && (_jsx("span", { className: "rounded bg-orange-100 px-2 py-0.5 text-xs font-semibold text-orange-600", children: "Example" }))] }), !isExampleQ && isWrong && hasCorrectAnswer && (_jsxs("p", { className: "text-sm text-green-600", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", correctAnswer] })), !isExampleQ && hasCorrectAnswer && !userAnswer && (_jsxs("p", { className: "text-sm text-green-600", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", correctAnswer] })), question.suggestedAnswers && question.suggestedAnswers.length > 0 && !isDisabled && (_jsx("div", { className: "flex flex-wrap gap-2", children: question.suggestedAnswers.map((sa, idx) => (_jsx("button", { type: "button", onClick: () => onAnswerChange(question.id, sa), className: "rounded-full border border-blue-300 bg-blue-50 px-3 py-1 text-sm text-blue-700 transition-colors hover:bg-blue-100", children: sa }, idx))) }))] })] }));
|
|
@@ -26,5 +34,5 @@ export function MoversAnswerTheQuestionRenderer({ partNumber, partName, question
|
|
|
26
34
|
// Separate example questions from regular questions
|
|
27
35
|
const exampleQuestions = questions.filter((q) => q.isExample);
|
|
28
36
|
const regularQuestions = questions.filter((q) => !q.isExample);
|
|
29
|
-
return (_jsxs("div", { className: "flex flex-col h-full gap-4 sm:gap-6 overflow-y-auto custom-scrollbar", children: [_jsxs("div", { className: "flex flex-col gap-4 p-3 sm:px-6 sm:pt-2", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), audioUrl && (_jsx("div", { className: "relative z-10", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true }) })), imageUrl && (_jsx("div", { className: "relative z-0 flex items-center justify-center", children: _jsx(
|
|
37
|
+
return (_jsxs("div", { className: "flex flex-col h-full gap-4 sm:gap-6 overflow-y-auto custom-scrollbar", children: [_jsxs("div", { className: "flex flex-col gap-4 p-3 sm:px-6 sm:pt-2", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), audioUrl && (_jsx("div", { className: "relative z-10", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true }) })), toAnswerTheQuestionImageUrls(imageUrl).length > 0 && (_jsx("div", { className: "relative z-0 flex items-center justify-center", children: _jsx(QuestionImages, { imageUrl: imageUrl, alt: "Shared illustration", className: "max-h-[450px] rounded-lg object-contain shadow-md" }) })), groupContent && (_jsx("div", { className: "rounded-lg bg-amber-50 p-4 text-base leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: groupContent } })), exampleQuestions.length > 0 && (_jsx("div", { className: "flex flex-col gap-4 mt-2", children: exampleQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswerTextMap: correctAnswerTextMap }, question.id))) }))] }), exampleQuestions.length > 0 && regularQuestions.length > 0 && (_jsx("div", { className: "px-3 sm:px-6", children: _jsx("hr", { className: "border-t border-slate-200/60 my-1" }) })), _jsx("div", { className: "grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-5 p-3 sm:px-6 sm:pb-6", children: regularQuestions.map((question) => (_jsx("div", { id: `question-${question.id}`, className: "w-full rounded-xl border border-gray-200 bg-white shadow-sm transition-all hover:border-indigo-200 hover:shadow-md", children: _jsx(QuestionItem, { question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswerTextMap: correctAnswerTextMap }) }, question.id))) })] }));
|
|
30
38
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SortWordsIntoCategoriesPartData } from '../../utils/question-transformers';
|
|
2
|
+
import type { SortWordsPlacement } from '../../../../questions/_shared/types/sort-words-into-categories.type';
|
|
3
|
+
interface SortWordsIntoCategoriesRendererProps {
|
|
4
|
+
partNumber: number;
|
|
5
|
+
partName?: string;
|
|
6
|
+
questionCount: number;
|
|
7
|
+
instruction: string;
|
|
8
|
+
data: SortWordsIntoCategoriesPartData;
|
|
9
|
+
answers: Record<string, unknown>;
|
|
10
|
+
onAnswerChange: (questionId: string, value: {
|
|
11
|
+
placements: SortWordsPlacement[];
|
|
12
|
+
}) => void;
|
|
13
|
+
isReviewMode?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function SortWordsIntoCategoriesRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode, }: SortWordsIntoCategoriesRendererProps): import("react").JSX.Element;
|
|
16
|
+
export {};
|
package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
4
|
+
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
5
|
+
import { SortWordsIntoCategoriesClient } from '../../../../questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient';
|
|
6
|
+
import { padPlacements, readPlacements } from '../../../../questions/types/sort-words-into-categories/map-sort-words-into-categories-data';
|
|
7
|
+
export function SortWordsIntoCategoriesRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode = false, }) {
|
|
8
|
+
const stored = readPlacements(answers[data.questionId]);
|
|
9
|
+
const exampleSeed = data.words.map((word) => ({
|
|
10
|
+
wordId: word.id,
|
|
11
|
+
categoryId: word.isExample ? word.categoryId : '',
|
|
12
|
+
}));
|
|
13
|
+
const userPlacements = padPlacements(data.words, stored ?? exampleSeed);
|
|
14
|
+
return (_jsxs("div", { className: "w-full space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx("div", { className: "mt-4", children: _jsx(SortWordsIntoCategoriesClient, { questionData: {
|
|
15
|
+
title: data.title,
|
|
16
|
+
instruction: data.instruction,
|
|
17
|
+
categories: data.categories,
|
|
18
|
+
words: data.words,
|
|
19
|
+
explanation: data.explanation,
|
|
20
|
+
}, isReviewMode: isReviewMode, userPlacements: userPlacements, onPlacementsChange: isReviewMode ? undefined : (next) => {
|
|
21
|
+
onAnswerChange(data.questionId, { placements: next });
|
|
22
|
+
} }) })] }));
|
|
23
|
+
}
|
|
@@ -25,4 +25,6 @@ export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchG
|
|
|
25
25
|
export { MoversMatchingWithLinesGroupRenderer } from './MoversMatchingWithLinesGroupRenderer';
|
|
26
26
|
export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
|
|
27
27
|
export { FindWordsInMatrixRenderer } from './FindWordsInMatrixRenderer';
|
|
28
|
+
export { SortWordsIntoCategoriesRenderer } from './SortWordsIntoCategoriesRenderer';
|
|
29
|
+
export { ChooseThenAnswerGroupRenderer } from './ChooseThenAnswerGroupRenderer';
|
|
28
30
|
export { ArticlesViewer } from './ArticlesViewer';
|
|
@@ -29,4 +29,6 @@ export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchG
|
|
|
29
29
|
export { MoversMatchingWithLinesGroupRenderer } from './MoversMatchingWithLinesGroupRenderer';
|
|
30
30
|
export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
|
|
31
31
|
export { FindWordsInMatrixRenderer } from './FindWordsInMatrixRenderer';
|
|
32
|
+
export { SortWordsIntoCategoriesRenderer } from './SortWordsIntoCategoriesRenderer';
|
|
33
|
+
export { ChooseThenAnswerGroupRenderer } from './ChooseThenAnswerGroupRenderer';
|
|
32
34
|
export { ArticlesViewer } from './ArticlesViewer';
|
|
@@ -93,7 +93,8 @@ function findParentQuestion(subQuestionId, parts) {
|
|
|
93
93
|
part.questionType === 'WORD_ORDER_AND_MATCH_GROUP' ||
|
|
94
94
|
part.questionType === 'SPONTANEOUS_QA_GROUP' ||
|
|
95
95
|
part.questionType === 'ANSWER_THE_QUESTION_GROUP' ||
|
|
96
|
-
part.questionType === 'WORD_ORDERING_GROUP'
|
|
96
|
+
part.questionType === 'WORD_ORDERING_GROUP' ||
|
|
97
|
+
part.questionType === 'CHOOSE_THEN_ANSWER_GROUP') {
|
|
97
98
|
const parent = part.questions.find((question) => subQuestionId.startsWith(`${question.id}-item-`));
|
|
98
99
|
if (parent) {
|
|
99
100
|
return {
|
|
@@ -205,7 +206,29 @@ function findQuestionType(questionId, parts) {
|
|
|
205
206
|
/**
|
|
206
207
|
* Wrap answer in proper format for backend grading
|
|
207
208
|
*/
|
|
209
|
+
function normalizeSortWordsIntoCategoriesAnswer(answer) {
|
|
210
|
+
const raw = answer && typeof answer === 'object' && !Array.isArray(answer)
|
|
211
|
+
? answer.placements
|
|
212
|
+
: Array.isArray(answer)
|
|
213
|
+
? answer
|
|
214
|
+
: [];
|
|
215
|
+
const source = Array.isArray(raw) ? raw : [];
|
|
216
|
+
return {
|
|
217
|
+
placements: source.map((item) => {
|
|
218
|
+
const record = item && typeof item === 'object'
|
|
219
|
+
? item
|
|
220
|
+
: {};
|
|
221
|
+
return {
|
|
222
|
+
wordId: typeof record.wordId === 'string' ? record.wordId : '',
|
|
223
|
+
categoryId: typeof record.categoryId === 'string' ? record.categoryId : '',
|
|
224
|
+
};
|
|
225
|
+
}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
208
228
|
function wrapAnswer(answer, questionType) {
|
|
229
|
+
if (questionType === 'SORT_WORDS_INTO_CATEGORIES') {
|
|
230
|
+
return normalizeSortWordsIntoCategoriesAnswer(answer);
|
|
231
|
+
}
|
|
209
232
|
// For single/multiple choice, wrap in { answer: value }
|
|
210
233
|
if (typeof answer === 'number') {
|
|
211
234
|
return { answer };
|
|
@@ -373,6 +396,16 @@ export function transformAnswersForSubmission(answers, parts) {
|
|
|
373
396
|
if (parentType === 'CROSS_OUT_WORD_GROUP') {
|
|
374
397
|
return normalizeCrossOutWordGroupAnswer(raw);
|
|
375
398
|
}
|
|
399
|
+
if (parentType === 'CHOOSE_THEN_ANSWER_GROUP') {
|
|
400
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
401
|
+
const obj = raw;
|
|
402
|
+
return {
|
|
403
|
+
optionId: typeof obj.optionId === 'string' ? obj.optionId : '',
|
|
404
|
+
text: typeof obj.text === 'string' ? obj.text : '',
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return { optionId: '', text: '' };
|
|
408
|
+
}
|
|
376
409
|
if (parentType === 'TRUE_FALSE_CORRECT_GROUP') {
|
|
377
410
|
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
378
411
|
const obj = raw;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Each transformer handles specific question type data structure.
|
|
6
6
|
*/
|
|
7
7
|
import type { FillBlankQuestion, ChooseBestAnswerQuestion, PictureChooseSubQuestion, PictureFillBlankSubQuestion, ReadingPassageQuestion, LabelThePictureLabel, FillWordHintSubQuestion, ColoringHintItem, ColoringRegion } from '../types';
|
|
8
|
-
export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_ORDERING_GROUP' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
|
|
8
|
+
export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_ORDERING_GROUP' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'SORT_WORDS_INTO_CATEGORIES' | 'CHOOSE_THEN_ANSWER_GROUP' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
|
|
9
9
|
export interface ApiQuestion {
|
|
10
10
|
id: string;
|
|
11
11
|
content: any;
|
|
@@ -233,7 +233,7 @@ export interface AnswerTheQuestionQuestion {
|
|
|
233
233
|
id: string;
|
|
234
234
|
questionNumber: number;
|
|
235
235
|
question: string;
|
|
236
|
-
imageUrl?: string;
|
|
236
|
+
imageUrl?: string | string[];
|
|
237
237
|
prefixText?: string;
|
|
238
238
|
suffixText?: string;
|
|
239
239
|
suggestedAnswers?: string[];
|
|
@@ -243,7 +243,7 @@ export interface AnswerTheQuestionQuestion {
|
|
|
243
243
|
export declare function transformAnswerTheQuestion(questions: ApiQuestion[]): {
|
|
244
244
|
questions: AnswerTheQuestionQuestion[];
|
|
245
245
|
instruction: string;
|
|
246
|
-
imageUrl?: string;
|
|
246
|
+
imageUrl?: string | string[];
|
|
247
247
|
title?: string;
|
|
248
248
|
groupContent?: string;
|
|
249
249
|
};
|
|
@@ -251,6 +251,7 @@ export declare function transformAnswerTheQuestionGroup(questions: ApiQuestion[]
|
|
|
251
251
|
questions: AnswerTheQuestionQuestion[];
|
|
252
252
|
instruction: string;
|
|
253
253
|
groupContent?: string;
|
|
254
|
+
imageUrl?: string | string[];
|
|
254
255
|
};
|
|
255
256
|
export interface TrueFalseQuestion {
|
|
256
257
|
id: string;
|
|
@@ -532,6 +533,44 @@ export interface FindWordsInMatrixPartData {
|
|
|
532
533
|
explanation?: string;
|
|
533
534
|
}
|
|
534
535
|
export declare function transformFindWordsInMatrix(questions: ApiQuestion[]): FindWordsInMatrixPartData;
|
|
536
|
+
export interface SortWordsIntoCategoriesPartData {
|
|
537
|
+
questionId: string;
|
|
538
|
+
title?: string;
|
|
539
|
+
instruction: string;
|
|
540
|
+
categories: Array<{
|
|
541
|
+
id: string;
|
|
542
|
+
name: string;
|
|
543
|
+
}>;
|
|
544
|
+
words: Array<{
|
|
545
|
+
id: string;
|
|
546
|
+
text: string;
|
|
547
|
+
categoryId: string;
|
|
548
|
+
isExample?: boolean;
|
|
549
|
+
points: number;
|
|
550
|
+
}>;
|
|
551
|
+
explanation?: string;
|
|
552
|
+
}
|
|
553
|
+
export declare function transformSortWordsIntoCategories(questions: ApiQuestion[]): SortWordsIntoCategoriesPartData;
|
|
554
|
+
export interface ChooseThenAnswerGroupQuestion {
|
|
555
|
+
id: string;
|
|
556
|
+
questionNumber: number;
|
|
557
|
+
question: string;
|
|
558
|
+
options: Array<{
|
|
559
|
+
id: string;
|
|
560
|
+
text: string;
|
|
561
|
+
}>;
|
|
562
|
+
correctOptionId: string;
|
|
563
|
+
isExample: boolean;
|
|
564
|
+
points: number;
|
|
565
|
+
}
|
|
566
|
+
export interface ChooseThenAnswerGroupPartData {
|
|
567
|
+
questionId: string;
|
|
568
|
+
title?: string;
|
|
569
|
+
instruction: string;
|
|
570
|
+
questions: ChooseThenAnswerGroupQuestion[];
|
|
571
|
+
explanation?: string;
|
|
572
|
+
}
|
|
573
|
+
export declare function transformChooseThenAnswerGroup(questions: ApiQuestion[]): ChooseThenAnswerGroupPartData;
|
|
535
574
|
export interface MatchingWithLinesGroupQuestionItem {
|
|
536
575
|
id: string;
|
|
537
576
|
questionNumber: number;
|
|
@@ -829,7 +829,8 @@ export function transformAnswerTheQuestion(questions) {
|
|
|
829
829
|
return {
|
|
830
830
|
questions: transformed,
|
|
831
831
|
instruction: 'ANSWER THE QUESTION',
|
|
832
|
-
|
|
832
|
+
// Do not lift the first item image here — that duplicates content.imageUrl.
|
|
833
|
+
imageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl,
|
|
833
834
|
title: firstContent?.title,
|
|
834
835
|
groupContent: groupPayload?.groupContent || firstContent?.groupContent,
|
|
835
836
|
};
|
|
@@ -857,6 +858,7 @@ export function transformAnswerTheQuestionGroup(questions) {
|
|
|
857
858
|
id: `${parent.id}-item-${itemIndex}`,
|
|
858
859
|
questionNumber: item.questionNumber || itemIndex + 1,
|
|
859
860
|
question: item.content?.question || '',
|
|
861
|
+
imageUrl: item.content?.imageUrl,
|
|
860
862
|
isExample,
|
|
861
863
|
exampleAnswer: isExample ? exampleAnswer : undefined,
|
|
862
864
|
};
|
|
@@ -865,6 +867,7 @@ export function transformAnswerTheQuestionGroup(questions) {
|
|
|
865
867
|
questions: transformed,
|
|
866
868
|
instruction: content?.meta?.instruction || 'Read the text again and answer the questions.',
|
|
867
869
|
groupContent: content?.meta?.passage,
|
|
870
|
+
imageUrl: content?.meta?.imageUrl,
|
|
868
871
|
};
|
|
869
872
|
}
|
|
870
873
|
export function transformTrueFalse(questions) {
|
|
@@ -1379,6 +1382,73 @@ export function transformFindWordsInMatrix(questions) {
|
|
|
1379
1382
|
explanation: question?.explanation,
|
|
1380
1383
|
};
|
|
1381
1384
|
}
|
|
1385
|
+
export function transformSortWordsIntoCategories(questions) {
|
|
1386
|
+
const question = questions[0];
|
|
1387
|
+
const content = (question?.content || {});
|
|
1388
|
+
const correctAnswer = (question?.correct_answer || question?.correctAnswer || {});
|
|
1389
|
+
const placementMap = new Map();
|
|
1390
|
+
(Array.isArray(correctAnswer.placements) ? correctAnswer.placements : []).forEach((item) => {
|
|
1391
|
+
if (item?.wordId && !placementMap.has(item.wordId)) {
|
|
1392
|
+
placementMap.set(item.wordId, item.categoryId || '');
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
const words = (Array.isArray(content.words) ? content.words : []).map((item, index) => {
|
|
1396
|
+
const id = item?.id || `w${index + 1}`;
|
|
1397
|
+
const isExample = item?.isExample === true;
|
|
1398
|
+
return {
|
|
1399
|
+
id,
|
|
1400
|
+
text: item?.text || '',
|
|
1401
|
+
categoryId: placementMap.get(id) || '',
|
|
1402
|
+
isExample,
|
|
1403
|
+
points: isExample ? 0 : (typeof item?.points === 'number' ? item.points : 1),
|
|
1404
|
+
};
|
|
1405
|
+
});
|
|
1406
|
+
return {
|
|
1407
|
+
questionId: question?.id || 'sort-words',
|
|
1408
|
+
title: content.meta?.title || '',
|
|
1409
|
+
instruction: question?.instruction ||
|
|
1410
|
+
content.meta?.instruction ||
|
|
1411
|
+
'Read and sort the words.',
|
|
1412
|
+
categories: (Array.isArray(content.categories) ? content.categories : []).map((item, index) => ({
|
|
1413
|
+
id: item?.id || `cat_${index + 1}`,
|
|
1414
|
+
name: item?.name || '',
|
|
1415
|
+
})),
|
|
1416
|
+
words,
|
|
1417
|
+
explanation: question?.explanation,
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
export function transformChooseThenAnswerGroup(questions) {
|
|
1421
|
+
const parent = questions[0];
|
|
1422
|
+
const content = (parent?.content || {});
|
|
1423
|
+
const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
|
|
1424
|
+
? parent.correct_answer.answer
|
|
1425
|
+
: [];
|
|
1426
|
+
const items = Array.isArray(content.items) ? content.items : [];
|
|
1427
|
+
return {
|
|
1428
|
+
questionId: parent?.id || 'choose-then-answer',
|
|
1429
|
+
title: content.meta?.title || '',
|
|
1430
|
+
instruction: parent?.instruction ||
|
|
1431
|
+
content.meta?.instruction ||
|
|
1432
|
+
'Read and choose. Then answer for you.',
|
|
1433
|
+
questions: items.map((item, index) => {
|
|
1434
|
+
const fallback = parentCorrect[index];
|
|
1435
|
+
const optionId = item.correctAnswer?.answer?.optionId || fallback?.optionId || '';
|
|
1436
|
+
return {
|
|
1437
|
+
id: `${parent?.id || 'choose-then-answer'}-item-${index}`,
|
|
1438
|
+
questionNumber: item.questionNumber || index + 1,
|
|
1439
|
+
question: item.content?.question || '',
|
|
1440
|
+
options: (Array.isArray(item.content?.options) ? item.content.options : []).map((option, optionIndex) => ({
|
|
1441
|
+
id: option?.id || `opt-${optionIndex + 1}`,
|
|
1442
|
+
text: option?.text || '',
|
|
1443
|
+
})),
|
|
1444
|
+
correctOptionId: optionId,
|
|
1445
|
+
isExample: item.isExample === true,
|
|
1446
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
1447
|
+
};
|
|
1448
|
+
}),
|
|
1449
|
+
explanation: parent?.explanation,
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1382
1452
|
export function transformMatchingWithLinesGroup(questions) {
|
|
1383
1453
|
const parent = questions[0];
|
|
1384
1454
|
const content = (parent?.content || {});
|
|
@@ -347,6 +347,16 @@ export const QUESTION_TYPE_CONFIG = {
|
|
|
347
347
|
requiresContent: true,
|
|
348
348
|
hasDedicatedComponent: true,
|
|
349
349
|
},
|
|
350
|
+
SORT_WORDS_INTO_CATEGORIES: {
|
|
351
|
+
label: 'Sắp xếp từ vào nhóm',
|
|
352
|
+
requiresContent: true,
|
|
353
|
+
hasDedicatedComponent: true,
|
|
354
|
+
},
|
|
355
|
+
CHOOSE_THEN_ANSWER_GROUP: {
|
|
356
|
+
label: 'Chọn đáp án rồi trả lời',
|
|
357
|
+
requiresContent: true,
|
|
358
|
+
hasDedicatedComponent: true,
|
|
359
|
+
},
|
|
350
360
|
MATCH_COLUMNS_TO_MAKE_SENTENCES: {
|
|
351
361
|
label: 'Ghép cột thành câu',
|
|
352
362
|
requiresContent: true,
|
|
@@ -413,6 +423,8 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
|
|
|
413
423
|
'WORD_FILL_STRUCTURED_FORM',
|
|
414
424
|
'CROSSWORD_PUZZLE',
|
|
415
425
|
'FIND_WORDS_IN_MATRIX',
|
|
426
|
+
'SORT_WORDS_INTO_CATEGORIES',
|
|
427
|
+
'CHOOSE_THEN_ANSWER_GROUP',
|
|
416
428
|
'MATCH_COLUMNS_TO_MAKE_SENTENCES',
|
|
417
429
|
'MATCH_WORD_TO_PICTURE_GROUP',
|
|
418
430
|
'WORD_ORDERING_GROUP',
|
|
@@ -5,13 +5,17 @@ export interface AnswerTheQuestionGroupItemData {
|
|
|
5
5
|
isExample?: boolean;
|
|
6
6
|
points: number;
|
|
7
7
|
questionNumber: number;
|
|
8
|
+
imageUrl?: string | string[];
|
|
8
9
|
}
|
|
9
10
|
export declare function toAnswerTheQuestionGroupImageUrls(value?: unknown): string[];
|
|
11
|
+
export declare function fromAnswerTheQuestionGroupImageUrls(value?: unknown): string | string[] | undefined;
|
|
10
12
|
export interface AnswerTheQuestionGroupData {
|
|
11
13
|
instruction: string;
|
|
12
14
|
passage?: string;
|
|
15
|
+
/** Group-level listening audio (one player for the whole group). */
|
|
16
|
+
audioUrl?: string;
|
|
13
17
|
/** Shared images above the question list (1 or many). */
|
|
14
|
-
imageUrl?: string[];
|
|
18
|
+
imageUrl?: string | string[];
|
|
15
19
|
items: AnswerTheQuestionGroupItemData[];
|
|
16
20
|
explanation?: string;
|
|
17
21
|
points?: number;
|
|
@@ -10,3 +10,10 @@ export function toAnswerTheQuestionGroupImageUrls(value) {
|
|
|
10
10
|
}
|
|
11
11
|
return [];
|
|
12
12
|
}
|
|
13
|
+
export function fromAnswerTheQuestionGroupImageUrls(value) {
|
|
14
|
+
const urls = toAnswerTheQuestionGroupImageUrls(value);
|
|
15
|
+
if (urls.length === 0) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return urls.length === 1 ? urls[0] : urls;
|
|
19
|
+
}
|
|
@@ -23,13 +23,15 @@ export interface AnswerTheQuestionData {
|
|
|
23
23
|
groupNumber?: number;
|
|
24
24
|
isExample?: boolean;
|
|
25
25
|
suggestedAnswers?: string[];
|
|
26
|
-
imageUrl
|
|
26
|
+
imageUrl?: string | string[];
|
|
27
27
|
title?: string;
|
|
28
28
|
subtitle?: string;
|
|
29
29
|
groupContent?: string;
|
|
30
30
|
groups?: AnswerTheQuestionGroup[];
|
|
31
31
|
explanation?: string;
|
|
32
32
|
}
|
|
33
|
+
export declare function toAnswerTheQuestionImageUrls(value?: unknown): string[];
|
|
34
|
+
export declare function fromAnswerTheQuestionImageUrls(value?: unknown): string | string[] | undefined;
|
|
33
35
|
import type { ValidationRef } from './look-and-choose-answer.type';
|
|
34
36
|
export type { ValidationRef };
|
|
35
37
|
export interface AnswerTheQuestionCreatorProps {
|
|
@@ -1 +1,19 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export function toAnswerTheQuestionImageUrls(value) {
|
|
2
|
+
if (Array.isArray(value)) {
|
|
3
|
+
return value
|
|
4
|
+
.filter((url) => typeof url === 'string')
|
|
5
|
+
.map((url) => url.trim())
|
|
6
|
+
.filter((url) => url !== '');
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
9
|
+
return [value.trim()];
|
|
10
|
+
}
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
export function fromAnswerTheQuestionImageUrls(value) {
|
|
14
|
+
const urls = toAnswerTheQuestionImageUrls(value);
|
|
15
|
+
if (urls.length === 0) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return urls.length === 1 ? urls[0] : urls;
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const CHOOSE_THEN_ANSWER_ITEM_TYPE: "CHOOSE_THEN_ANSWER";
|
|
3
|
+
export declare const CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION = "Read and choose. Then answer for you.";
|
|
4
|
+
export interface ChooseThenAnswerOption {
|
|
5
|
+
id: string;
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ChooseThenAnswerItemAnswer {
|
|
9
|
+
optionId: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ChooseThenAnswerStudentItemAnswer {
|
|
12
|
+
optionId: string;
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ChooseThenAnswerGroupItemData {
|
|
16
|
+
question: string;
|
|
17
|
+
options: ChooseThenAnswerOption[];
|
|
18
|
+
optionId: string;
|
|
19
|
+
isExample?: boolean;
|
|
20
|
+
points: number;
|
|
21
|
+
questionNumber: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ChooseThenAnswerGroupData {
|
|
24
|
+
title?: string;
|
|
25
|
+
instruction: string;
|
|
26
|
+
items: ChooseThenAnswerGroupItemData[];
|
|
27
|
+
explanation?: string;
|
|
28
|
+
points?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ChooseThenAnswerGroupCreatorProps {
|
|
31
|
+
initialData?: ChooseThenAnswerGroupData;
|
|
32
|
+
onSave?: (data: ChooseThenAnswerGroupData) => void;
|
|
33
|
+
onCancel?: () => void;
|
|
34
|
+
onChange?: (data: ChooseThenAnswerGroupData) => void;
|
|
35
|
+
externalErrors?: string[];
|
|
36
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
37
|
+
validationRef?: MutableRefObject<{
|
|
38
|
+
triggerValidation: () => Promise<boolean>;
|
|
39
|
+
getFormData?: () => ChooseThenAnswerGroupData;
|
|
40
|
+
} | null>;
|
|
41
|
+
}
|
|
42
|
+
export interface ChooseThenAnswerGroupClientProps {
|
|
43
|
+
questionData: ChooseThenAnswerGroupData;
|
|
44
|
+
isReviewMode?: boolean;
|
|
45
|
+
userAnswers?: ChooseThenAnswerStudentItemAnswer[];
|
|
46
|
+
onAnswerChange?: (index: number, next: ChooseThenAnswerStudentItemAnswer) => void;
|
|
47
|
+
}
|