@tinyweb_dev/oe-exam-sdk 0.2.5 → 0.2.6
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/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/utils/answer-transformers.js +16 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +45 -1
- package/dist/components/exams/take/utils/question-transformers.js +90 -0
- 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/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-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/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/question-reverse-transform.js +86 -0
- 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
|
+
}
|
|
@@ -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';
|
|
@@ -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') {
|
|
@@ -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;
|
|
@@ -962,6 +962,63 @@ export function transformTrueFalseCorrectGroup(questions) {
|
|
|
962
962
|
|| 'Tick (✓) the true and cross (✗) the false sentences. Correct the false sentences.',
|
|
963
963
|
};
|
|
964
964
|
}
|
|
965
|
+
function parseFillInBlankGroupAnswer(raw) {
|
|
966
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
967
|
+
return undefined;
|
|
968
|
+
}
|
|
969
|
+
const obj = raw;
|
|
970
|
+
if (!Array.isArray(obj.answers)) {
|
|
971
|
+
return undefined;
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
answers: obj.answers.map((blank) => (Array.isArray(blank)
|
|
975
|
+
? blank.map((alt) => String(alt ?? '').trim()).filter(Boolean)
|
|
976
|
+
: [String(blank ?? '').trim()].filter(Boolean))),
|
|
977
|
+
caseSensitive: obj.caseSensitive === true,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function blankCountFromMatches(matches) {
|
|
981
|
+
const indices = matches.map((match) => Number.parseInt(match[1], 10));
|
|
982
|
+
return Math.max(...indices, 0) + 1;
|
|
983
|
+
}
|
|
984
|
+
export function transformFillInBlankGroup(questions) {
|
|
985
|
+
const parent = questions[0];
|
|
986
|
+
const content = parent?.content;
|
|
987
|
+
const items = Array.isArray(content?.items) ? content.items : [];
|
|
988
|
+
const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
|
|
989
|
+
? parent.correct_answer.answer
|
|
990
|
+
: [];
|
|
991
|
+
const groupAudioUrl = content?.meta?.audioUrl?.trim() || '';
|
|
992
|
+
const transformed = items.map((item, index) => {
|
|
993
|
+
const parsed = parseFillInBlankGroupAnswer(item.correctAnswer ?? parentCorrect[index]);
|
|
994
|
+
const question = item.content?.question || '{0}';
|
|
995
|
+
const blankMatches = [...question.matchAll(/\{(\d+)\}/g)];
|
|
996
|
+
const blankCount = blankMatches.length > 0 ? blankCountFromMatches(blankMatches) : 1;
|
|
997
|
+
const isExample = item.isExample === true;
|
|
998
|
+
return {
|
|
999
|
+
id: `${parent.id}-item-${index}`,
|
|
1000
|
+
questionNumber: item.questionNumber || index + 1,
|
|
1001
|
+
question,
|
|
1002
|
+
imageUrl: item.content?.imageUrl,
|
|
1003
|
+
audioUrl: groupAudioUrl ? undefined : item.content?.audioUrl,
|
|
1004
|
+
isExample,
|
|
1005
|
+
blankCount,
|
|
1006
|
+
exampleAnswers: isExample
|
|
1007
|
+
? Array.from({ length: blankCount }, (_, slot) => parsed?.answers?.[slot]?.[0] || '')
|
|
1008
|
+
: undefined,
|
|
1009
|
+
};
|
|
1010
|
+
});
|
|
1011
|
+
return {
|
|
1012
|
+
questions: transformed,
|
|
1013
|
+
instruction: content?.meta?.instruction || 'Complete the sentences.',
|
|
1014
|
+
audioUrl: groupAudioUrl || undefined,
|
|
1015
|
+
imageUrl: content?.meta?.imageUrl,
|
|
1016
|
+
showHints: content?.meta?.showHints === true,
|
|
1017
|
+
hints: Array.isArray(content?.meta?.hints)
|
|
1018
|
+
? content.meta.hints.map((hint) => String(hint ?? '').trim()).filter(Boolean)
|
|
1019
|
+
: [],
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
965
1022
|
export function transformWordOrdering(questions) {
|
|
966
1023
|
const transformed = questions.map((q) => {
|
|
967
1024
|
const content = q.content;
|
|
@@ -1326,3 +1383,36 @@ export function transformFindWordsInMatrix(questions) {
|
|
|
1326
1383
|
explanation: question?.explanation,
|
|
1327
1384
|
};
|
|
1328
1385
|
}
|
|
1386
|
+
export function transformMatchingWithLinesGroup(questions) {
|
|
1387
|
+
const parent = questions[0];
|
|
1388
|
+
const content = (parent?.content || {});
|
|
1389
|
+
const items = Array.isArray(content.items) ? content.items : [];
|
|
1390
|
+
const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
|
|
1391
|
+
? parent.correct_answer.answer
|
|
1392
|
+
: [];
|
|
1393
|
+
const rightItems = Array.isArray(content.rightItems)
|
|
1394
|
+
? content.rightItems.map((item, index) => ({
|
|
1395
|
+
id: typeof item?.id === 'string' ? item.id : `r${index + 1}`,
|
|
1396
|
+
text: typeof item?.text === 'string' ? item.text : '',
|
|
1397
|
+
}))
|
|
1398
|
+
: [];
|
|
1399
|
+
return {
|
|
1400
|
+
questions: items.map((item, index) => {
|
|
1401
|
+
const isExample = item.isExample === true;
|
|
1402
|
+
const fromItem = typeof item.correctAnswer?.answer === 'string' ? item.correctAnswer.answer : '';
|
|
1403
|
+
const fromParent = typeof parentCorrect[index] === 'string' ? parentCorrect[index] : '';
|
|
1404
|
+
return {
|
|
1405
|
+
id: `${parent?.id || 'q'}-item-${index}`,
|
|
1406
|
+
questionNumber: item.questionNumber || index + 1,
|
|
1407
|
+
imageUrl: item.content?.imageUrl || '',
|
|
1408
|
+
correctRightId: fromItem || fromParent,
|
|
1409
|
+
isExample,
|
|
1410
|
+
points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
1411
|
+
};
|
|
1412
|
+
}),
|
|
1413
|
+
rightItems,
|
|
1414
|
+
title: content.meta?.title || '',
|
|
1415
|
+
instruction: content.meta?.instruction || 'Look and match.',
|
|
1416
|
+
explanation: parent?.explanation,
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
@@ -177,6 +177,11 @@ export const QUESTION_TYPE_CONFIG = {
|
|
|
177
177
|
requiresContent: true,
|
|
178
178
|
hasDedicatedComponent: true,
|
|
179
179
|
},
|
|
180
|
+
FILL_IN_BLANK_GROUP: {
|
|
181
|
+
label: 'Điền chỗ trống (nhóm)',
|
|
182
|
+
requiresContent: true,
|
|
183
|
+
hasDedicatedComponent: true,
|
|
184
|
+
},
|
|
180
185
|
CROSS_OUT_WORD_GROUP: {
|
|
181
186
|
label: 'Cross out word (group)',
|
|
182
187
|
requiresContent: true,
|
|
@@ -237,6 +242,11 @@ export const QUESTION_TYPE_CONFIG = {
|
|
|
237
242
|
requiresContent: true,
|
|
238
243
|
hasDedicatedComponent: true,
|
|
239
244
|
},
|
|
245
|
+
MATCHING_WITH_LINES_GROUP: {
|
|
246
|
+
label: 'Nối bằng đường kẻ (nhóm)',
|
|
247
|
+
requiresContent: true,
|
|
248
|
+
hasDedicatedComponent: true,
|
|
249
|
+
},
|
|
240
250
|
WRITE_SENTENCES: {
|
|
241
251
|
label: 'Viết câu',
|
|
242
252
|
requiresContent: true,
|
|
@@ -354,6 +364,7 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
|
|
|
354
364
|
'CHOOSE_THE_CORRECT_ANSWER_GROUP',
|
|
355
365
|
'TRUE_FALSE_GROUP',
|
|
356
366
|
'TRUE_FALSE_CORRECT_GROUP',
|
|
367
|
+
'FILL_IN_BLANK_GROUP',
|
|
357
368
|
'CROSS_OUT_WORD_GROUP',
|
|
358
369
|
'WRITE_A_SHORT_LETTER',
|
|
359
370
|
'WRITE_CORRECT_VERB_FORM',
|
|
@@ -365,6 +376,7 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
|
|
|
365
376
|
'WORD_FILL_PARAGRAPH',
|
|
366
377
|
'MATCH_BY_WRITING_ANSWER',
|
|
367
378
|
'MATCH_WORD_TO_PICTURE',
|
|
379
|
+
'MATCHING_WITH_LINES_GROUP',
|
|
368
380
|
'WRITE_SENTENCES',
|
|
369
381
|
'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP',
|
|
370
382
|
'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE',
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const FILL_IN_BLANK_ITEM_TYPE: "FILL_IN_BLANK";
|
|
3
|
+
export declare const FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION = "Complete the sentences.";
|
|
4
|
+
export interface FillInBlankGroupItemAnswer {
|
|
5
|
+
answers: string[][];
|
|
6
|
+
caseSensitive?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface FillInBlankGroupStudentItemAnswer {
|
|
9
|
+
answers: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface FillInBlankGroupItemData {
|
|
12
|
+
question: string;
|
|
13
|
+
answers: string[][];
|
|
14
|
+
caseSensitive?: boolean;
|
|
15
|
+
imageUrl?: string;
|
|
16
|
+
audioUrl?: string;
|
|
17
|
+
showHints?: boolean;
|
|
18
|
+
hints?: string[];
|
|
19
|
+
isExample?: boolean;
|
|
20
|
+
points: number;
|
|
21
|
+
questionNumber: number;
|
|
22
|
+
}
|
|
23
|
+
export interface FillInBlankGroupData {
|
|
24
|
+
instruction: string;
|
|
25
|
+
audioUrl?: string;
|
|
26
|
+
imageUrl?: string;
|
|
27
|
+
showHints?: boolean;
|
|
28
|
+
hints?: string[];
|
|
29
|
+
items: FillInBlankGroupItemData[];
|
|
30
|
+
explanation?: string;
|
|
31
|
+
points?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface FillInBlankGroupCreatorProps {
|
|
34
|
+
initialData?: FillInBlankGroupData;
|
|
35
|
+
onSave?: (data: FillInBlankGroupData) => void;
|
|
36
|
+
onCancel?: () => void;
|
|
37
|
+
onChange?: (data: FillInBlankGroupData) => void;
|
|
38
|
+
externalErrors?: string[];
|
|
39
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
40
|
+
validationRef?: MutableRefObject<{
|
|
41
|
+
triggerValidation: () => Promise<boolean>;
|
|
42
|
+
getFormData?: () => FillInBlankGroupData;
|
|
43
|
+
} | null>;
|
|
44
|
+
}
|
|
45
|
+
export interface FillInBlankGroupClientProps {
|
|
46
|
+
questionData: FillInBlankGroupData;
|
|
47
|
+
isReviewMode?: boolean;
|
|
48
|
+
userAnswers?: FillInBlankGroupStudentItemAnswer[];
|
|
49
|
+
onAnswerChange?: (answers: FillInBlankGroupStudentItemAnswer[]) => void;
|
|
50
|
+
}
|
|
@@ -12,5 +12,7 @@ export * from './fill-in-blank.type';
|
|
|
12
12
|
export * from './read-passage-and-answer-questions.type';
|
|
13
13
|
export * from './word-fill-structured-form.type';
|
|
14
14
|
export * from './true-false-correct-group.type';
|
|
15
|
+
export * from './fill-in-blank-group.type';
|
|
15
16
|
export * from './match-word-to-picture.type';
|
|
17
|
+
export * from './matching-with-lines-group.type';
|
|
16
18
|
export * from './spontaneous-qa-group.type';
|
|
@@ -12,5 +12,7 @@ export * from './fill-in-blank.type';
|
|
|
12
12
|
export * from './read-passage-and-answer-questions.type';
|
|
13
13
|
export * from './word-fill-structured-form.type';
|
|
14
14
|
export * from './true-false-correct-group.type';
|
|
15
|
+
export * from './fill-in-blank-group.type';
|
|
15
16
|
export * from './match-word-to-picture.type';
|
|
17
|
+
export * from './matching-with-lines-group.type';
|
|
16
18
|
export * from './spontaneous-qa-group.type';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION = "Look and match.";
|
|
3
|
+
export interface MatchingWithLinesRightItem {
|
|
4
|
+
id: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export interface MatchingWithLinesGroupItemData {
|
|
8
|
+
imageUrl: string;
|
|
9
|
+
correctAnswer: string;
|
|
10
|
+
isExample?: boolean;
|
|
11
|
+
points: number;
|
|
12
|
+
questionNumber: number;
|
|
13
|
+
}
|
|
14
|
+
export interface MatchingWithLinesGroupData {
|
|
15
|
+
title?: string;
|
|
16
|
+
instruction: string;
|
|
17
|
+
rightItems: MatchingWithLinesRightItem[];
|
|
18
|
+
items: MatchingWithLinesGroupItemData[];
|
|
19
|
+
explanation?: string;
|
|
20
|
+
points?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface MatchingWithLinesGroupCreatorProps {
|
|
23
|
+
initialData?: MatchingWithLinesGroupData;
|
|
24
|
+
onSave?: (data: MatchingWithLinesGroupData) => void;
|
|
25
|
+
onCancel?: () => void;
|
|
26
|
+
onChange?: (data: MatchingWithLinesGroupData) => void;
|
|
27
|
+
externalErrors?: string[];
|
|
28
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
29
|
+
validationRef?: MutableRefObject<{
|
|
30
|
+
triggerValidation: () => Promise<boolean>;
|
|
31
|
+
getFormData?: () => MatchingWithLinesGroupData;
|
|
32
|
+
} | null>;
|
|
33
|
+
}
|
|
34
|
+
export interface MatchingWithLinesGroupClientProps {
|
|
35
|
+
questionData: MatchingWithLinesGroupData;
|
|
36
|
+
isReviewMode?: boolean;
|
|
37
|
+
userAnswers?: string[];
|
|
38
|
+
onAnswerChange?: (index: number, rightId: string) => void;
|
|
39
|
+
onAnswersChange?: (next: string[]) => void;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION = 'Look and match.';
|
|
@@ -23,11 +23,13 @@ const IMAGE_BASED_TYPES = [
|
|
|
23
23
|
'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER',
|
|
24
24
|
'LOOK_PICTURE_FILL_WORD_HINT',
|
|
25
25
|
'FILL_IN_BLANK',
|
|
26
|
+
'FILL_IN_BLANK_GROUP',
|
|
26
27
|
'LABEL_THE_PICTURE',
|
|
27
28
|
'READ_AND_COLOR_OBJECTS',
|
|
28
29
|
'IMAGE_OBJECT_MATCHING',
|
|
29
30
|
'FILL_IN_BLANK',
|
|
30
31
|
'MATCH_WORD_TO_PICTURE',
|
|
32
|
+
'MATCHING_WITH_LINES_GROUP',
|
|
31
33
|
];
|
|
32
34
|
// Passage-based question types
|
|
33
35
|
const PASSAGE_BASED_TYPES = [
|
|
@@ -62,8 +64,10 @@ const getQuestionTypeName = (type) => {
|
|
|
62
64
|
'IMAGE_OBJECT_MATCHING': 'Ghép hình ảnh - Đối tượng',
|
|
63
65
|
'WRITE_CORRECT_VERB_FORM': 'Viết dạng động từ',
|
|
64
66
|
'FILL_IN_BLANK': 'Điền vào chỗ trống',
|
|
67
|
+
'FILL_IN_BLANK_GROUP': 'Điền chỗ trống (nhóm)',
|
|
65
68
|
'READ_PASSAGE_AND_ANSWER_QUESTIONS': 'Đọc hiểu',
|
|
66
69
|
'MATCH_WORD_TO_PICTURE': 'Nối từ với tranh',
|
|
70
|
+
'MATCHING_WITH_LINES_GROUP': 'Nối bằng đường kẻ (nhóm)',
|
|
67
71
|
};
|
|
68
72
|
return type ? typeMap[type] || type.replace(/_/g, ' ').toLowerCase() : 'Chưa xác định';
|
|
69
73
|
};
|
|
@@ -27,7 +27,9 @@ export function QuestionTypeSelector({ value, onValueChange, disabled, }) {
|
|
|
27
27
|
'WORD_ORDERING',
|
|
28
28
|
'MATCH_BY_WRITING_ANSWER',
|
|
29
29
|
'MATCH_WORD_TO_PICTURE',
|
|
30
|
+
'MATCHING_WITH_LINES_GROUP',
|
|
30
31
|
'WRITE_SENTENCES',
|
|
32
|
+
'FILL_IN_BLANK_GROUP',
|
|
31
33
|
];
|
|
32
34
|
return (_jsxs(Select, { value: value, onValueChange: onValueChange, disabled: disabled, children: [_jsx(SelectTrigger, { id: "questionType", className: "mt-1", children: _jsx(SelectValue, { placeholder: "Ch\u1ECDn lo\u1EA1i c\u00E2u h\u1ECFi" }) }), _jsx(SelectContent, { children: questionTypes.map((type) => (_jsx(SelectItem, { value: type, children: getQuestionTypeLabel(type) }, type))) })] }));
|
|
33
35
|
}
|