@tinyweb_dev/oe-exam-sdk 0.2.5 → 0.2.7
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 +10 -2
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.d.ts +22 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +60 -0
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +2 -2
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +58 -14
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.d.ts +13 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +31 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/index.js +2 -0
- package/dist/components/exams/take/types.d.ts +12 -5
- package/dist/components/exams/take/utils/answer-transformers.js +17 -1
- package/dist/components/exams/take/utils/question-transformers.d.ts +45 -1
- package/dist/components/exams/take/utils/question-transformers.js +109 -53
- package/dist/components/questions/_shared/config/question-types.config.js +12 -0
- package/dist/components/questions/_shared/types/fill-in-blank-group.type.d.ts +50 -0
- package/dist/components/questions/_shared/types/fill-in-blank-group.type.js +2 -0
- package/dist/components/questions/_shared/types/fill-in-blank.type.d.ts +16 -1
- package/dist/components/questions/_shared/types/index.d.ts +2 -0
- package/dist/components/questions/_shared/types/index.js +2 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +40 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.js +1 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +4 -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/fill-in-blank/FillInBlankClient.d.ts +1 -1
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +37 -60
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +122 -149
- package/dist/components/questions/types/fill-in-blank/answer-utils.d.ts +34 -0
- package/dist/components/questions/types/fill-in-blank/answer-utils.js +58 -0
- package/dist/components/questions/types/fill-in-blank/register.js +19 -6
- package/dist/components/questions/types/fill-in-blank/transform.js +23 -26
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +64 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +158 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.d.ts +7 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +30 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +20 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +99 -0
- package/dist/components/questions/types/fill-in-blank-group/index.d.ts +5 -0
- package/dist/components/questions/types/fill-in-blank-group/index.js +5 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.d.ts +4 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +102 -0
- package/dist/components/questions/types/fill-in-blank-group/register.d.ts +3 -0
- package/dist/components/questions/types/fill-in-blank-group/register.js +37 -0
- package/dist/components/questions/types/fill-in-blank-group/transform.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/transform.js +68 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts +20 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +109 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +30 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +180 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +6 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +26 -0
- package/dist/components/questions/types/matching-with-lines-group/index.d.ts +4 -0
- package/dist/components/questions/types/matching-with-lines-group/index.js +4 -0
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.d.ts +8 -0
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +104 -0
- package/dist/components/questions/types/matching-with-lines-group/register.d.ts +3 -0
- package/dist/components/questions/types/matching-with-lines-group/register.js +37 -0
- package/dist/components/questions/types/matching-with-lines-group/transform.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/transform.js +44 -0
- package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +10 -0
- package/dist/components/questions/viewer/FillInBlankGroupViewer.js +14 -0
- package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.d.ts +7 -0
- package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.js +48 -0
- package/dist/components/questions/viewer/QuestionViewer.js +6 -0
- package/dist/components/questions/viewer/index.d.ts +2 -0
- package/dist/components/questions/viewer/index.js +2 -0
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +2 -3
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +17 -8
- package/dist/components/results/renderers/review-question-body-dedicated.js +2 -2
- package/dist/components/results/renderers/review-question-body-movers.js +37 -2
- package/dist/components/results/review-data.js +37 -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/fill-in-blank.d.ts +46 -0
- package/dist/shared/lib/utils/fill-in-blank.js +96 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +150 -3
- 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/fill-in-blank-group.d.ts +45 -0
- package/dist/shared/types/questions/fill-in-blank-group.js +1 -0
- package/dist/shared/types/questions/index.d.ts +2 -0
- package/dist/shared/types/questions/index.js +4 -0
- package/dist/shared/types/questions/matching-with-lines-group.d.ts +39 -0
- package/dist/shared/types/questions/matching-with-lines-group.js +1 -0
- package/package.json +1 -1
|
@@ -631,6 +631,12 @@ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (a
|
|
|
631
631
|
map.set(`${question.id}-item-${index}`, question.id);
|
|
632
632
|
});
|
|
633
633
|
}
|
|
634
|
+
if (part.questionType === 'FILL_IN_BLANK_GROUP' &&
|
|
635
|
+
Array.isArray(content?.items)) {
|
|
636
|
+
content.items.forEach((_, index) => {
|
|
637
|
+
map.set(`${question.id}-item-${index}`, question.id);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
634
640
|
});
|
|
635
641
|
});
|
|
636
642
|
return map;
|
|
@@ -8,9 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* 2. Transforms data to renderer-specific props
|
|
9
9
|
* 3. Renders the correct component for each question type
|
|
10
10
|
*/
|
|
11
|
-
import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
|
|
11
|
+
import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
|
|
12
12
|
import { SpeakingQuestionRenderer } from './speaking/renderers';
|
|
13
|
-
import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, } from '../utils/question-transformers';
|
|
13
|
+
import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, } from '../utils/question-transformers';
|
|
14
14
|
import { transformCrossOutWordGroup } from '../utils/cross-out-word-group.transformer';
|
|
15
15
|
import { normalizeColoringAssignments } from '../utils/coloring-assignments';
|
|
16
16
|
export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode = false, onExit, onPartComplete, onPartBack, speakingTheme, initialQuestionIndex, skipTeacherVideo, onTeacherIntroPlayed, currentQuestionIndex, }) {
|
|
@@ -161,6 +161,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
161
161
|
const data = transformTrueFalseCorrectGroup(questions);
|
|
162
162
|
return (_jsx(MoversTrueFalseCorrectGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
163
163
|
}
|
|
164
|
+
case 'FILL_IN_BLANK_GROUP': {
|
|
165
|
+
const data = transformFillInBlankGroup(questions);
|
|
166
|
+
return (_jsx(MoversFillInBlankGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: data.questions.filter((question) => !question.isExample).length, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl || part.audioUrl, imageUrl: data.imageUrl, showHints: data.showHints, hints: data.hints, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
167
|
+
}
|
|
164
168
|
case 'CROSS_OUT_WORD_GROUP': {
|
|
165
169
|
const data = transformCrossOutWordGroup(questions);
|
|
166
170
|
return (_jsx(MoversCrossOutWordGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
@@ -169,6 +173,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
169
173
|
const data = transformWordOrderAndMatchGroup(questions);
|
|
170
174
|
return (_jsx(MoversWordOrderAndMatchGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, matchOptions: data.matchOptions, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
171
175
|
}
|
|
176
|
+
case 'MATCHING_WITH_LINES_GROUP': {
|
|
177
|
+
const data = transformMatchingWithLinesGroup(questions);
|
|
178
|
+
return (_jsx(MoversMatchingWithLinesGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, data: data, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
179
|
+
}
|
|
172
180
|
case 'TRUE_FALSE': {
|
|
173
181
|
const data = transformTrueFalse(questions);
|
|
174
182
|
return (_jsx(MoversTrueFalseRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FILL_IN_BLANK_GROUP Renderer
|
|
3
|
+
* Shared audio/image on top, then each item: picture + fill blanks.
|
|
4
|
+
*/
|
|
5
|
+
import type { FillInBlankGroupQuestion, FillInBlankGroupItemAnswer, FillInBlankGroupStudentItemAnswer } from '../../utils/question-transformers';
|
|
6
|
+
interface MoversFillInBlankGroupRendererProps {
|
|
7
|
+
partNumber: number;
|
|
8
|
+
partName?: string;
|
|
9
|
+
questionCount: number;
|
|
10
|
+
instruction: string;
|
|
11
|
+
audioUrl?: string;
|
|
12
|
+
imageUrl?: string;
|
|
13
|
+
showHints?: boolean;
|
|
14
|
+
hints?: string[];
|
|
15
|
+
questions: FillInBlankGroupQuestion[];
|
|
16
|
+
answers: Record<string, FillInBlankGroupStudentItemAnswer | undefined>;
|
|
17
|
+
onAnswerChange: (questionId: string, value: FillInBlankGroupStudentItemAnswer) => void;
|
|
18
|
+
isReviewMode?: boolean;
|
|
19
|
+
correctAnswerMap?: Record<string, FillInBlankGroupItemAnswer>;
|
|
20
|
+
}
|
|
21
|
+
export declare function MoversFillInBlankGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, imageUrl, showHints, hints, questions, answers, onAnswerChange, isReviewMode, correctAnswerMap, }: MoversFillInBlankGroupRendererProps): import("react").JSX.Element;
|
|
22
|
+
export {};
|
package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* FILL_IN_BLANK_GROUP Renderer
|
|
4
|
+
* Shared audio/image on top, then each item: picture + fill blanks.
|
|
5
|
+
*/
|
|
6
|
+
import { Fragment } from 'react';
|
|
7
|
+
import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
|
|
8
|
+
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
9
|
+
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
10
|
+
import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
|
|
11
|
+
import { extractPlaceholderIndices, isBlankMatch, padStudentAnswers, splitStem, } from '../../../../questions/types/fill-in-blank-group/blank-utils';
|
|
12
|
+
function emptyAnswer(blankCount) {
|
|
13
|
+
return { answers: Array.from({ length: blankCount }, () => '') };
|
|
14
|
+
}
|
|
15
|
+
function ItemCard({ question, answer, onAnswerChange, isReviewMode, correctAnswer, }) {
|
|
16
|
+
const isDisabled = isReviewMode || question.isExample;
|
|
17
|
+
const current = question.isExample
|
|
18
|
+
? { answers: question.exampleAnswers || emptyAnswer(question.blankCount).answers }
|
|
19
|
+
: { answers: padStudentAnswers(answer, question.blankCount) };
|
|
20
|
+
const stem = splitStem(question.question || '{0}');
|
|
21
|
+
const expected = correctAnswer?.answers || [];
|
|
22
|
+
const blankIndices = extractPlaceholderIndices(question.question || '{0}');
|
|
23
|
+
const allCorrect = isReviewMode
|
|
24
|
+
&& !question.isExample
|
|
25
|
+
&& blankIndices.length > 0
|
|
26
|
+
&& blankIndices.every((index) => isBlankMatch(current.answers[index], expected[index]));
|
|
27
|
+
const anyFilled = current.answers.some((value) => value.trim().length > 0);
|
|
28
|
+
const isWrong = isReviewMode && !question.isExample && anyFilled && !allCorrect;
|
|
29
|
+
const setBlank = (blankIndex, value) => {
|
|
30
|
+
const next = [...current.answers];
|
|
31
|
+
next[blankIndex] = value;
|
|
32
|
+
onAnswerChange(question.id, { answers: next });
|
|
33
|
+
};
|
|
34
|
+
return (_jsxs("div", { id: `question-${question.id}`, className: `rounded-2xl border p-3 sm:p-4 transition-all ${question.isExample
|
|
35
|
+
? 'border-amber-200 bg-amber-50/60'
|
|
36
|
+
: allCorrect
|
|
37
|
+
? 'border-emerald-300 bg-emerald-50/30'
|
|
38
|
+
: isWrong
|
|
39
|
+
? 'border-rose-300 bg-rose-50/30'
|
|
40
|
+
: 'border-slate-200 bg-white'}`, children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [_jsx("span", { className: `flex h-7 w-7 items-center justify-center rounded-full text-xs font-black text-white ${question.isExample ? 'bg-amber-500' : allCorrect ? 'bg-emerald-500' : isWrong ? 'bg-rose-500' : 'bg-blue-600'}`, children: question.questionNumber }), question.isExample && (_jsx("span", { className: "rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-extrabold uppercase text-amber-800", children: "Example" }))] }), question.audioUrl && (_jsx("div", { className: "mb-2", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: question.audioUrl, compact: true, dense: true }) })), _jsxs("div", { className: "flex items-center gap-3", children: [question.imageUrl && (_jsx(ResolvedImage, { src: question.imageUrl, alt: `Question ${question.questionNumber}`, className: "h-24 w-24 flex-shrink-0 rounded-lg object-contain" })), _jsx("div", { className: "min-w-0 flex-1 text-[15px] leading-relaxed text-slate-800", children: stem.map((part, partIndex) => {
|
|
41
|
+
if (part.type === 'text') {
|
|
42
|
+
const lines = part.value.split('\n');
|
|
43
|
+
return (_jsx(Fragment, { children: lines.map((line, lineIndex) => (_jsxs(Fragment, { children: [lineIndex > 0 ? _jsx("br", {}) : null, line] }, lineIndex))) }, partIndex));
|
|
44
|
+
}
|
|
45
|
+
const student = current.answers[part.index] || '';
|
|
46
|
+
const alts = expected[part.index] || [];
|
|
47
|
+
const blankCorrect = isReviewMode && !question.isExample && isBlankMatch(student, alts);
|
|
48
|
+
const blankWrong = isReviewMode && !question.isExample && student.trim().length > 0 && !blankCorrect;
|
|
49
|
+
return (_jsx("input", { type: "text", value: student, disabled: isDisabled, onChange: (event) => setBlank(part.index, event.target.value), placeholder: question.isExample ? '' : '...', className: `mx-1 inline-block w-[4.5rem] rounded-md border-2 px-1 py-1 text-center text-sm font-semibold outline-none sm:w-20 ${question.isExample
|
|
50
|
+
? 'border-amber-300 bg-amber-100/70 text-amber-800'
|
|
51
|
+
: blankCorrect
|
|
52
|
+
? 'border-emerald-400 bg-emerald-50 text-emerald-800'
|
|
53
|
+
: blankWrong
|
|
54
|
+
? 'border-rose-400 bg-rose-50 text-rose-800'
|
|
55
|
+
: 'border-blue-200 bg-blue-50/40 text-blue-900 focus:border-blue-500 focus:bg-white'}` }, partIndex));
|
|
56
|
+
}) })] }), isReviewMode && !question.isExample && expected.length > 0 && (_jsxs("div", { className: "mt-2 rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-2 text-sm text-emerald-800", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", expected.map((alts) => alts.join(' / ') || '(trống)').join(' | ')] }))] }));
|
|
57
|
+
}
|
|
58
|
+
export function MoversFillInBlankGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, imageUrl, showHints = false, hints = [], questions, answers, onAnswerChange, isReviewMode = false, correctAnswerMap, }) {
|
|
59
|
+
return (_jsxs("div", { className: "relative flex h-full flex-col gap-4 overflow-y-auto custom-scrollbar sm:gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2 p-3 sm:gap-3 sm:px-6 sm:pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), (audioUrl || imageUrl) && (_jsxs("div", { className: "sticky top-0 z-20 flex flex-col gap-3 border-b border-gray-100 bg-white/95 p-3 pb-2 shadow-sm backdrop-blur sm:px-6", children: [audioUrl && _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true }), imageUrl && (_jsx("div", { className: "flex justify-center", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Shared illustration", className: "max-h-[180px] rounded-lg object-contain" }) }))] })), showHints && hints.length > 0 && !isReviewMode && (_jsx("div", { className: "flex flex-wrap gap-2 px-3 sm:px-6", children: hints.map((hint) => (_jsx("span", { className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: hint }, hint))) })), _jsx("div", { className: "grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 sm:px-6 lg:grid-cols-3", children: questions.map((question) => (_jsx(ItemCard, { question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerMap?.[question.id] }, question.id))) })] }));
|
|
60
|
+
}
|
package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ interface MoversListenAndWriteRendererProps {
|
|
|
10
10
|
exampleText?: string;
|
|
11
11
|
exampleHighlight?: string;
|
|
12
12
|
questions: FillBlankQuestion[];
|
|
13
|
-
answers: Record<string,
|
|
14
|
-
onAnswerChange: (questionId: string, value: string) => void;
|
|
13
|
+
answers: Record<string, unknown>;
|
|
14
|
+
onAnswerChange: (questionId: string, value: string[]) => void;
|
|
15
15
|
isReviewMode?: boolean;
|
|
16
16
|
showHints?: boolean;
|
|
17
17
|
groupContent?: string;
|
package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js
CHANGED
|
@@ -5,22 +5,68 @@ import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-y
|
|
|
5
5
|
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
6
6
|
import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
|
|
7
7
|
import CambridgeYleExampleText from '../../../../../components/themes/cambridge-yle/CambridgeYleExampleText';
|
|
8
|
-
|
|
8
|
+
import { countBlanks, formatAcceptedAnswers, getPrimaryAnswer, isBlankCorrect, toBlankValues, } from '../../../../../shared/lib/utils/fill-in-blank';
|
|
9
|
+
/** Preserve author newlines in fill-blank stems by mapping `\n` → `<br />`. */
|
|
9
10
|
function renderTextWithLineBreaks(text) {
|
|
10
11
|
const lines = text.split('\n');
|
|
11
12
|
return lines.map((line, index) => (_jsxs(Fragment, { children: [index > 0 ? _jsx("br", {}) : null, line] }, index)));
|
|
12
13
|
}
|
|
14
|
+
function blankInputClassName({ isExampleQ, isReviewMode, isCorrect, isWrong, }) {
|
|
15
|
+
if (isExampleQ)
|
|
16
|
+
return 'border-amber-300 bg-amber-100/50 text-amber-800 disabled:opacity-100';
|
|
17
|
+
if (isReviewMode) {
|
|
18
|
+
if (isCorrect)
|
|
19
|
+
return 'border-emerald-400 bg-emerald-50 text-emerald-800';
|
|
20
|
+
if (isWrong)
|
|
21
|
+
return 'border-rose-400 bg-rose-50 text-rose-800';
|
|
22
|
+
return 'border-slate-200 bg-slate-50 text-slate-500';
|
|
23
|
+
}
|
|
24
|
+
return 'border-blue-200 bg-blue-50/30 text-blue-900 focus:border-blue-500 focus:bg-white focus:ring-4 focus:ring-blue-100';
|
|
25
|
+
}
|
|
13
26
|
export function MoversListenAndWriteRenderer({ partNumber, partName, questionCount, instruction, imageUrl, audioUrl, title, exampleText, exampleHighlight, questions, answers, onAnswerChange, isReviewMode = false, showHints = false, groupContent, groupTitle, }) {
|
|
14
27
|
const hasGroupContent = !!(audioUrl || imageUrl || groupContent || exampleText);
|
|
15
28
|
return (_jsxs("div", { className: "flex flex-col h-full gap-4 sm:gap-6 overflow-y-auto custom-scrollbar relative", children: [_jsxs("div", { className: "flex flex-col gap-2 sm:gap-3 p-3 sm:px-6 sm:pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, hasExample: !!exampleText, isReviewMode: isReviewMode })] }), hasGroupContent && (_jsxs("div", { className: "flex flex-col gap-2 sm:gap-3 p-3 sm:px-6 sm:pt-0 sticky top-0 z-20 bg-white/95 backdrop-blur pb-2 shadow-sm border-b border-gray-100", children: [audioUrl && _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true }), _jsxs("div", { className: "max-h-[35vh] overflow-y-auto custom-scrollbar pr-2 flex flex-col gap-3", children: [imageUrl && (_jsx("div", { className: "flex items-center justify-center", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Shared illustration", className: "max-h-[200px] rounded-lg object-contain shadow-md" }) })), groupContent && (_jsxs("div", { className: "rounded-lg bg-amber-50 p-3 text-sm leading-relaxed text-gray-700 shadow-inner", children: [groupTitle && _jsx("h3", { className: "mb-2 font-semibold text-gray-800", children: groupTitle }), groupContent] }))] }), exampleText && (_jsx(CambridgeYleExampleText, { text: exampleText, highlightedWord: exampleHighlight || '' }))] })), _jsxs("div", { className: "flex flex-col gap-4 p-3 sm:gap-6 sm:pb-6 sm:px-6", children: [title && (_jsx("h2", { className: "text-center text-3xl font-bold uppercase text-coral-500", children: title })), _jsx("div", { className: "flex flex-col gap-6 sm:gap-8", children: questions.map((question) => {
|
|
16
29
|
const isExampleQ = question.isExample || false;
|
|
17
30
|
const isDisabled = isReviewMode || isExampleQ;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
const blankCount = countBlanks(question.segments);
|
|
32
|
+
// Examples are pre-filled with the first accepted answer; everyone
|
|
33
|
+
// else shows what the student typed.
|
|
34
|
+
const displayValues = isExampleQ
|
|
35
|
+
? question.acceptedAnswers.map(getPrimaryAnswer)
|
|
36
|
+
: toBlankValues(answers[question.id], blankCount);
|
|
37
|
+
const blankStatuses = displayValues.map((value, index) => {
|
|
38
|
+
const hasValue = !!value.trim();
|
|
39
|
+
const matches = isBlankCorrect(value, question.acceptedAnswers[index]);
|
|
40
|
+
return {
|
|
41
|
+
hasValue,
|
|
42
|
+
isCorrect: isReviewMode && !isExampleQ && hasValue && matches,
|
|
43
|
+
isWrong: isReviewMode && !isExampleQ && hasValue && !matches,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
const hasAnswer = blankStatuses.some((status) => status.hasValue);
|
|
47
|
+
const isCorrect = isReviewMode &&
|
|
48
|
+
!isExampleQ &&
|
|
49
|
+
hasAnswer &&
|
|
50
|
+
blankStatuses.every((status) => status.isCorrect);
|
|
51
|
+
const isWrong = isReviewMode && !isExampleQ && hasAnswer && !isCorrect;
|
|
52
|
+
const setBlankValue = (blankIndex, value) => {
|
|
53
|
+
const next = toBlankValues(answers[question.id], blankCount);
|
|
54
|
+
next[blankIndex] = value;
|
|
55
|
+
onAnswerChange(question.id, next);
|
|
56
|
+
};
|
|
57
|
+
const renderBlankInput = (blankIndex) => {
|
|
58
|
+
const status = blankStatuses[blankIndex] || {
|
|
59
|
+
hasValue: false,
|
|
60
|
+
isCorrect: false,
|
|
61
|
+
isWrong: false,
|
|
62
|
+
};
|
|
63
|
+
return (_jsx("span", { className: "mx-1 inline-block w-[7.5rem] max-w-[40%] min-w-[5.5rem] align-baseline sm:w-[8.5rem]", children: _jsx("input", { type: "text", value: displayValues[blankIndex] || '', onChange: (e) => setBlankValue(blankIndex, e.target.value), disabled: isDisabled, placeholder: isExampleQ ? '' : '...', className: `w-full rounded-md border-2 px-2 py-1 text-center text-sm font-semibold outline-none transition-all sm:text-base ${blankInputClassName({
|
|
64
|
+
isExampleQ,
|
|
65
|
+
isReviewMode,
|
|
66
|
+
isCorrect: status.isCorrect,
|
|
67
|
+
isWrong: status.isWrong,
|
|
68
|
+
})} ${isDisabled ? 'disabled:cursor-not-allowed disabled:opacity-75' : ''}` }) }));
|
|
69
|
+
};
|
|
24
70
|
return (_jsxs("div", { id: `question-${question.id}`, className: `flex flex-col gap-4 rounded-2xl border p-4 sm:p-6 transition-all duration-200 hover:shadow-md ${isExampleQ
|
|
25
71
|
? 'border-amber-300 bg-amber-50/60 shadow-xs'
|
|
26
72
|
: 'border-slate-200/80 bg-white shadow-xs hover:border-blue-300'}`, children: [_jsxs("div", { className: "flex items-center justify-between border-b border-slate-100 pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: `inline-flex items-center justify-center rounded-full px-3.5 py-1 text-sm font-bold text-white shadow-xs ${isExampleQ
|
|
@@ -31,12 +77,10 @@ export function MoversListenAndWriteRenderer({ partNumber, partName, questionCou
|
|
|
31
77
|
: isWrong
|
|
32
78
|
? 'bg-rose-500'
|
|
33
79
|
: 'bg-[#001590]'
|
|
34
|
-
: 'bg-[#001590]'}`, children: isExampleQ ? `
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
: 'border-rose-400 bg-rose-50 text-rose-800'
|
|
40
|
-
: 'border-blue-200 bg-blue-50/30 text-blue-900 focus:border-blue-500 focus:bg-white focus:ring-4 focus:ring-blue-100'} ${isDisabled ? 'disabled:cursor-not-allowed disabled:opacity-75' : ''}` }) }), question.textAfter && (_jsx("span", { className: "font-medium", children: renderTextWithLineBreaks(question.textAfter) })), question.verbHint && (_jsxs("span", { className: "ml-1 italic text-slate-500 font-normal", children: ["(", question.verbHint, ")"] }))] }), showHints && question.hints && question.hints.length > 0 && !isReviewMode && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 pt-2 border-t border-slate-100", children: [_jsx("span", { className: "text-xs font-medium text-slate-500", children: "G\u1EE3i \u00FD:" }), question.hints.map((hint, idx) => (_jsx("button", { type: "button", onClick: () => onAnswerChange(question.id, hint), className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700 shadow-2xs transition-all hover:bg-blue-600 hover:text-white hover:border-blue-600 active:scale-95", children: hint }, idx)))] })), isReviewMode && !isExampleQ && question.correctAnswer && (_jsx("div", { className: "mt-1 rounded-lg bg-emerald-50/80 p-2.5 text-sm border border-emerald-200/80", children: _jsxs("span", { className: "font-semibold text-emerald-800", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("span", { className: "font-bold underline", children: question.correctAnswer })] }) }))] }, question.id));
|
|
80
|
+
: 'bg-[#001590]'}`, children: isExampleQ ? `Và dụ` : `${question.questionNumber}.` }), isExampleQ && (_jsx("span", { className: "rounded-md bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: "Example" }))] }), isReviewMode && !isExampleQ && (_jsx("div", { children: isCorrect ? (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700 border border-emerald-200", children: "\u00E2\u0153\u201C \u00C4\u0090\u00C3\u00BAng" })) : isWrong ? (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-rose-50 px-2.5 py-0.5 text-xs font-semibold text-rose-700 border border-rose-200", children: "\u00E2\u0153\u2022 Ch\u00C6\u00B0a \u00C4\u2018\u00C3\u00BAng" })) : (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-medium text-slate-600", children: "Ch\u00C6\u00B0a tr\u00E1\u00BA\u00A3 l\u00E1\u00BB\u009Di" })) }))] }), question.audioUrl && (_jsx("div", { className: "w-full my-1", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: question.audioUrl, compact: true, dense: true }) })), question.imageUrl && (_jsx("div", { className: "flex items-center justify-center rounded-xl bg-slate-50/80 p-3 border border-slate-100 max-w-md mx-auto w-full", children: _jsx(ResolvedImage, { src: question.imageUrl, alt: `Question ${question.questionNumber} illustration`, className: "max-h-[200px] sm:max-h-[220px] w-auto rounded-lg object-contain drop-shadow-xs" }) })), _jsxs("div", { className: "text-base sm:text-lg leading-relaxed text-slate-800 pt-1", children: [question.segments.map((segment, segmentIndex) => segment.type === 'text' ? (_jsx("span", { className: "font-medium", children: renderTextWithLineBreaks(segment.value) }, `text-${segmentIndex}`)) : (_jsx(Fragment, { children: renderBlankInput(segment.index) }, `blank-${segment.index}-${segmentIndex}`))), question.verbHint && (_jsxs("span", { className: "ml-1 italic text-slate-500 font-normal", children: ["(", question.verbHint, ")"] }))] }), showHints && question.hints && question.hints.length > 0 && !isReviewMode && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 pt-2 border-t border-slate-100", children: [_jsx("span", { className: "text-xs font-medium text-slate-500", children: "G\u00E1\u00BB\u00A3i \u00C3\u00BD:" }), question.hints.map((hint, idx) => (_jsx("button", { type: "button", onClick: () => {
|
|
81
|
+
const current = toBlankValues(answers[question.id], blankCount);
|
|
82
|
+
const emptyIndex = current.findIndex((value) => !value.trim());
|
|
83
|
+
setBlankValue(emptyIndex >= 0 ? emptyIndex : 0, hint);
|
|
84
|
+
}, className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700 shadow-2xs transition-all hover:bg-blue-600 hover:text-white hover:border-blue-600 active:scale-95", children: hint }, idx)))] })), isReviewMode && !isExampleQ && question.acceptedAnswers.some((alts) => alts.length > 0) && (_jsx("div", { className: "mt-1 rounded-lg bg-emerald-50/80 p-2.5 text-sm border border-emerald-200/80", children: _jsxs("span", { className: "font-semibold text-emerald-800", children: ["\u00C4\u0090\u00C3\u00A1p \u00C3\u00A1n \u00C4\u2018\u00C3\u00BAng:", ' ', _jsx("span", { className: "font-bold underline", children: question.acceptedAnswers.map(formatAcceptedAnswers).filter(Boolean).join(' | ') })] }) }))] }, question.id));
|
|
41
85
|
}) })] })] }));
|
|
42
86
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MatchingWithLinesGroupPartData } from '../../utils/question-transformers';
|
|
2
|
+
interface MoversMatchingWithLinesGroupRendererProps {
|
|
3
|
+
partNumber: number;
|
|
4
|
+
partName?: string;
|
|
5
|
+
questionCount: number;
|
|
6
|
+
instruction: string;
|
|
7
|
+
data: MatchingWithLinesGroupPartData;
|
|
8
|
+
answers: Record<string, string | undefined>;
|
|
9
|
+
onAnswerChange: (questionId: string, value: string) => void;
|
|
10
|
+
isReviewMode?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function MoversMatchingWithLinesGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode, }: MoversMatchingWithLinesGroupRendererProps): import("react").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
4
|
+
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
5
|
+
import { MatchingWithLinesGroupClient } from '../../../../questions/types/matching-with-lines-group/MatchingWithLinesGroupClient';
|
|
6
|
+
export function MoversMatchingWithLinesGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode = false, }) {
|
|
7
|
+
const userAnswers = data.questions.map((question) => {
|
|
8
|
+
if (question.isExample)
|
|
9
|
+
return question.correctRightId;
|
|
10
|
+
const raw = answers[question.id];
|
|
11
|
+
return typeof raw === 'string' ? raw : '';
|
|
12
|
+
});
|
|
13
|
+
return (_jsxs("div", { className: "w-full space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx("div", { className: "mt-4", children: _jsx(MatchingWithLinesGroupClient, { questionData: {
|
|
14
|
+
title: data.title,
|
|
15
|
+
instruction: data.instruction,
|
|
16
|
+
rightItems: data.rightItems,
|
|
17
|
+
items: data.questions.map((question) => ({
|
|
18
|
+
imageUrl: question.imageUrl,
|
|
19
|
+
correctAnswer: question.correctRightId,
|
|
20
|
+
isExample: question.isExample,
|
|
21
|
+
points: question.points,
|
|
22
|
+
questionNumber: question.questionNumber,
|
|
23
|
+
})),
|
|
24
|
+
explanation: data.explanation,
|
|
25
|
+
}, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: (index, rightId) => {
|
|
26
|
+
const target = data.questions[index];
|
|
27
|
+
if (target && !target.isExample) {
|
|
28
|
+
onAnswerChange(target.id, rightId);
|
|
29
|
+
}
|
|
30
|
+
} }) })] }));
|
|
31
|
+
}
|
|
@@ -11,6 +11,7 @@ export { MoversFillWordHintRenderer } from './MoversFillWordHintRenderer';
|
|
|
11
11
|
export { MoversGridFillRenderer } from './MoversGridFillRenderer';
|
|
12
12
|
export { MoversTrueFalseRenderer } from './MoversTrueFalseRenderer';
|
|
13
13
|
export { MoversTrueFalseCorrectGroupRenderer } from './MoversTrueFalseCorrectGroupRenderer';
|
|
14
|
+
export { MoversFillInBlankGroupRenderer } from './MoversFillInBlankGroupRenderer';
|
|
14
15
|
export { MoversCrossOutWordGroupRenderer } from './MoversCrossOutWordGroupRenderer';
|
|
15
16
|
export { MoversAnswerTheQuestionRenderer } from './MoversAnswerTheQuestionRenderer';
|
|
16
17
|
export { MoversWriteSentencesRenderer } from './MoversWriteSentencesRenderer';
|
|
@@ -20,6 +21,7 @@ export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswer
|
|
|
20
21
|
export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
|
|
21
22
|
export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
|
|
22
23
|
export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
|
|
24
|
+
export { MoversMatchingWithLinesGroupRenderer } from './MoversMatchingWithLinesGroupRenderer';
|
|
23
25
|
export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
|
|
24
26
|
export { FindWordsInMatrixRenderer } from './FindWordsInMatrixRenderer';
|
|
25
27
|
export { ArticlesViewer } from './ArticlesViewer';
|
|
@@ -15,6 +15,7 @@ export { MoversGridFillRenderer } from './MoversGridFillRenderer';
|
|
|
15
15
|
// Starters & Movers renderers
|
|
16
16
|
export { MoversTrueFalseRenderer } from './MoversTrueFalseRenderer';
|
|
17
17
|
export { MoversTrueFalseCorrectGroupRenderer } from './MoversTrueFalseCorrectGroupRenderer';
|
|
18
|
+
export { MoversFillInBlankGroupRenderer } from './MoversFillInBlankGroupRenderer';
|
|
18
19
|
export { MoversCrossOutWordGroupRenderer } from './MoversCrossOutWordGroupRenderer';
|
|
19
20
|
export { MoversAnswerTheQuestionRenderer } from './MoversAnswerTheQuestionRenderer';
|
|
20
21
|
export { MoversWriteSentencesRenderer } from './MoversWriteSentencesRenderer';
|
|
@@ -24,6 +25,7 @@ export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswer
|
|
|
24
25
|
export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
|
|
25
26
|
export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
|
|
26
27
|
export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
|
|
28
|
+
export { MoversMatchingWithLinesGroupRenderer } from './MoversMatchingWithLinesGroupRenderer';
|
|
27
29
|
export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
|
|
28
30
|
export { FindWordsInMatrixRenderer } from './FindWordsInMatrixRenderer';
|
|
29
31
|
export { ArticlesViewer } from './ArticlesViewer';
|
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
* Types for Movers Exam UI
|
|
3
3
|
*/
|
|
4
4
|
import type { ReactNode } from 'react';
|
|
5
|
+
import type { FillBlankSegment } from '../../../shared/lib/utils/fill-in-blank';
|
|
6
|
+
export type { FillBlankSegment };
|
|
7
|
+
/**
|
|
8
|
+
* A fill-in-the-blank stem, already split into segments. Every blank in
|
|
9
|
+
* `segments` gets its own input, and the student answer is always one string
|
|
10
|
+
* per blank, index-aligned with `acceptedAnswers`.
|
|
11
|
+
*/
|
|
5
12
|
export interface FillBlankQuestion {
|
|
6
13
|
id: string;
|
|
7
14
|
questionNumber: number;
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
segments: FillBlankSegment[];
|
|
16
|
+
/** acceptedAnswers[i] = equivalent correct answers for blank i. */
|
|
17
|
+
acceptedAnswers: string[][];
|
|
10
18
|
verbHint?: string;
|
|
11
|
-
correctAnswer?: string;
|
|
12
19
|
imageUrl?: string;
|
|
13
20
|
audioUrl?: string;
|
|
14
21
|
hints?: string[];
|
|
@@ -25,8 +32,8 @@ export interface MoversListenAndWriteRendererProps {
|
|
|
25
32
|
exampleText?: string;
|
|
26
33
|
exampleHighlight?: string;
|
|
27
34
|
questions: FillBlankQuestion[];
|
|
28
|
-
answers: Record<string,
|
|
29
|
-
onAnswerChange: (questionId: string, value: string) => void;
|
|
35
|
+
answers: Record<string, unknown>;
|
|
36
|
+
onAnswerChange: (questionId: string, value: string[]) => void;
|
|
30
37
|
isReviewMode?: boolean;
|
|
31
38
|
showHints?: boolean;
|
|
32
39
|
}
|
|
@@ -88,6 +88,8 @@ function findParentQuestion(subQuestionId, parts) {
|
|
|
88
88
|
part.questionType === 'CROSS_OUT_WORD_GROUP' ||
|
|
89
89
|
part.questionType === 'TRUE_FALSE_GROUP' ||
|
|
90
90
|
part.questionType === 'TRUE_FALSE_CORRECT_GROUP' ||
|
|
91
|
+
part.questionType === 'FILL_IN_BLANK_GROUP' ||
|
|
92
|
+
part.questionType === 'MATCHING_WITH_LINES_GROUP' ||
|
|
91
93
|
part.questionType === 'WORD_ORDER_AND_MATCH_GROUP' ||
|
|
92
94
|
part.questionType === 'SPONTANEOUS_QA_GROUP' ||
|
|
93
95
|
part.questionType === 'ANSWER_THE_QUESTION_GROUP') {
|
|
@@ -214,7 +216,7 @@ function wrapAnswer(answer, questionType) {
|
|
|
214
216
|
// For array answers (word fill with word bank), wrap in { answers: array }
|
|
215
217
|
// WORD_FILL_PARAGRAPH stores answers as array of strings
|
|
216
218
|
if (Array.isArray(answer)) {
|
|
217
|
-
if (questionType === 'WORD_FILL_PARAGRAPH') {
|
|
219
|
+
if (questionType === 'WORD_FILL_PARAGRAPH' || questionType === 'FILL_IN_BLANK') {
|
|
218
220
|
return { answers: answer };
|
|
219
221
|
}
|
|
220
222
|
// Fallback for other question types with array answers
|
|
@@ -381,6 +383,20 @@ export function transformAnswersForSubmission(answers, parts) {
|
|
|
381
383
|
}
|
|
382
384
|
return { isTrue: null, correction: '' };
|
|
383
385
|
}
|
|
386
|
+
if (parentType === 'FILL_IN_BLANK_GROUP') {
|
|
387
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw) && Array.isArray(raw.answers)) {
|
|
388
|
+
return {
|
|
389
|
+
answers: raw.answers.map((value) => String(value ?? '')),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (Array.isArray(raw)) {
|
|
393
|
+
return { answers: raw.map((value) => String(value ?? '')) };
|
|
394
|
+
}
|
|
395
|
+
if (typeof raw === 'string') {
|
|
396
|
+
return { answers: [raw] };
|
|
397
|
+
}
|
|
398
|
+
return { answers: [] };
|
|
399
|
+
}
|
|
384
400
|
if (parentType === 'WORD_ORDER_AND_MATCH_GROUP') {
|
|
385
401
|
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
386
402
|
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' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
|
|
8
|
+
export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
|
|
9
9
|
export interface ApiQuestion {
|
|
10
10
|
id: string;
|
|
11
11
|
content: any;
|
|
@@ -285,6 +285,31 @@ export declare function transformTrueFalseCorrectGroup(questions: ApiQuestion[])
|
|
|
285
285
|
questions: TrueFalseCorrectGroupQuestion[];
|
|
286
286
|
instruction: string;
|
|
287
287
|
};
|
|
288
|
+
export interface FillInBlankGroupItemAnswer {
|
|
289
|
+
answers: string[][];
|
|
290
|
+
caseSensitive?: boolean;
|
|
291
|
+
}
|
|
292
|
+
export interface FillInBlankGroupStudentItemAnswer {
|
|
293
|
+
answers: string[];
|
|
294
|
+
}
|
|
295
|
+
export interface FillInBlankGroupQuestion {
|
|
296
|
+
id: string;
|
|
297
|
+
questionNumber: number;
|
|
298
|
+
question: string;
|
|
299
|
+
imageUrl?: string;
|
|
300
|
+
audioUrl?: string;
|
|
301
|
+
isExample: boolean;
|
|
302
|
+
blankCount: number;
|
|
303
|
+
exampleAnswers?: string[];
|
|
304
|
+
}
|
|
305
|
+
export declare function transformFillInBlankGroup(questions: ApiQuestion[]): {
|
|
306
|
+
questions: FillInBlankGroupQuestion[];
|
|
307
|
+
instruction: string;
|
|
308
|
+
audioUrl?: string;
|
|
309
|
+
imageUrl?: string;
|
|
310
|
+
showHints?: boolean;
|
|
311
|
+
hints?: string[];
|
|
312
|
+
};
|
|
288
313
|
export interface WordOrderingQuestion {
|
|
289
314
|
id: string;
|
|
290
315
|
questionNumber: number;
|
|
@@ -505,3 +530,22 @@ export interface FindWordsInMatrixPartData {
|
|
|
505
530
|
explanation?: string;
|
|
506
531
|
}
|
|
507
532
|
export declare function transformFindWordsInMatrix(questions: ApiQuestion[]): FindWordsInMatrixPartData;
|
|
533
|
+
export interface MatchingWithLinesGroupQuestionItem {
|
|
534
|
+
id: string;
|
|
535
|
+
questionNumber: number;
|
|
536
|
+
imageUrl: string;
|
|
537
|
+
correctRightId: string;
|
|
538
|
+
isExample: boolean;
|
|
539
|
+
points: number;
|
|
540
|
+
}
|
|
541
|
+
export interface MatchingWithLinesGroupPartData {
|
|
542
|
+
questions: MatchingWithLinesGroupQuestionItem[];
|
|
543
|
+
rightItems: Array<{
|
|
544
|
+
id: string;
|
|
545
|
+
text: string;
|
|
546
|
+
}>;
|
|
547
|
+
title?: string;
|
|
548
|
+
instruction: string;
|
|
549
|
+
explanation?: string;
|
|
550
|
+
}
|
|
551
|
+
export declare function transformMatchingWithLinesGroup(questions: ApiQuestion[]): MatchingWithLinesGroupPartData;
|