@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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/matching-with-lines-group.type';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return isRecord(value) ? value : {};
|
|
7
|
+
}
|
|
8
|
+
function asString(value, fallback = '') {
|
|
9
|
+
return typeof value === 'string' ? value : fallback;
|
|
10
|
+
}
|
|
11
|
+
export function nextRightId(existing) {
|
|
12
|
+
let n = existing.length + 1;
|
|
13
|
+
const used = new Set(existing);
|
|
14
|
+
while (used.has(`r${n}`))
|
|
15
|
+
n += 1;
|
|
16
|
+
return `r${n}`;
|
|
17
|
+
}
|
|
18
|
+
export function createEmptyMatchingPair(questionNumber, rightId) {
|
|
19
|
+
return {
|
|
20
|
+
rightItem: { id: rightId, text: '' },
|
|
21
|
+
item: {
|
|
22
|
+
imageUrl: '',
|
|
23
|
+
correctAnswer: rightId,
|
|
24
|
+
isExample: false,
|
|
25
|
+
points: 1,
|
|
26
|
+
questionNumber,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function mapMatchingWithLinesGroupItem(item, index, fallbackAnswer) {
|
|
31
|
+
const record = asRecord(item);
|
|
32
|
+
const content = asRecord(record.content);
|
|
33
|
+
const correctAnswer = asRecord(record.correctAnswer);
|
|
34
|
+
const isExample = record.isExample === true;
|
|
35
|
+
const answer = asString(correctAnswer.answer) ||
|
|
36
|
+
(typeof fallbackAnswer === 'string' ? fallbackAnswer : '');
|
|
37
|
+
return {
|
|
38
|
+
imageUrl: asString(content.imageUrl) || asString(record.imageUrl),
|
|
39
|
+
correctAnswer: answer,
|
|
40
|
+
isExample,
|
|
41
|
+
points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
|
|
42
|
+
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function mapRightItems(raw) {
|
|
46
|
+
if (!Array.isArray(raw))
|
|
47
|
+
return [];
|
|
48
|
+
return raw.map((item, index) => {
|
|
49
|
+
const record = asRecord(item);
|
|
50
|
+
return {
|
|
51
|
+
id: asString(record.id, `r${index + 1}`),
|
|
52
|
+
text: asString(record.text),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function mapQuestionToMatchingWithLinesGroupData(question) {
|
|
57
|
+
const source = asRecord(question);
|
|
58
|
+
const content = asRecord(source.content);
|
|
59
|
+
const answer = asRecord(source.answer);
|
|
60
|
+
const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
|
|
61
|
+
const fallbackAnswers = Array.isArray(correctAnswer.answer)
|
|
62
|
+
? correctAnswer.answer
|
|
63
|
+
: Array.isArray(answer.answer)
|
|
64
|
+
? answer.answer
|
|
65
|
+
: [];
|
|
66
|
+
const explanation = asString(source.explanation) || asString(answer.explanation);
|
|
67
|
+
const meta = asRecord(content.meta ?? answer.meta);
|
|
68
|
+
if (Array.isArray(content.items)) {
|
|
69
|
+
const items = content.items.map((item, index) => mapMatchingWithLinesGroupItem(item, index, fallbackAnswers[index]));
|
|
70
|
+
return {
|
|
71
|
+
title: asString(meta.title) || asString(answer.title),
|
|
72
|
+
instruction: asString(meta.instruction) ||
|
|
73
|
+
asString(answer.instruction) ||
|
|
74
|
+
MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
|
|
75
|
+
rightItems: mapRightItems(content.rightItems),
|
|
76
|
+
items,
|
|
77
|
+
explanation,
|
|
78
|
+
points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(answer.items)) {
|
|
82
|
+
const items = answer.items.map((item, index) => mapMatchingWithLinesGroupItem(item, index, fallbackAnswers[index]));
|
|
83
|
+
return {
|
|
84
|
+
title: asString(answer.title),
|
|
85
|
+
instruction: asString(answer.instruction) || MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
|
|
86
|
+
rightItems: mapRightItems(answer.rightItems),
|
|
87
|
+
items,
|
|
88
|
+
explanation,
|
|
89
|
+
points: typeof answer.points === 'number'
|
|
90
|
+
? answer.points
|
|
91
|
+
: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const first = createEmptyMatchingPair(1, 'r1');
|
|
95
|
+
const second = createEmptyMatchingPair(2, 'r2');
|
|
96
|
+
return {
|
|
97
|
+
title: '',
|
|
98
|
+
instruction: MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
|
|
99
|
+
rightItems: [first.rightItem, second.rightItem],
|
|
100
|
+
items: [first.item, second.item],
|
|
101
|
+
explanation,
|
|
102
|
+
points: 2,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { QuestionTypeRegistration } from '../../creator/question-type-registry';
|
|
2
|
+
export { createEmptyMatchingPair, mapMatchingWithLinesGroupItem, mapQuestionToMatchingWithLinesGroupData, nextRightId, } from './map-matching-with-lines-group-data';
|
|
3
|
+
export declare const matchingWithLinesGroupRegistration: QuestionTypeRegistration;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MatchingWithLinesGroupCreator } from './MatchingWithLinesGroupCreator';
|
|
2
|
+
import { mapQuestionToMatchingWithLinesGroupData } from './map-matching-with-lines-group-data';
|
|
3
|
+
export { createEmptyMatchingPair, mapMatchingWithLinesGroupItem, mapQuestionToMatchingWithLinesGroupData, nextRightId, } from './map-matching-with-lines-group-data';
|
|
4
|
+
export const matchingWithLinesGroupRegistration = {
|
|
5
|
+
component: MatchingWithLinesGroupCreator,
|
|
6
|
+
transformInitialData: (initialData) => mapQuestionToMatchingWithLinesGroupData(initialData),
|
|
7
|
+
getExtraProps: (ctx) => ({
|
|
8
|
+
onUnsavedChangesChange: ctx.onUnsavedChangesChange,
|
|
9
|
+
validationRef: ctx.validationRef,
|
|
10
|
+
}),
|
|
11
|
+
wrapOnSave: (data, ctx) => {
|
|
12
|
+
const payload = data;
|
|
13
|
+
return {
|
|
14
|
+
type: ctx.questionType,
|
|
15
|
+
content: ctx.state.content,
|
|
16
|
+
points: payload.points ?? ctx.state.points ?? 1,
|
|
17
|
+
level: ctx.state.level,
|
|
18
|
+
difficulty: ctx.state.difficulty,
|
|
19
|
+
skill: ctx.state.skill,
|
|
20
|
+
answer: payload,
|
|
21
|
+
explanation: payload.explanation || '',
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
wrapOnChange: (data, ctx) => {
|
|
25
|
+
const payload = data;
|
|
26
|
+
return {
|
|
27
|
+
type: ctx.questionType,
|
|
28
|
+
content: ctx.state.content,
|
|
29
|
+
points: payload.points ?? ctx.state.points ?? 1,
|
|
30
|
+
level: ctx.state.level,
|
|
31
|
+
difficulty: ctx.state.difficulty,
|
|
32
|
+
skill: ctx.state.skill,
|
|
33
|
+
answer: payload,
|
|
34
|
+
explanation: payload.explanation || '',
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/matching-with-lines-group.type';
|
|
2
|
+
function sumGradablePoints(items) {
|
|
3
|
+
return items.reduce((total, item) => {
|
|
4
|
+
if (item.isExample)
|
|
5
|
+
return total;
|
|
6
|
+
const points = typeof item.points === 'number' ? item.points : 1;
|
|
7
|
+
return total + points;
|
|
8
|
+
}, 0);
|
|
9
|
+
}
|
|
10
|
+
export const transformMatchingWithLinesGroup = (question) => {
|
|
11
|
+
const answerData = question.answer;
|
|
12
|
+
const items = Array.isArray(answerData?.items) ? answerData.items : [];
|
|
13
|
+
const rightItems = Array.isArray(answerData?.rightItems) ? answerData.rightItems : [];
|
|
14
|
+
if (items.length === 0) {
|
|
15
|
+
return { apiContent: { rightItems: [], items: [] }, apiCorrectAnswer: { answer: [] } };
|
|
16
|
+
}
|
|
17
|
+
const apiItems = items.map((item, index) => ({
|
|
18
|
+
...(item.isExample ? { isExample: true } : {}),
|
|
19
|
+
content: {
|
|
20
|
+
imageUrl: item.imageUrl || '',
|
|
21
|
+
},
|
|
22
|
+
correctAnswer: { answer: item.correctAnswer || '' },
|
|
23
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
24
|
+
questionNumber: item.questionNumber || index + 1,
|
|
25
|
+
}));
|
|
26
|
+
const title = typeof answerData?.title === 'string' ? answerData.title.trim() : '';
|
|
27
|
+
return {
|
|
28
|
+
apiContent: {
|
|
29
|
+
meta: {
|
|
30
|
+
instruction: answerData?.instruction || MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
|
|
31
|
+
...(title ? { title } : {}),
|
|
32
|
+
},
|
|
33
|
+
rightItems: rightItems.map((item) => ({
|
|
34
|
+
id: item.id || '',
|
|
35
|
+
text: item.text || '',
|
|
36
|
+
})),
|
|
37
|
+
items: apiItems,
|
|
38
|
+
},
|
|
39
|
+
apiCorrectAnswer: {
|
|
40
|
+
answer: apiItems.map((item) => item.correctAnswer.answer),
|
|
41
|
+
},
|
|
42
|
+
totalPoints: sumGradablePoints(items),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Question } from '../../../shared/types/entities/question.types';
|
|
2
|
+
import type { FillInBlankGroupStudentItemAnswer } from '../_shared/types/fill-in-blank-group.type';
|
|
3
|
+
type FillInBlankGroupViewerProps = {
|
|
4
|
+
question: Question;
|
|
5
|
+
isReviewMode?: boolean;
|
|
6
|
+
userAnswer?: FillInBlankGroupStudentItemAnswer[];
|
|
7
|
+
onSubmitAnswer?: (answer: FillInBlankGroupStudentItemAnswer[]) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare function FillInBlankGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: FillInBlankGroupViewerProps): import("react").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { FillInBlankGroupClient } from '../types/fill-in-blank-group/FillInBlankGroupClient';
|
|
4
|
+
import { mapQuestionToFillInBlankGroupData } from '../types/fill-in-blank-group/map-fill-in-blank-group-data';
|
|
5
|
+
export function FillInBlankGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
6
|
+
const questionData = mapQuestionToFillInBlankGroupData(question);
|
|
7
|
+
const studentAnswers = Array.isArray(userAnswer) ? userAnswer : undefined;
|
|
8
|
+
const reviewAnswers = isReviewMode && !studentAnswers
|
|
9
|
+
? questionData.items.map((item) => ({
|
|
10
|
+
answers: item.answers.map((alts) => alts[0] || ''),
|
|
11
|
+
}))
|
|
12
|
+
: studentAnswers;
|
|
13
|
+
return (_jsx(FillInBlankGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Question } from '../../../shared/types/entities/question.types';
|
|
2
|
+
export declare function MatchingWithLinesGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
|
|
3
|
+
question: Question;
|
|
4
|
+
isReviewMode?: boolean;
|
|
5
|
+
userAnswer?: unknown;
|
|
6
|
+
onSubmitAnswer?: (answer: string[]) => void;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { MatchingWithLinesGroupClient } from '../types/matching-with-lines-group/MatchingWithLinesGroupClient';
|
|
5
|
+
import { mapQuestionToMatchingWithLinesGroupData } from '../types/matching-with-lines-group/map-matching-with-lines-group-data';
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function emptyAnswers(length) {
|
|
10
|
+
return Array.from({ length }, () => '');
|
|
11
|
+
}
|
|
12
|
+
function parseMatchingAnswers(value, length) {
|
|
13
|
+
const source = Array.isArray(value)
|
|
14
|
+
? value
|
|
15
|
+
: isRecord(value) && Array.isArray(value.answer)
|
|
16
|
+
? value.answer
|
|
17
|
+
: null;
|
|
18
|
+
if (!source)
|
|
19
|
+
return undefined;
|
|
20
|
+
return Array.from({ length }, (_, index) => typeof source[index] === 'string' ? source[index] : '');
|
|
21
|
+
}
|
|
22
|
+
export function MatchingWithLinesGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
23
|
+
const questionData = mapQuestionToMatchingWithLinesGroupData(question);
|
|
24
|
+
const itemCount = questionData.items.length;
|
|
25
|
+
const parsedUser = parseMatchingAnswers(userAnswer, itemCount);
|
|
26
|
+
const questionKey = `${question.id}:${question.type}:${itemCount}:${questionData.rightItems
|
|
27
|
+
.map((item) => `${item.id}:${item.text}`)
|
|
28
|
+
.join(',')}`;
|
|
29
|
+
const [cachedKey, setCachedKey] = useState(questionKey);
|
|
30
|
+
const [localAnswers, setLocalAnswers] = useState(() => parsedUser ?? emptyAnswers(itemCount));
|
|
31
|
+
if (cachedKey !== questionKey) {
|
|
32
|
+
setCachedKey(questionKey);
|
|
33
|
+
setLocalAnswers(parsedUser ?? emptyAnswers(itemCount));
|
|
34
|
+
}
|
|
35
|
+
const userAnswers = isReviewMode && !parsedUser
|
|
36
|
+
? questionData.items.map((item) => item.correctAnswer)
|
|
37
|
+
: parsedUser ?? localAnswers;
|
|
38
|
+
const handleAnswersChange = (next) => {
|
|
39
|
+
const padded = questionData.items.map((item, index) => {
|
|
40
|
+
if (item.isExample)
|
|
41
|
+
return item.correctAnswer || '';
|
|
42
|
+
return typeof next[index] === 'string' ? next[index] : '';
|
|
43
|
+
});
|
|
44
|
+
setLocalAnswers(padded);
|
|
45
|
+
onSubmitAnswer?.(padded);
|
|
46
|
+
};
|
|
47
|
+
return (_jsx(MatchingWithLinesGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswersChange: isReviewMode ? undefined : handleAnswersChange }));
|
|
48
|
+
}
|
|
@@ -26,6 +26,8 @@ import { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
|
|
|
26
26
|
import { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
|
|
27
27
|
import { TrueFalseGroupViewer } from './TrueFalseGroupViewer';
|
|
28
28
|
import { TrueFalseCorrectGroupViewer } from './TrueFalseCorrectGroupViewer';
|
|
29
|
+
import { FillInBlankGroupViewer } from './FillInBlankGroupViewer';
|
|
30
|
+
import { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
|
|
29
31
|
import { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
|
|
30
32
|
import { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
|
|
31
33
|
import { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
@@ -635,6 +637,10 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
635
637
|
return (_jsx(TrueFalseGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
636
638
|
case 'TRUE_FALSE_CORRECT_GROUP':
|
|
637
639
|
return (_jsx(TrueFalseCorrectGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
640
|
+
case 'FILL_IN_BLANK_GROUP':
|
|
641
|
+
return (_jsx(FillInBlankGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
642
|
+
case 'MATCHING_WITH_LINES_GROUP':
|
|
643
|
+
return (_jsx(MatchingWithLinesGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
638
644
|
case 'CROSS_OUT_WORD_GROUP':
|
|
639
645
|
return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode }));
|
|
640
646
|
case 'ANSWER_THE_QUESTION_GROUP':
|
|
@@ -4,6 +4,8 @@ export { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
|
|
|
4
4
|
export { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
|
|
5
5
|
export { TrueFalseGroupViewer } from './TrueFalseGroupViewer';
|
|
6
6
|
export { TrueFalseCorrectGroupViewer } from './TrueFalseCorrectGroupViewer';
|
|
7
|
+
export { FillInBlankGroupViewer } from './FillInBlankGroupViewer';
|
|
8
|
+
export { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
|
|
7
9
|
export { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
|
|
8
10
|
export { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
|
|
9
11
|
export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
@@ -4,6 +4,8 @@ export { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
|
|
|
4
4
|
export { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
|
|
5
5
|
export { TrueFalseGroupViewer } from './TrueFalseGroupViewer';
|
|
6
6
|
export { TrueFalseCorrectGroupViewer } from './TrueFalseCorrectGroupViewer';
|
|
7
|
+
export { FillInBlankGroupViewer } from './FillInBlankGroupViewer';
|
|
8
|
+
export { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
|
|
7
9
|
export { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
|
|
8
10
|
export { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
|
|
9
11
|
export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
@@ -10,9 +10,8 @@ interface ReviewListenAndWriteRendererProps {
|
|
|
10
10
|
exampleText?: string;
|
|
11
11
|
exampleHighlight?: string;
|
|
12
12
|
questions: FillBlankQuestion[];
|
|
13
|
-
answers: Record<string,
|
|
13
|
+
answers: Record<string, unknown>;
|
|
14
14
|
isCorrectMap: Record<string, boolean | null>;
|
|
15
|
-
correctAnswerTextMap: Record<string, string>;
|
|
16
15
|
}
|
|
17
|
-
export declare function ReviewListenAndWriteRenderer({ partNumber, questionCount, instruction, partName, audioUrl, title, exampleText, questions, answers, isCorrectMap,
|
|
16
|
+
export declare function ReviewListenAndWriteRenderer({ partNumber, questionCount, instruction, partName, audioUrl, title, exampleText, questions, answers, isCorrectMap, }: ReviewListenAndWriteRendererProps): import("react").JSX.Element;
|
|
18
17
|
export {};
|
|
@@ -5,7 +5,8 @@ import { ResolvedImage } from '../../../components/common/ResolvedImage';
|
|
|
5
5
|
import CambridgeYlePartBanner from '../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
6
6
|
import CambridgeYleInstructionBanner from '../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
7
7
|
import CambridgeYleAudioPlayer from '../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
|
|
8
|
-
|
|
8
|
+
import { countBlanks, formatAcceptedAnswers, hasAnyBlankValue, isBlankCorrect, toBlankValues, } from '../../../shared/lib/utils/fill-in-blank';
|
|
9
|
+
export function ReviewListenAndWriteRenderer({ partNumber, questionCount, instruction, partName, audioUrl, title, exampleText, questions, answers, isCorrectMap, }) {
|
|
9
10
|
/**
|
|
10
11
|
* Get answer status from API's isCorrect field
|
|
11
12
|
* Returns: 'correct' | 'incorrect' | 'unanswered'
|
|
@@ -14,7 +15,7 @@ export function ReviewListenAndWriteRenderer({ partNumber, questionCount, instru
|
|
|
14
15
|
const isCorrect = isCorrectMap[questionId];
|
|
15
16
|
const userAnswer = answers[questionId];
|
|
16
17
|
// No answer from student (null or undefined or empty string)
|
|
17
|
-
if (userAnswer
|
|
18
|
+
if (!hasAnyBlankValue(userAnswer)) {
|
|
18
19
|
return 'unanswered';
|
|
19
20
|
}
|
|
20
21
|
// Use API's isCorrect value
|
|
@@ -25,8 +26,7 @@ export function ReviewListenAndWriteRenderer({ partNumber, questionCount, instru
|
|
|
25
26
|
// isCorrect is null but student answered - treat as unanswered/pending
|
|
26
27
|
return 'unanswered';
|
|
27
28
|
};
|
|
28
|
-
const getUnderlineColor = (
|
|
29
|
-
const status = getAnswerStatus(questionId);
|
|
29
|
+
const getUnderlineColor = (status) => {
|
|
30
30
|
switch (status) {
|
|
31
31
|
case 'correct':
|
|
32
32
|
return '#22c55e'; // green-500
|
|
@@ -38,11 +38,20 @@ export function ReviewListenAndWriteRenderer({ partNumber, questionCount, instru
|
|
|
38
38
|
return '#9ca3af'; // gray-400
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
|
+
const renderBlankAnswer = (status, userValue, correctValue) => (_jsxs("span", { className: "mx-1 inline-block min-w-[120px] align-baseline", style: { borderBottom: `2px solid ${getUnderlineColor(status)}` }, children: [status === 'correct' && (_jsx("span", { className: "block w-full text-center", children: userValue })), status === 'unanswered' && (_jsx("span", { className: "block w-full text-center text-blue-600", children: correctValue })), status === 'incorrect' && (_jsxs("span", { className: "flex items-center justify-center gap-2", children: [_jsx("span", { className: "text-red-500 line-through", children: userValue }), _jsx("span", { className: "text-gray-400", children: "\u00E2\u2020\u2019" }), _jsx("span", { className: "text-blue-600", children: correctValue })] }))] }));
|
|
42
|
+
const getPerBlankStatus = (overall, userValue, accepted) => {
|
|
43
|
+
if (overall === 'unanswered' || !userValue.trim())
|
|
44
|
+
return 'unanswered';
|
|
45
|
+
return isBlankCorrect(userValue, accepted) ? 'correct' : 'incorrect';
|
|
46
|
+
};
|
|
41
47
|
return (_jsxs("div", { className: "flex flex-col gap-6 h-full", children: [_jsxs("div", { className: "flex flex-col gap-4 pb-0 pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: true }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, hasExample: !!exampleText, isReviewMode: true }), audioUrl && _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true })] }), _jsxs("div", { className: "flex flex-col gap-6 pb-6 pl-2 pt-2", children: [_jsx("h2", { className: "text-center text-3xl font-bold uppercase text-coral-500", children: title }), _jsx("div", { className: "flex flex-col gap-4", children: questions.map((question) => {
|
|
42
48
|
const status = getAnswerStatus(question.id);
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
const userValues = toBlankValues(answers[question.id], countBlanks(question.segments));
|
|
50
|
+
const renderBlank = (blankIndex) => {
|
|
51
|
+
const userValue = userValues[blankIndex] || '';
|
|
52
|
+
const accepted = question.acceptedAnswers[blankIndex];
|
|
53
|
+
return renderBlankAnswer(getPerBlankStatus(status, userValue, accepted), userValue, formatAcceptedAnswers(accepted));
|
|
54
|
+
};
|
|
55
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [question.audioUrl && (_jsx("div", { className: "w-full max-w-md", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: question.audioUrl, compact: true }) })), question.imageUrl && (_jsx("div", { className: "flex items-center justify-center", children: _jsx(ResolvedImage, { src: question.imageUrl, alt: `Question ${question.questionNumber} illustration`, className: "max-h-[150px] rounded-lg object-contain shadow-md" }) })), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-sm font-bold text-white", children: question.questionNumber }), _jsx("div", { className: "flex-1 text-base leading-relaxed text-gray-800", children: _jsxs("div", { className: "leading-loose", children: [question.segments.map((segment, segmentIndex) => segment.type === 'text' ? (_jsx("span", { children: segment.value }, `text-${segmentIndex}`)) : (_jsx("span", { children: renderBlank(segment.index) }, `blank-${segment.index}-${segmentIndex}`))), status === 'correct' && (_jsx("div", { className: "ml-2 inline-flex h-6 w-6 items-center justify-center rounded-full bg-green-500 align-middle", children: _jsx(Check, { className: "h-4 w-4 text-white", strokeWidth: 3 }) })), status === 'incorrect' && (_jsx("div", { className: "ml-2 inline-flex h-6 w-6 items-center justify-center rounded-full bg-red-500 align-middle", children: _jsx(X, { className: "h-4 w-4 text-white", strokeWidth: 3 }) })), status === 'unanswered' && (_jsx("div", { className: "ml-2 inline-flex h-6 w-6 items-center justify-center rounded-full bg-blue-500 align-middle", children: _jsx(Check, { className: "h-4 w-4 text-white", strokeWidth: 3 }) }))] }) })] })] }, question.id));
|
|
47
56
|
}) })] })] }));
|
|
48
57
|
}
|
|
@@ -28,11 +28,11 @@ export function renderDedicatedReviewBody(ctx) {
|
|
|
28
28
|
switch (questionType) {
|
|
29
29
|
case 'FILL_IN_BLANK': {
|
|
30
30
|
const data = transformFillInBlank(apiQuestions);
|
|
31
|
-
return (_jsx(ReviewListenAndWriteRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, instruction: part.instructions || data.instruction, title: data.title, imageUrl: data.imageUrl, audioUrl: data.audioUrl, exampleText: data.exampleText, exampleHighlight: data.exampleHighlight, questions: data.questions, answers: answers, isCorrectMap: isCorrectMap
|
|
31
|
+
return (_jsx(ReviewListenAndWriteRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, instruction: part.instructions || data.instruction, title: data.title, imageUrl: data.imageUrl, audioUrl: data.audioUrl, exampleText: data.exampleText, exampleHighlight: data.exampleHighlight, questions: data.questions, answers: answers, isCorrectMap: isCorrectMap }));
|
|
32
32
|
}
|
|
33
33
|
case 'WRITE_CORRECT_VERB_FORM': {
|
|
34
34
|
const data = transformWriteCorrectVerbForm(apiQuestions);
|
|
35
|
-
return (_jsx(ReviewListenAndWriteRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, instruction: part.instructions || data.instruction, title: data.title, questions: data.questions, answers: answers, isCorrectMap: isCorrectMap
|
|
35
|
+
return (_jsx(ReviewListenAndWriteRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, instruction: part.instructions || data.instruction, title: data.title, questions: data.questions, answers: answers, isCorrectMap: isCorrectMap }));
|
|
36
36
|
}
|
|
37
37
|
case 'CHOOSE_THE_CORRECT_ANSWER': {
|
|
38
38
|
const data = transformChooseCorrectAnswer(apiQuestions);
|
|
@@ -4,8 +4,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
4
4
|
* Movers fallback review cases (isReviewMode=true).
|
|
5
5
|
* Source: oe-exam-fe ReviewQuestionRenderer.tsx
|
|
6
6
|
*/
|
|
7
|
-
import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, } from '../../../components/exams/take/components/question-renderers';
|
|
8
|
-
import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, } from '../../../components/exams/take/utils/question-transformers';
|
|
7
|
+
import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, } from '../../../components/exams/take/components/question-renderers';
|
|
8
|
+
import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformMatchingWithLinesGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, } from '../../../components/exams/take/utils/question-transformers';
|
|
9
9
|
import { normalizeCrossOutWordGroupAnswer, transformCrossOutWordGroup, } from '../../../components/exams/take/utils/cross-out-word-group.transformer';
|
|
10
10
|
function asApiQuestions(questions) {
|
|
11
11
|
return questions;
|
|
@@ -142,6 +142,41 @@ export function renderMoversReviewBody(ctx) {
|
|
|
142
142
|
});
|
|
143
143
|
return (_jsx(MoversTrueFalseCorrectGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerMap: correctAnswerMap }));
|
|
144
144
|
}
|
|
145
|
+
case 'FILL_IN_BLANK_GROUP': {
|
|
146
|
+
const data = transformFillInBlankGroup(apiQuestions);
|
|
147
|
+
const correctAnswerMap = {};
|
|
148
|
+
const parent = apiQuestions[0];
|
|
149
|
+
const rawCorrect = parent?.id ? rawCorrectAnswerMap[parent.id] : undefined;
|
|
150
|
+
const rawCorrectRecord = rawCorrect && typeof rawCorrect === 'object' && !Array.isArray(rawCorrect)
|
|
151
|
+
? rawCorrect
|
|
152
|
+
: undefined;
|
|
153
|
+
const correctArr = Array.isArray(rawCorrectRecord?.answer)
|
|
154
|
+
? rawCorrectRecord.answer
|
|
155
|
+
: Array.isArray(parent?.correct_answer?.answer)
|
|
156
|
+
? parent.correct_answer.answer
|
|
157
|
+
: [];
|
|
158
|
+
data.questions.forEach((question, index) => {
|
|
159
|
+
const rawItem = correctArr[index];
|
|
160
|
+
if (!rawItem || typeof rawItem !== 'object' || Array.isArray(rawItem)) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const obj = rawItem;
|
|
164
|
+
if (!Array.isArray(obj.answers)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
correctAnswerMap[question.id] = {
|
|
168
|
+
answers: obj.answers.map((blank) => (Array.isArray(blank)
|
|
169
|
+
? blank.map((alt) => String(alt ?? '').trim()).filter(Boolean)
|
|
170
|
+
: [String(blank ?? '').trim()].filter(Boolean))),
|
|
171
|
+
caseSensitive: obj.caseSensitive === true,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
return (_jsx(MoversFillInBlankGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: data.questions.filter((question) => !question.isExample).length, audioUrl: data.audioUrl, imageUrl: data.imageUrl, showHints: data.showHints, hints: data.hints, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerMap: correctAnswerMap }));
|
|
175
|
+
}
|
|
176
|
+
case 'MATCHING_WITH_LINES_GROUP': {
|
|
177
|
+
const data = transformMatchingWithLinesGroup(apiQuestions);
|
|
178
|
+
return (_jsx(MoversMatchingWithLinesGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, data: data, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true }));
|
|
179
|
+
}
|
|
145
180
|
case 'TRUE_FALSE': {
|
|
146
181
|
const data = transformTrueFalse(apiQuestions);
|
|
147
182
|
return (_jsx(MoversTrueFalseRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, imageUrl: data.imageUrl, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerTextMap: correctAnswerTextMap }));
|
|
@@ -9,6 +9,8 @@ const FLATTENED_ANSWER_TYPES = new Set([
|
|
|
9
9
|
'ANSWER_THE_QUESTION_GROUP',
|
|
10
10
|
'CROSS_OUT_WORD_GROUP',
|
|
11
11
|
'TRUE_FALSE_CORRECT_GROUP',
|
|
12
|
+
'FILL_IN_BLANK_GROUP',
|
|
13
|
+
'MATCHING_WITH_LINES_GROUP',
|
|
12
14
|
]);
|
|
13
15
|
export function transformResultAnswersForReview(answers, templateParts = []) {
|
|
14
16
|
const groups = groupAnswers(answers);
|
|
@@ -205,6 +207,41 @@ function transformStudentAnswer(questionId, questionType, value) {
|
|
|
205
207
|
return [`${questionId}-item-${index}`, { isTrue: null, correction: '' }];
|
|
206
208
|
}));
|
|
207
209
|
}
|
|
210
|
+
if (questionType === 'FILL_IN_BLANK_GROUP') {
|
|
211
|
+
const answer = Array.isArray(record.answer)
|
|
212
|
+
? record.answer
|
|
213
|
+
: Array.isArray(value)
|
|
214
|
+
? value
|
|
215
|
+
: [];
|
|
216
|
+
return Object.fromEntries(answer.map((item, index) => {
|
|
217
|
+
if (item && typeof item === 'object' && !Array.isArray(item) && Array.isArray(item.answers)) {
|
|
218
|
+
return [
|
|
219
|
+
`${questionId}-item-${index}`,
|
|
220
|
+
{
|
|
221
|
+
answers: item.answers.map((value) => String(value ?? '')),
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(item)) {
|
|
226
|
+
return [`${questionId}-item-${index}`, { answers: item.map((value) => String(value ?? '')) }];
|
|
227
|
+
}
|
|
228
|
+
if (typeof item === 'string') {
|
|
229
|
+
return [`${questionId}-item-${index}`, { answers: [item] }];
|
|
230
|
+
}
|
|
231
|
+
return [`${questionId}-item-${index}`, { answers: [] }];
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
if (questionType === 'MATCHING_WITH_LINES_GROUP') {
|
|
235
|
+
const answer = Array.isArray(record.answer)
|
|
236
|
+
? record.answer
|
|
237
|
+
: Array.isArray(value)
|
|
238
|
+
? value
|
|
239
|
+
: [];
|
|
240
|
+
return Object.fromEntries(answer.map((item, index) => [
|
|
241
|
+
`${questionId}-item-${index}`,
|
|
242
|
+
typeof item === 'string' ? item : '',
|
|
243
|
+
]));
|
|
244
|
+
}
|
|
208
245
|
if (questionType === 'CHOOSE_THE_CORRECT_ANSWER' || questionType === 'CHOOSE_CORRECT_ADJECTIVE') {
|
|
209
246
|
const rawAns = record.answer ?? record.optionId ?? record.optionIds;
|
|
210
247
|
const ids = getOptionIds(questionId, rawAns);
|
package/dist/index.d.ts
CHANGED
|
@@ -26,4 +26,4 @@ export { validateQuestion } from './components/questions/_shared/utils/question-
|
|
|
26
26
|
export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
|
|
27
27
|
export type { QuestionFormState } from './components/questions/_shared/hooks/useQuestionFormState';
|
|
28
28
|
export * from './components/questions/_shared/types';
|
|
29
|
-
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, } from './components/questions/viewer';
|
|
29
|
+
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, } from './components/questions/viewer';
|
package/dist/index.js
CHANGED
|
@@ -21,4 +21,4 @@ export { QUESTION_TYPE_CONFIG, QUESTION_TYPES_WITH_DEDICATED_COMPONENTS } from '
|
|
|
21
21
|
export { validateQuestion } from './components/questions/_shared/utils/question-validation';
|
|
22
22
|
export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
|
|
23
23
|
export * from './components/questions/_shared/types';
|
|
24
|
-
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, } from './components/questions/viewer';
|
|
24
|
+
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, } from './components/questions/viewer';
|
|
@@ -45,6 +45,7 @@ export const QUESTION_TYPE_SKILLS = {
|
|
|
45
45
|
'CHOOSE_THE_CORRECT_ANSWER_GROUP': SkillEnum.READING,
|
|
46
46
|
'CHOOSE_CORRECT_ADJECTIVE': SkillEnum.READING,
|
|
47
47
|
'FILL_IN_BLANK': SkillEnum.READING,
|
|
48
|
+
'FILL_IN_BLANK_GROUP': SkillEnum.READING,
|
|
48
49
|
'FILL_MISSING_WORDS_IN_GRID': SkillEnum.READING,
|
|
49
50
|
'READ_PASSAGE_AND_ANSWER_QUESTIONS': SkillEnum.READING,
|
|
50
51
|
'ANSWER_THE_QUESTION': SkillEnum.READING,
|
|
@@ -53,6 +54,7 @@ export const QUESTION_TYPE_SKILLS = {
|
|
|
53
54
|
'WORD_FILL_PARAGRAPH': SkillEnum.READING,
|
|
54
55
|
'MATCH_BY_WRITING_ANSWER': SkillEnum.READING,
|
|
55
56
|
'MATCH_WORD_TO_PICTURE': SkillEnum.READING,
|
|
57
|
+
'MATCHING_WITH_LINES_GROUP': SkillEnum.READING,
|
|
56
58
|
// Visual-based reading questions
|
|
57
59
|
'LABEL_THE_PICTURE': SkillEnum.READING,
|
|
58
60
|
'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER': SkillEnum.READING,
|
|
@@ -177,6 +179,7 @@ export const QUESTION_TYPE_LABELS = {
|
|
|
177
179
|
'CHOOSE_THE_CORRECT_ANSWER_GROUP': 'Chọn đáp án đúng (nhóm)',
|
|
178
180
|
'CHOOSE_CORRECT_ADJECTIVE': 'Chọn tính từ',
|
|
179
181
|
'FILL_IN_BLANK': 'Điền vào chỗ trống',
|
|
182
|
+
'FILL_IN_BLANK_GROUP': 'Điền chỗ trống (nhóm)',
|
|
180
183
|
'FILL_MISSING_WORDS_IN_GRID': 'Điền từ vào lưới',
|
|
181
184
|
'READ_PASSAGE_AND_ANSWER_QUESTIONS': 'Đọc hiểu - Trả lời',
|
|
182
185
|
'LABEL_THE_PICTURE': 'Gắn nhãn hình ảnh',
|
|
@@ -209,6 +212,7 @@ export const QUESTION_TYPE_LABELS = {
|
|
|
209
212
|
'WORD_FILL_PARAGRAPH': 'Điền từ vào đoạn văn',
|
|
210
213
|
'MATCH_BY_WRITING_ANSWER': 'Ghép đáp án bằng cách viết',
|
|
211
214
|
'MATCH_WORD_TO_PICTURE': 'Nối từ với tranh',
|
|
215
|
+
'MATCHING_WITH_LINES_GROUP': 'Nối bằng đường kẻ (nhóm)',
|
|
212
216
|
'WORD_FILL_STRUCTURED_FORM': 'Điền từ vào biểu mẫu có cấu trúc',
|
|
213
217
|
'CROSSWORD_PUZZLE': 'Giải ô chữ (Crossword)',
|
|
214
218
|
'FIND_WORDS_IN_MATRIX': 'Tìm từ trong ma trận (Word Search)',
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FILL_IN_BLANK — single source of truth for the whole flow.
|
|
3
|
+
*
|
|
4
|
+
* Wire format (never change one side without the other):
|
|
5
|
+
* content.question "A dog {0} swim, but it {1} fly."
|
|
6
|
+
* correct_answer.answers string[][] — answers[i] = accepted equivalents for blank {i}
|
|
7
|
+
* student answer { answers: string[] } — one entry per blank, index-aligned
|
|
8
|
+
*
|
|
9
|
+
* Matching rule (mirrors the API `normalizeText`): trim, lowercase, collapse inner
|
|
10
|
+
* whitespace, keep diacritics. A blank is correct when the input equals ANY equivalent.
|
|
11
|
+
*/
|
|
12
|
+
export type FillBlankSegment = {
|
|
13
|
+
type: 'text';
|
|
14
|
+
value: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'blank';
|
|
17
|
+
index: number;
|
|
18
|
+
};
|
|
19
|
+
/** Split a stem into the text / blank segments used by every renderer. */
|
|
20
|
+
export declare function parseFillBlankSegments(text: string): FillBlankSegment[];
|
|
21
|
+
/**
|
|
22
|
+
* Renderer-facing parse: like `parseFillBlankSegments`, but guarantees at least
|
|
23
|
+
* one blank so a stem authored without `{0}` still gets an input box.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseFillBlankStem(text: string): FillBlankSegment[];
|
|
26
|
+
/** Number of input boxes a stem needs (highest placeholder index + 1). */
|
|
27
|
+
export declare function countBlanks(segments: FillBlankSegment[]): number;
|
|
28
|
+
/** Placeholder indices present in a stem, ascending and de-duplicated. */
|
|
29
|
+
export declare function extractPlaceholderIndices(text: string): number[];
|
|
30
|
+
/** Normalize one blank's stored value into a list of accepted equivalents. */
|
|
31
|
+
export declare function toAcceptedAnswers(value: unknown): string[];
|
|
32
|
+
/**
|
|
33
|
+
* Normalize `correct_answer.answers` into `string[][]` padded to `blankCount`.
|
|
34
|
+
* Accepts the canonical `string[][]` and the legacy flat `string[]`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function normalizeAcceptedAnswers(raw: unknown, blankCount: number): string[][];
|
|
37
|
+
/** Normalize a stored student answer into one string per blank. */
|
|
38
|
+
export declare function toBlankValues(raw: unknown, blankCount: number): string[];
|
|
39
|
+
/** True when the student typed something into at least one blank. */
|
|
40
|
+
export declare function hasAnyBlankValue(raw: unknown): boolean;
|
|
41
|
+
/** Compare a student input against the accepted equivalents of one blank. */
|
|
42
|
+
export declare function isBlankCorrect(value: string, accepted: string[] | undefined): boolean;
|
|
43
|
+
/** Join equivalents for display, e.g. "is / 's". */
|
|
44
|
+
export declare function formatAcceptedAnswers(accepted: string[] | undefined): string;
|
|
45
|
+
/** First equivalent — used to prefill example questions and review hints. */
|
|
46
|
+
export declare function getPrimaryAnswer(accepted: string[] | undefined): string;
|