@tinyweb_dev/oe-exam-sdk 0.2.3 → 0.2.5
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/components/QuestionRenderer.js +11 -3
- package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.d.ts +10 -0
- package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.js +38 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -11
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.js +75 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.js +92 -0
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +2 -1
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +2 -2
- 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/exam-taking.utils.d.ts +1 -0
- package/dist/components/exams/take/exam-taking.utils.js +9 -1
- package/dist/components/exams/take/types.d.ts +3 -2
- package/dist/components/exams/take/utils/answer-transformers.js +12 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +29 -1
- package/dist/components/exams/take/utils/question-transformers.js +98 -0
- package/dist/components/questions/_shared/config/question-types.config.js +12 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +9 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +49 -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/match-word-to-picture.type.d.ts +62 -0
- package/dist/components/questions/_shared/types/match-word-to-picture.type.js +1 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +42 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.js +2 -0
- package/dist/components/questions/_shared/types/write-a-short-letter.type.d.ts +4 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +2 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +1 -0
- package/dist/components/questions/creator/question-type-registry.js +4 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +13 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +32 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.d.ts +1 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +42 -6
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +40 -20
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.d.ts +1 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +5 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +31 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.js +267 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +65 -0
- package/dist/components/questions/types/match-word-to-picture/WordBankEditor.d.ts +8 -0
- package/dist/components/questions/types/match-word-to-picture/WordBankEditor.js +15 -0
- package/dist/components/questions/types/match-word-to-picture/index.d.ts +6 -0
- package/dist/components/questions/types/match-word-to-picture/index.js +6 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.js +53 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.d.ts +4 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.js +75 -0
- package/dist/components/questions/types/match-word-to-picture/register.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/register.js +55 -0
- package/dist/components/questions/types/match-word-to-picture/transform.d.ts +11 -0
- package/dist/components/questions/types/match-word-to-picture/transform.js +43 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +25 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.js +144 -0
- package/dist/components/questions/types/true-false-correct-group/index.d.ts +5 -0
- package/dist/components/questions/types/true-false-correct-group/index.js +5 -0
- package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.d.ts +4 -0
- package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.js +79 -0
- package/dist/components/questions/types/true-false-correct-group/register.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/register.js +36 -0
- package/dist/components/questions/types/true-false-correct-group/transform.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/transform.js +49 -0
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +27 -3
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +8 -2
- package/dist/components/questions/types/write-a-short-letter/register.js +4 -0
- package/dist/components/questions/types/write-a-short-letter/transform.js +4 -0
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +8 -0
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +11 -0
- package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.d.ts +2 -1
- package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +2 -2
- package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.d.ts +9 -0
- package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +11 -0
- package/dist/components/questions/viewer/MatchWordToPictureViewer.d.ts +9 -0
- package/dist/components/questions/viewer/MatchWordToPictureViewer.js +8 -0
- package/dist/components/questions/viewer/QuestionViewer.js +58 -9
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +9 -0
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -0
- package/dist/components/questions/viewer/index.d.ts +4 -0
- package/dist/components/questions/viewer/index.js +4 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
- package/dist/components/results/renderers/review-question-body-movers.js +39 -3
- package/dist/components/results/review-data.js +22 -0
- package/dist/components/themes/english-certification/EcWritingTask.js +2 -2
- 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/hooks/usePresignedFileUrl.d.ts +3 -1
- package/dist/shared/lib/hooks/usePresignedFileUrl.js +30 -12
- package/dist/shared/lib/utils/question-reverse-transform.js +111 -2
- 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/choose-the-correct-answer-group.d.ts +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/match-word-to-picture.d.ts +35 -0
- package/dist/shared/types/questions/match-word-to-picture.js +5 -0
- package/dist/shared/types/questions/true-false-correct-group.d.ts +39 -0
- package/dist/shared/types/questions/true-false-correct-group.js +1 -0
- package/package.json +7 -2
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/true-false-correct-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
|
+
function parseItemAnswer(raw) {
|
|
12
|
+
if (!isRecord(raw)) {
|
|
13
|
+
return { isTrue: true, correction: '' };
|
|
14
|
+
}
|
|
15
|
+
const isTrue = raw.isTrue === true;
|
|
16
|
+
return {
|
|
17
|
+
isTrue,
|
|
18
|
+
correction: isTrue ? '' : asString(raw.correction).trim(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function createEmptyTrueFalseCorrectGroupItem(questionNumber) {
|
|
22
|
+
return {
|
|
23
|
+
question: '',
|
|
24
|
+
isTrue: true,
|
|
25
|
+
correction: '',
|
|
26
|
+
isExample: false,
|
|
27
|
+
points: 1,
|
|
28
|
+
questionNumber,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function mapTrueFalseCorrectGroupItem(item, index, fallbackAnswer) {
|
|
32
|
+
const record = asRecord(item);
|
|
33
|
+
const content = asRecord(record.content);
|
|
34
|
+
const correctAnswer = asRecord(record.correctAnswer);
|
|
35
|
+
const isExample = record.isExample === true;
|
|
36
|
+
const parsed = parseItemAnswer(correctAnswer.answer ?? fallbackAnswer);
|
|
37
|
+
return {
|
|
38
|
+
question: asString(content.question) || asString(record.question),
|
|
39
|
+
isTrue: parsed.isTrue,
|
|
40
|
+
correction: parsed.correction,
|
|
41
|
+
isExample,
|
|
42
|
+
points: isExample ? 0 : typeof record.points === 'number' ? record.points : 1,
|
|
43
|
+
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function mapQuestionToTrueFalseCorrectGroupData(question) {
|
|
47
|
+
const source = asRecord(question);
|
|
48
|
+
const content = asRecord(source.content);
|
|
49
|
+
const answer = asRecord(source.answer);
|
|
50
|
+
const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
|
|
51
|
+
const explanation = String(source.explanation || answer.explanation || '');
|
|
52
|
+
const fallbackAnswers = Array.isArray(correctAnswer.answer)
|
|
53
|
+
? correctAnswer.answer
|
|
54
|
+
: [];
|
|
55
|
+
if (Array.isArray(content.items)) {
|
|
56
|
+
const items = content.items.map((item, index) => mapTrueFalseCorrectGroupItem(item, index, fallbackAnswers[index]));
|
|
57
|
+
return {
|
|
58
|
+
instruction: asString(asRecord(content.meta).instruction)
|
|
59
|
+
|| TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION,
|
|
60
|
+
items,
|
|
61
|
+
explanation,
|
|
62
|
+
points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(answer.items)) {
|
|
66
|
+
return {
|
|
67
|
+
instruction: asString(answer.instruction, TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION),
|
|
68
|
+
items: answer.items.map((item, index) => mapTrueFalseCorrectGroupItem(item, index, fallbackAnswers[index])),
|
|
69
|
+
explanation,
|
|
70
|
+
points: typeof answer.points === 'number' ? answer.points : undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
instruction: TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION,
|
|
75
|
+
items: [createEmptyTrueFalseCorrectGroupItem(1)],
|
|
76
|
+
explanation,
|
|
77
|
+
points: 1,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { TrueFalseCorrectGroupCreator } from './TrueFalseCorrectGroupCreator';
|
|
2
|
+
import { mapQuestionToTrueFalseCorrectGroupData } from './map-true-false-correct-group-data';
|
|
3
|
+
export const trueFalseCorrectGroupRegistration = {
|
|
4
|
+
component: TrueFalseCorrectGroupCreator,
|
|
5
|
+
transformInitialData: (initialData) => mapQuestionToTrueFalseCorrectGroupData(initialData),
|
|
6
|
+
getExtraProps: (ctx) => ({
|
|
7
|
+
onUnsavedChangesChange: ctx.onUnsavedChangesChange,
|
|
8
|
+
validationRef: ctx.validationRef,
|
|
9
|
+
}),
|
|
10
|
+
wrapOnSave: (data, ctx) => {
|
|
11
|
+
const payload = data;
|
|
12
|
+
return {
|
|
13
|
+
type: ctx.questionType,
|
|
14
|
+
content: ctx.state.content,
|
|
15
|
+
points: payload.points ?? ctx.state.points ?? 1,
|
|
16
|
+
level: ctx.state.level,
|
|
17
|
+
difficulty: ctx.state.difficulty,
|
|
18
|
+
skill: ctx.state.skill,
|
|
19
|
+
answer: payload,
|
|
20
|
+
explanation: payload.explanation || '',
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
wrapOnChange: (data, ctx) => {
|
|
24
|
+
const payload = data;
|
|
25
|
+
return {
|
|
26
|
+
type: ctx.questionType,
|
|
27
|
+
content: ctx.state.content,
|
|
28
|
+
points: payload.points ?? ctx.state.points ?? 1,
|
|
29
|
+
level: ctx.state.level,
|
|
30
|
+
difficulty: ctx.state.difficulty,
|
|
31
|
+
skill: ctx.state.skill,
|
|
32
|
+
answer: payload,
|
|
33
|
+
explanation: payload.explanation || '',
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION, TRUE_FALSE_CORRECT_ITEM_TYPE, } from '../../_shared/types/true-false-correct-group.type';
|
|
2
|
+
function sumGradablePoints(items) {
|
|
3
|
+
return items.reduce((total, item) => {
|
|
4
|
+
if (item.isExample) {
|
|
5
|
+
return total;
|
|
6
|
+
}
|
|
7
|
+
const points = typeof item.points === 'number' ? item.points : 1;
|
|
8
|
+
return total + points;
|
|
9
|
+
}, 0);
|
|
10
|
+
}
|
|
11
|
+
function toItemAnswer(item) {
|
|
12
|
+
const isTrue = item.isTrue === true;
|
|
13
|
+
return {
|
|
14
|
+
isTrue,
|
|
15
|
+
correction: isTrue ? '' : (item.correction || '').trim(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export const transformTrueFalseCorrectGroup = (question) => {
|
|
19
|
+
const answerData = question.answer;
|
|
20
|
+
const items = Array.isArray(answerData?.items) ? answerData.items : [];
|
|
21
|
+
if (items.length === 0) {
|
|
22
|
+
return { apiContent: { items: [] }, apiCorrectAnswer: { answer: [] } };
|
|
23
|
+
}
|
|
24
|
+
const apiItems = items.map((item, index) => {
|
|
25
|
+
const correctAnswer = toItemAnswer(item);
|
|
26
|
+
return {
|
|
27
|
+
questionType: TRUE_FALSE_CORRECT_ITEM_TYPE,
|
|
28
|
+
...(item.isExample ? { isExample: true } : {}),
|
|
29
|
+
content: {
|
|
30
|
+
question: item.question || '',
|
|
31
|
+
},
|
|
32
|
+
correctAnswer: { answer: correctAnswer },
|
|
33
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
34
|
+
questionNumber: item.questionNumber || index + 1,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
apiContent: {
|
|
39
|
+
meta: {
|
|
40
|
+
instruction: answerData?.instruction || TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION,
|
|
41
|
+
},
|
|
42
|
+
items: apiItems,
|
|
43
|
+
},
|
|
44
|
+
apiCorrectAnswer: {
|
|
45
|
+
answer: apiItems.map((item) => item.correctAnswer.answer),
|
|
46
|
+
},
|
|
47
|
+
totalPoints: sumGradablePoints(items),
|
|
48
|
+
};
|
|
49
|
+
};
|
|
@@ -6,6 +6,15 @@ import { cn } from '../../../../shared/lib/utils';
|
|
|
6
6
|
import { HtmlOrText } from '../../../../shared/lib/html-content';
|
|
7
7
|
import { FileText, PenLine, Check, AlertCircle, BookOpen, Lightbulb, ImageIcon } from 'lucide-react';
|
|
8
8
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks/usePresignedFileUrl';
|
|
9
|
+
function normalizePassage(raw) {
|
|
10
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
11
|
+
return [raw];
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(raw)) {
|
|
14
|
+
return raw.filter((item) => typeof item === 'string' && item.trim().length > 0);
|
|
15
|
+
}
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
9
18
|
// Component to render a single guide image in client mode
|
|
10
19
|
function GuideImageDisplay({ imageKey }) {
|
|
11
20
|
const { previewUrl } = usePresignedFileUrl(imageKey);
|
|
@@ -17,8 +26,23 @@ function GuideImageDisplay({ imageKey }) {
|
|
|
17
26
|
} }) }));
|
|
18
27
|
}
|
|
19
28
|
export function WriteAShortLetterClient({ questionData, onSubmit, isReviewMode = false, userAnswer, explanation, }) {
|
|
20
|
-
const {
|
|
21
|
-
const instruction = instructions?.mainInstruction ||
|
|
29
|
+
const { minWords = 20, maxWords } = questionData;
|
|
30
|
+
const instruction = questionData.instructions?.mainInstruction ||
|
|
31
|
+
(typeof questionData.instructions === 'string' ? questionData.instructions : '') ||
|
|
32
|
+
questionData.instruction ||
|
|
33
|
+
questionData.prompt ||
|
|
34
|
+
'';
|
|
35
|
+
const informations = Array.isArray(questionData.informations)
|
|
36
|
+
? questionData.informations
|
|
37
|
+
: Array.isArray(questionData.requirements)
|
|
38
|
+
? questionData.requirements
|
|
39
|
+
: [];
|
|
40
|
+
const guideImages = Array.isArray(questionData.guideImages)
|
|
41
|
+
? questionData.guideImages
|
|
42
|
+
: Array.isArray(questionData.images)
|
|
43
|
+
? questionData.images
|
|
44
|
+
: [];
|
|
45
|
+
const passageList = normalizePassage(questionData.passage ?? questionData.passages);
|
|
22
46
|
// Initialize answer state - ensure it's always a string
|
|
23
47
|
const [answer, setAnswer] = useState(typeof userAnswer === 'string' ? userAnswer : '');
|
|
24
48
|
// Sync userAnswer when it changes (e.g., when loading from server in review mode)
|
|
@@ -61,7 +85,7 @@ export function WriteAShortLetterClient({ questionData, onSubmit, isReviewMode =
|
|
|
61
85
|
const target = minWords;
|
|
62
86
|
return Math.min((wordCount / target) * 100, 100);
|
|
63
87
|
}, [wordCount, minWords]);
|
|
64
|
-
return (_jsxs("div", { className: "space-y-5", children: [instruction && (_jsx("div", { className: "overflow-hidden rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 shadow-sm", children: _jsxs("div", { className: "flex items-center gap-3 px-5 py-4", children: [_jsx("div", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm", children: _jsx(BookOpen, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-blue-900", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(HtmlOrText, { value: instruction, className: "text-sm text-blue-700" })] })] }) })), guideImages && guideImages.length > 0 && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-3 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 text-white shadow-sm", children: _jsx(ImageIcon, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "\u1EA2nh h\u01B0\u1EDBng d\u1EABn" })] }), _jsx("div", { className: "p-5", children: _jsx("div", { className: "flex flex-col gap-4", children: guideImages.map((imageKey, index) => (_jsx(GuideImageDisplay, { imageKey: imageKey }, `${imageKey}-${index}`))) }) })] })), informations && informations.filter((i) => i?.trim()).length > 0 && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-3 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 text-white shadow-sm", children: _jsx(FileText, { className: "h-4 w-4" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["Th\u00F4ng tin c\u1EA7n bao g\u1ED3m (", informations.length, ")"] })] }), _jsx("div", { className: "p-5", children: _jsx("div", { className: "space-y-3", children: informations.filter((i) => i?.trim()).map((info, index) => (_jsxs("div", { className: "flex items-start gap-3 rounded-lg border border-gray-100 bg-gradient-to-r from-white to-gray-50 p-3 transition-all hover:border-gray-200 hover:shadow-sm", children: [_jsx("div", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-emerald-400 to-teal-500 text-xs font-bold text-white shadow-sm", children: index + 1 }), _jsx(HtmlOrText, { value: info, className: "text-sm text-gray-700 leading-relaxed pt-0.5" })] }, index))) }) })] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-purple-500 to-violet-600 text-white shadow-sm", children: _jsx(PenLine, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "Vi\u1EBFt th\u01B0 c\u1EE7a b\u1EA1n" })] }), _jsxs("div", { className: cn('flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-semibold transition-all', isExceeded
|
|
88
|
+
return (_jsxs("div", { className: "space-y-5", children: [instruction && (_jsx("div", { className: "overflow-hidden rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 shadow-sm", children: _jsxs("div", { className: "flex items-center gap-3 px-5 py-4", children: [_jsx("div", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm", children: _jsx(BookOpen, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-blue-900", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(HtmlOrText, { value: instruction, className: "text-sm text-blue-700" })] })] }) })), passageList.length > 0 && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-indigo-100 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-3 border-b border-indigo-50 bg-gradient-to-r from-indigo-50 to-white px-5 py-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-blue-600 text-white shadow-sm", children: _jsx(BookOpen, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-indigo-800", children: "B\u00E0i \u0111\u1ECDc" })] }), _jsx("div", { className: "space-y-3 p-5", children: passageList.map((item, index) => (_jsx(HtmlOrText, { value: item, className: "text-sm leading-relaxed text-gray-800" }, index))) })] })), guideImages && guideImages.length > 0 && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-3 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 text-white shadow-sm", children: _jsx(ImageIcon, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "\u1EA2nh h\u01B0\u1EDBng d\u1EABn" })] }), _jsx("div", { className: "p-5", children: _jsx("div", { className: "flex flex-col gap-4", children: guideImages.map((imageKey, index) => (_jsx(GuideImageDisplay, { imageKey: imageKey }, `${imageKey}-${index}`))) }) })] })), informations && informations.filter((i) => i?.trim()).length > 0 && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-3 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 text-white shadow-sm", children: _jsx(FileText, { className: "h-4 w-4" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["Th\u00F4ng tin c\u1EA7n bao g\u1ED3m (", informations.length, ")"] })] }), _jsx("div", { className: "p-5", children: _jsx("div", { className: "space-y-3", children: informations.filter((i) => i?.trim()).map((info, index) => (_jsxs("div", { className: "flex items-start gap-3 rounded-lg border border-gray-100 bg-gradient-to-r from-white to-gray-50 p-3 transition-all hover:border-gray-200 hover:shadow-sm", children: [_jsx("div", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-emerald-400 to-teal-500 text-xs font-bold text-white shadow-sm", children: index + 1 }), _jsx(HtmlOrText, { value: info, className: "text-sm text-gray-700 leading-relaxed pt-0.5" })] }, index))) }) })] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-purple-500 to-violet-600 text-white shadow-sm", children: _jsx(PenLine, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "Vi\u1EBFt th\u01B0 c\u1EE7a b\u1EA1n" })] }), _jsxs("div", { className: cn('flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-semibold transition-all', isExceeded
|
|
65
89
|
? 'bg-red-100 text-red-700'
|
|
66
90
|
: isValidWordCount
|
|
67
91
|
? 'bg-green-100 text-green-700'
|
|
@@ -36,6 +36,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
|
|
|
36
36
|
const [maxWords, setMaxWords] = useState(initialData?.maxWords);
|
|
37
37
|
const [instruction, setInstruction] = useState(initialData?.instructions?.mainInstruction || '');
|
|
38
38
|
const [guideImages, setGuideImages] = useState(initialData?.guideImages || []);
|
|
39
|
+
const [passage, setPassage] = useState(initialData?.passage || '');
|
|
39
40
|
const [points, setPoints] = useState(initialData?.points || 10);
|
|
40
41
|
// Store presigned URLs keyed by imageKey (NOT index) to avoid index races
|
|
41
42
|
// when the user uploads several images quickly.
|
|
@@ -80,7 +81,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
|
|
|
80
81
|
},
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
|
-
}, [validationRef, informations, minWords, maxWords, instruction, guideImages, points]);
|
|
84
|
+
}, [validationRef, informations, minWords, maxWords, instruction, guideImages, passage, points]);
|
|
84
85
|
// Sync state from initialData when it changes
|
|
85
86
|
useEffect(() => {
|
|
86
87
|
const incomingStr = JSON.stringify(initialData);
|
|
@@ -106,6 +107,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
|
|
|
106
107
|
setMaxWords(initialData?.maxWords);
|
|
107
108
|
setInstruction(initialData?.instructions?.mainInstruction ?? '');
|
|
108
109
|
setGuideImages(initialData?.guideImages ?? []);
|
|
110
|
+
setPassage(initialData?.passage ?? '');
|
|
109
111
|
setPoints(initialData?.points ?? 10);
|
|
110
112
|
// Clear stale preview URLs from the previous question's uploads
|
|
111
113
|
setGuideImagePreviewUrls({});
|
|
@@ -133,6 +135,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
|
|
|
133
135
|
maxWords,
|
|
134
136
|
instruction,
|
|
135
137
|
guideImages,
|
|
138
|
+
passage,
|
|
136
139
|
points,
|
|
137
140
|
});
|
|
138
141
|
previousPayloadRef.current = currentPayload;
|
|
@@ -151,6 +154,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
|
|
|
151
154
|
mainInstruction: instruction || '',
|
|
152
155
|
},
|
|
153
156
|
guideImages: guideImages.length > 0 ? guideImages : undefined,
|
|
157
|
+
...(passage ? { passage } : {}),
|
|
154
158
|
points,
|
|
155
159
|
};
|
|
156
160
|
// Serialize payload for comparison
|
|
@@ -169,6 +173,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
|
|
|
169
173
|
maxWords,
|
|
170
174
|
instructions: { mainInstruction: instruction || '' },
|
|
171
175
|
guideImages,
|
|
176
|
+
...(passage ? { passage } : {}),
|
|
172
177
|
points,
|
|
173
178
|
});
|
|
174
179
|
recentEmittedInitialDataRef.current = [
|
|
@@ -177,7 +182,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
|
|
|
177
182
|
].slice(-10);
|
|
178
183
|
onChangeRef.current?.(payload);
|
|
179
184
|
}
|
|
180
|
-
}, [informations, minWords, maxWords, instruction, guideImages, points]);
|
|
185
|
+
}, [informations, minWords, maxWords, instruction, guideImages, passage, points]);
|
|
181
186
|
// Get validation errors
|
|
182
187
|
const getErrors = () => {
|
|
183
188
|
const errors = {};
|
|
@@ -287,6 +292,7 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
|
|
|
287
292
|
mainInstruction: instruction || '',
|
|
288
293
|
},
|
|
289
294
|
guideImages: guideImages.length > 0 ? guideImages : undefined,
|
|
295
|
+
...(passage ? { passage } : {}),
|
|
290
296
|
};
|
|
291
297
|
return (_jsxs("div", { className: "space-y-6", children: [_jsx(Card, { children: _jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { children: "Write a Short Letter (Preview)" }), _jsx("p", { className: "mt-1 text-sm text-gray-500", children: "Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc - Hi\u1EC3n th\u1ECB \u0111\u00E1p \u00E1n \u0111\u00FAng" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Quay l\u1EA1i Edit Mode"] })] }) }) }), _jsx(WriteAShortLetterClient, { questionData: questionData, onSubmit: () => { }, isReviewMode: true })] }));
|
|
292
298
|
}
|
|
@@ -8,6 +8,7 @@ export const writeAShortLetterRegistration = {
|
|
|
8
8
|
? parseFloat(initialData.total_points)
|
|
9
9
|
: (initialData?.points || 10); // WRITE_A_SHORT_LETTER defaults to 10 points
|
|
10
10
|
let creatorData = { points: apiPoints };
|
|
11
|
+
const passage = answer?.passage || content?.passage;
|
|
11
12
|
// Priority 1: Data from store (answer has informations directly)
|
|
12
13
|
if (answer?.informations && Array.isArray(answer.informations)) {
|
|
13
14
|
creatorData = {
|
|
@@ -16,6 +17,7 @@ export const writeAShortLetterRegistration = {
|
|
|
16
17
|
maxWords: answer.maxWords,
|
|
17
18
|
instructions: answer.instructions || { mainInstruction: '' },
|
|
18
19
|
guideImages: answer.guideImages || [],
|
|
20
|
+
...(passage ? { passage } : {}),
|
|
19
21
|
points: apiPoints,
|
|
20
22
|
};
|
|
21
23
|
}
|
|
@@ -27,6 +29,7 @@ export const writeAShortLetterRegistration = {
|
|
|
27
29
|
maxWords: content.maxWords,
|
|
28
30
|
instructions: content.instructions || { mainInstruction: '' },
|
|
29
31
|
guideImages: content.guideImages || [],
|
|
32
|
+
...(passage ? { passage } : {}),
|
|
30
33
|
points: apiPoints,
|
|
31
34
|
};
|
|
32
35
|
}
|
|
@@ -42,6 +45,7 @@ export const writeAShortLetterRegistration = {
|
|
|
42
45
|
maxWords: content?.maxWords ?? answer?.maxWords,
|
|
43
46
|
instructions: content?.instructions || answer?.instructions || { mainInstruction: '' },
|
|
44
47
|
guideImages: content?.guideImages || answer?.guideImages || [],
|
|
48
|
+
...(passage ? { passage } : {}),
|
|
45
49
|
points: apiPoints,
|
|
46
50
|
};
|
|
47
51
|
}
|
|
@@ -9,6 +9,7 @@ export const transformWriteAShortLetter = (question) => {
|
|
|
9
9
|
const guideImages = Array.isArray(answerData?.guideImages)
|
|
10
10
|
? answerData.guideImages.filter((url) => typeof url === 'string' && url.trim() !== '')
|
|
11
11
|
: [];
|
|
12
|
+
const passage = typeof answerData?.passage === 'string' ? answerData.passage.trim() : '';
|
|
12
13
|
const apiContent = {
|
|
13
14
|
informations,
|
|
14
15
|
minWords: answerData?.minWords || 20,
|
|
@@ -18,6 +19,9 @@ export const transformWriteAShortLetter = (question) => {
|
|
|
18
19
|
if (guideImages.length > 0) {
|
|
19
20
|
apiContent.guideImages = guideImages;
|
|
20
21
|
}
|
|
22
|
+
if (passage) {
|
|
23
|
+
apiContent.passage = passage;
|
|
24
|
+
}
|
|
21
25
|
return {
|
|
22
26
|
apiContent,
|
|
23
27
|
apiCorrectAnswer: {},
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Question } from '../../../shared/types/entities/question.types';
|
|
2
|
+
type AnswerTheQuestionGroupViewerProps = {
|
|
3
|
+
question: Question;
|
|
4
|
+
isReviewMode?: boolean;
|
|
5
|
+
userAnswer?: string[];
|
|
6
|
+
};
|
|
7
|
+
export declare function AnswerTheQuestionGroupViewer({ question, isReviewMode, userAnswer, }: AnswerTheQuestionGroupViewerProps): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { AnswerTheQuestionGroupClient } from '../types/answer-the-question-group/AnswerTheQuestionGroupClient';
|
|
4
|
+
import { mapQuestionToAnswerTheQuestionGroupData } from '../types/answer-the-question-group/map-answer-the-question-group-data';
|
|
5
|
+
export function AnswerTheQuestionGroupViewer({ question, isReviewMode = false, userAnswer, }) {
|
|
6
|
+
const questionData = mapQuestionToAnswerTheQuestionGroupData(question);
|
|
7
|
+
const reviewAnswers = isReviewMode && !userAnswer
|
|
8
|
+
? questionData.items.map((item) => item.expectedAnswers[0] || '')
|
|
9
|
+
: userAnswer;
|
|
10
|
+
return (_jsx(AnswerTheQuestionGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers }));
|
|
11
|
+
}
|
|
@@ -3,6 +3,7 @@ type ChooseTheCorrectAnswerGroupViewerProps = {
|
|
|
3
3
|
question: Question;
|
|
4
4
|
isReviewMode?: boolean;
|
|
5
5
|
userAnswer?: string[];
|
|
6
|
+
onSubmitAnswer?: (answer: string[]) => void;
|
|
6
7
|
};
|
|
7
|
-
export declare function ChooseTheCorrectAnswerGroupViewer({ question, isReviewMode, userAnswer, }: ChooseTheCorrectAnswerGroupViewerProps): import("react").JSX.Element;
|
|
8
|
+
export declare function ChooseTheCorrectAnswerGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: ChooseTheCorrectAnswerGroupViewerProps): import("react").JSX.Element;
|
|
8
9
|
export {};
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { ChooseTheCorrectAnswerGroupClient } from '../types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient';
|
|
4
4
|
import { mapQuestionToChooseAnswerGroupData } from '../types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data';
|
|
5
|
-
export function ChooseTheCorrectAnswerGroupViewer({ question, isReviewMode = false, userAnswer, }) {
|
|
5
|
+
export function ChooseTheCorrectAnswerGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
6
6
|
const questionData = mapQuestionToChooseAnswerGroupData(question);
|
|
7
7
|
const reviewAnswers = isReviewMode && !userAnswer
|
|
8
8
|
? questionData.items.map((item) => item.correctAnswer)
|
|
9
9
|
: userAnswer;
|
|
10
|
-
return (_jsx(ChooseTheCorrectAnswerGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers }));
|
|
10
|
+
return (_jsx(ChooseTheCorrectAnswerGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
|
|
11
11
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Question } from '../../../shared/types/entities/question.types';
|
|
2
|
+
type MatchWordToPictureGroupViewerProps = {
|
|
3
|
+
question: Question;
|
|
4
|
+
isReviewMode?: boolean;
|
|
5
|
+
userAnswer?: string[];
|
|
6
|
+
onSubmitAnswer?: (answer: string[]) => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function MatchWordToPictureGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: MatchWordToPictureGroupViewerProps): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { MatchWordToPictureGroupClient } from '../types/match-word-to-picture/MatchWordToPictureGroupClient';
|
|
4
|
+
import { mapQuestionToMatchWordToPictureGroupData } from '../types/match-word-to-picture/map-match-word-to-picture-group-data';
|
|
5
|
+
export function MatchWordToPictureGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
6
|
+
const questionData = mapQuestionToMatchWordToPictureGroupData(question);
|
|
7
|
+
const reviewAnswers = isReviewMode && !userAnswer
|
|
8
|
+
? questionData.items.map((item) => item.correctAnswer)
|
|
9
|
+
: userAnswer;
|
|
10
|
+
return (_jsx(MatchWordToPictureGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Question } from '../../../shared/types/entities/question.types';
|
|
2
|
+
type MatchWordToPictureViewerProps = {
|
|
3
|
+
question: Question;
|
|
4
|
+
isReviewMode?: boolean;
|
|
5
|
+
userAnswer?: string;
|
|
6
|
+
onSubmitAnswer?: (answer: string) => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function MatchWordToPictureViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: MatchWordToPictureViewerProps): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { MatchWordToPictureClient } from '../types/match-word-to-picture/MatchWordToPictureClient';
|
|
4
|
+
import { mapQuestionToMatchWordToPictureData } from '../types/match-word-to-picture/map-match-word-to-picture-data';
|
|
5
|
+
export function MatchWordToPictureViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
6
|
+
const questionData = mapQuestionToMatchWordToPictureData(question);
|
|
7
|
+
return (_jsx(MatchWordToPictureClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswer: userAnswer, autoFillCorrectAnswers: isReviewMode && !userAnswer, explanation: question.explanation, onSubmit: isReviewMode ? undefined : onSubmitAnswer }));
|
|
8
|
+
}
|
|
@@ -20,13 +20,18 @@ import { LookPictureFillBlankChooseAnswerClient } from '../types/look-picture-fi
|
|
|
20
20
|
import { FillMissingWordsInGridClient } from '../types/fill-missing-words-in-grid/FillMissingWordsInGridClient';
|
|
21
21
|
import { WordFillStructuredFormClient } from '../types/word-fill-structured-form/WordFillStructuredFormClient';
|
|
22
22
|
import { CrosswordPuzzleClient } from '../types/crossword-puzzle/CrosswordPuzzleClient';
|
|
23
|
+
import { FindWordsInMatrixClient } from '../types/find-words-in-matrix/FindWordsInMatrixClient';
|
|
23
24
|
import { parseFillBlankStemLine } from '../_shared/utils/parseFillBlankStem';
|
|
24
25
|
import { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
|
|
25
26
|
import { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
|
|
26
27
|
import { TrueFalseGroupViewer } from './TrueFalseGroupViewer';
|
|
28
|
+
import { TrueFalseCorrectGroupViewer } from './TrueFalseCorrectGroupViewer';
|
|
27
29
|
import { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
|
|
28
30
|
import { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
|
|
29
31
|
import { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
32
|
+
import { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
33
|
+
import { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
34
|
+
import { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|
|
30
35
|
export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false, userAnswer, }) {
|
|
31
36
|
const [selectedAnswer, setSelectedAnswer] = useState(userAnswer || null);
|
|
32
37
|
const [playCount, setPlayCount] = useState(0);
|
|
@@ -314,13 +319,43 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
314
319
|
}, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
315
320
|
};
|
|
316
321
|
const renderWriteAShortLetter = () => {
|
|
317
|
-
const
|
|
322
|
+
const content = (question.content || {});
|
|
323
|
+
const answer = (question.answer || {});
|
|
324
|
+
const mainInstruction = content?.instructions?.mainInstruction ||
|
|
325
|
+
content?.instruction ||
|
|
326
|
+
content?.prompt ||
|
|
327
|
+
answer?.instructions?.mainInstruction ||
|
|
328
|
+
answer?.instruction ||
|
|
329
|
+
(typeof content?.instructions === 'string' ? content.instructions : '') ||
|
|
330
|
+
(typeof content === 'string' ? content : '') ||
|
|
331
|
+
'';
|
|
332
|
+
const informations = (Array.isArray(content?.informations)
|
|
333
|
+
? content.informations
|
|
334
|
+
: Array.isArray(content?.requirements)
|
|
335
|
+
? content.requirements
|
|
336
|
+
: Array.isArray(answer?.informations)
|
|
337
|
+
? answer.informations
|
|
338
|
+
: []).map((info) => typeof info === 'string' ? info : (info.text || info.content || ''));
|
|
339
|
+
const guideImages = Array.isArray(content?.guideImages)
|
|
340
|
+
? content.guideImages
|
|
341
|
+
: Array.isArray(answer?.guideImages)
|
|
342
|
+
? answer.guideImages
|
|
343
|
+
: [];
|
|
344
|
+
const passage = content?.passage ?? answer?.passage;
|
|
345
|
+
const contexts = content?.contexts || answer?.contexts || [];
|
|
346
|
+
const minWords = content?.minWords ?? answer?.minWords ?? 20;
|
|
347
|
+
const maxWords = content?.maxWords ?? answer?.maxWords ?? null;
|
|
318
348
|
const questionData = {
|
|
319
|
-
contexts
|
|
320
|
-
informations
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
349
|
+
contexts,
|
|
350
|
+
informations,
|
|
351
|
+
guideImages,
|
|
352
|
+
passage,
|
|
353
|
+
minWords,
|
|
354
|
+
maxWords,
|
|
355
|
+
instructions: {
|
|
356
|
+
mainInstruction,
|
|
357
|
+
},
|
|
358
|
+
instruction: mainInstruction,
|
|
324
359
|
};
|
|
325
360
|
return (_jsx(WriteAShortLetterClient, { questionData: questionData, onSubmit: (answerText) => {
|
|
326
361
|
setSelectedAnswer(answerText);
|
|
@@ -595,16 +630,24 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
595
630
|
case 'CHOOSE_THE_CORRECT_ANSWER':
|
|
596
631
|
return renderChooseTheCorrectAnswer();
|
|
597
632
|
case 'CHOOSE_THE_CORRECT_ANSWER_GROUP':
|
|
598
|
-
return (_jsx(ChooseTheCorrectAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
633
|
+
return (_jsx(ChooseTheCorrectAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
599
634
|
case 'TRUE_FALSE_GROUP':
|
|
600
635
|
return (_jsx(TrueFalseGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
636
|
+
case 'TRUE_FALSE_CORRECT_GROUP':
|
|
637
|
+
return (_jsx(TrueFalseCorrectGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
601
638
|
case 'CROSS_OUT_WORD_GROUP':
|
|
602
639
|
return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode }));
|
|
640
|
+
case 'ANSWER_THE_QUESTION_GROUP':
|
|
641
|
+
return (_jsx(AnswerTheQuestionGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
603
642
|
case 'SPONTANEOUS_QA_GROUP':
|
|
604
643
|
return (_jsx(SpontaneousQaGroupViewer, { question: question, isReviewMode: isReviewMode }));
|
|
605
644
|
case 'WORD_ORDER_AND_MATCH_GROUP':
|
|
606
645
|
case 'WORD_ORDER_AND_MATCH':
|
|
607
646
|
return (_jsx(WordOrderAndMatchGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
647
|
+
case 'MATCH_WORD_TO_PICTURE_GROUP':
|
|
648
|
+
return (_jsx(MatchWordToPictureGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
649
|
+
case 'MATCH_WORD_TO_PICTURE':
|
|
650
|
+
return (_jsx(MatchWordToPictureViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
608
651
|
case 'FILL_IN_BLANK':
|
|
609
652
|
return renderFillInBlank();
|
|
610
653
|
case 'TRUE_FALSE':
|
|
@@ -646,6 +689,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
646
689
|
return (_jsx(WordFillStructuredFormViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
647
690
|
case 'CROSSWORD_PUZZLE':
|
|
648
691
|
return (_jsx(CrosswordPuzzleClient, { content: question.content, correctAnswer: question.answer, isReviewMode: isReviewMode, userAnswers: userAnswer }));
|
|
692
|
+
case 'FIND_WORDS_IN_MATRIX':
|
|
693
|
+
return (_jsx(FindWordsInMatrixClient, { content: question.content, correctAnswer: question.answer, showSolution: isReviewMode }));
|
|
649
694
|
default:
|
|
650
695
|
return _jsxs("div", { children: ["Lo\u1EA1i c\u00E2u h\u1ECFi kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3: ", question.type] });
|
|
651
696
|
}
|
|
@@ -657,7 +702,9 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
657
702
|
*/
|
|
658
703
|
const resolveCardTitle = () => {
|
|
659
704
|
// WFSF and Crossword show title/instruction inside the client — no outer card label.
|
|
660
|
-
if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
|
|
705
|
+
if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
|
|
706
|
+
question.type === 'CROSSWORD_PUZZLE' ||
|
|
707
|
+
question.type === 'MATCH_WORD_TO_PICTURE') {
|
|
661
708
|
return null;
|
|
662
709
|
}
|
|
663
710
|
// Modern fill stems live entirely in the body; title is a short label.
|
|
@@ -680,7 +727,9 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
680
727
|
};
|
|
681
728
|
const cardTitle = resolveCardTitle();
|
|
682
729
|
// WFSF and Crossword already have their own bordered shell — avoid double Card border.
|
|
683
|
-
if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
|
|
730
|
+
if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
|
|
731
|
+
question.type === 'CROSSWORD_PUZZLE' ||
|
|
732
|
+
question.type === 'MATCH_WORD_TO_PICTURE') {
|
|
684
733
|
return (_jsx("div", { className: "space-y-4", children: renderQuestion() }));
|
|
685
734
|
}
|
|
686
735
|
return (_jsxs(Card, { children: [cardTitle ? (_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center justify-between", children: [_jsx("span", { className: "min-w-0 pr-2 text-base font-semibold leading-snug", children: cardTitle }), _jsxs("span", { className: "shrink-0 text-sm font-normal text-gray-500", children: [question.points, " \u0111i\u1EC3m"] })] }) })) : null, _jsx(CardContent, { className: cardTitle ? 'space-y-4' : 'space-y-4 pt-4', children: renderQuestion() })] }));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Question } from '../../../shared/types/entities/question.types';
|
|
2
|
+
import type { TrueFalseCorrectStudentItemAnswer } from '../_shared/types/true-false-correct-group.type';
|
|
3
|
+
type TrueFalseCorrectGroupViewerProps = {
|
|
4
|
+
question: Question;
|
|
5
|
+
isReviewMode?: boolean;
|
|
6
|
+
userAnswer?: TrueFalseCorrectStudentItemAnswer[];
|
|
7
|
+
};
|
|
8
|
+
export declare function TrueFalseCorrectGroupViewer({ question, isReviewMode, userAnswer, }: TrueFalseCorrectGroupViewerProps): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { TrueFalseCorrectGroupClient } from '../types/true-false-correct-group/TrueFalseCorrectGroupClient';
|
|
4
|
+
import { mapQuestionToTrueFalseCorrectGroupData } from '../types/true-false-correct-group/map-true-false-correct-group-data';
|
|
5
|
+
export function TrueFalseCorrectGroupViewer({ question, isReviewMode = false, userAnswer, }) {
|
|
6
|
+
const questionData = mapQuestionToTrueFalseCorrectGroupData(question);
|
|
7
|
+
const reviewAnswers = isReviewMode && !userAnswer
|
|
8
|
+
? questionData.items.map((item) => ({
|
|
9
|
+
isTrue: item.isTrue,
|
|
10
|
+
correction: item.correction,
|
|
11
|
+
}))
|
|
12
|
+
: userAnswer;
|
|
13
|
+
return (_jsx(TrueFalseCorrectGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers }));
|
|
14
|
+
}
|
|
@@ -3,6 +3,10 @@ export { QuestionGroupViewer } from './QuestionGroupViewer';
|
|
|
3
3
|
export { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
|
|
4
4
|
export { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
|
|
5
5
|
export { TrueFalseGroupViewer } from './TrueFalseGroupViewer';
|
|
6
|
+
export { TrueFalseCorrectGroupViewer } from './TrueFalseCorrectGroupViewer';
|
|
6
7
|
export { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
|
|
7
8
|
export { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
|
|
8
9
|
export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
10
|
+
export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
11
|
+
export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
12
|
+
export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|
|
@@ -3,6 +3,10 @@ export { QuestionGroupViewer } from './QuestionGroupViewer';
|
|
|
3
3
|
export { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
|
|
4
4
|
export { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
|
|
5
5
|
export { TrueFalseGroupViewer } from './TrueFalseGroupViewer';
|
|
6
|
+
export { TrueFalseCorrectGroupViewer } from './TrueFalseCorrectGroupViewer';
|
|
6
7
|
export { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
|
|
7
8
|
export { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
|
|
8
9
|
export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
10
|
+
export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
11
|
+
export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
12
|
+
export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|