@tinyweb_dev/oe-exam-sdk 0.2.9 → 0.2.11
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 +7 -3
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.d.ts +13 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.js +30 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/utils/answer-transformers.js +8 -1
- package/dist/components/exams/take/utils/question-transformers.d.ts +16 -1
- package/dist/components/exams/take/utils/question-transformers.js +47 -2
- package/dist/components/questions/_shared/config/question-types.config.js +18 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/index.d.ts +1 -0
- package/dist/components/questions/_shared/types/index.js +1 -0
- package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.d.ts +56 -0
- package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.js +2 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +35 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +2 -0
- package/dist/components/questions/_shared/types/word-ordering-group.type.d.ts +34 -0
- package/dist/components/questions/_shared/types/word-ordering-group.type.js +2 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +3 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
- package/dist/components/questions/creator/question-type-registry.js +6 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +1 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +5 -3
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +16 -2
- package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
- package/dist/components/questions/types/fill-in-blank-group/transform.js +21 -2
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +74 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.js +222 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/index.d.ts +5 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/index.js +5 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.d.ts +8 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.js +199 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/register.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/register.js +36 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/transform.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/transform.js +66 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +65 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +95 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +6 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +16 -0
- package/dist/components/questions/types/match-word-to-picture-group/index.d.ts +5 -0
- package/dist/components/questions/types/match-word-to-picture-group/index.js +5 -0
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.d.ts +4 -0
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +77 -0
- package/dist/components/questions/types/match-word-to-picture-group/register.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/register.js +36 -0
- package/dist/components/questions/types/match-word-to-picture-group/transform.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/transform.js +36 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +25 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.js +121 -0
- package/dist/components/questions/types/word-ordering-group/index.d.ts +5 -0
- package/dist/components/questions/types/word-ordering-group/index.js +5 -0
- package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.d.ts +4 -0
- package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.js +74 -0
- package/dist/components/questions/types/word-ordering-group/register.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/register.js +36 -0
- package/dist/components/questions/types/word-ordering-group/transform.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/transform.js +33 -0
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +5 -5
- package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.d.ts +8 -0
- package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.js +49 -0
- package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +2 -2
- package/dist/components/questions/viewer/QuestionViewer.js +6 -0
- package/dist/components/questions/viewer/WordOrderingGroupViewer.d.ts +7 -0
- package/dist/components/questions/viewer/WordOrderingGroupViewer.js +29 -0
- package/dist/components/questions/viewer/index.d.ts +2 -0
- package/dist/components/questions/viewer/index.js +2 -0
- package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
- package/dist/components/results/renderers/review-question-body-movers.js +6 -2
- package/dist/components/results/review-data.js +13 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +6 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +111 -2
- package/dist/shared/lib/utils/question-transform.js +6 -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 +3 -0
- package/dist/shared/types/questions/index.js +6 -0
- package/dist/shared/types/questions/match-columns-to-make-sentences.d.ts +50 -0
- package/dist/shared/types/questions/match-columns-to-make-sentences.js +1 -0
- package/dist/shared/types/questions/match-word-to-picture-group.d.ts +31 -0
- package/dist/shared/types/questions/match-word-to-picture-group.js +1 -0
- package/dist/shared/types/questions/word-ordering-group.d.ts +28 -0
- package/dist/shared/types/questions/word-ordering-group.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { Button } from '../../../../components/ui/button';
|
|
5
|
+
import { Label } from '../../../../components/ui/label';
|
|
6
|
+
import { Input } from '../../../../components/ui/input';
|
|
7
|
+
import { PointsInput } from '../../../../components/ui/points-input';
|
|
8
|
+
import { Textarea } from '../../../../components/ui/textarea';
|
|
9
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
10
|
+
import { Switch } from '../../../../components/ui/switch';
|
|
11
|
+
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
12
|
+
function parsePointsValue(raw, allowZero = false) {
|
|
13
|
+
if (raw === '')
|
|
14
|
+
return allowZero ? 0 : 1;
|
|
15
|
+
const parsed = parseInt(raw.replace(/[^0-9]/g, ''), 10);
|
|
16
|
+
if (Number.isNaN(parsed))
|
|
17
|
+
return allowZero ? 0 : 1;
|
|
18
|
+
const min = allowZero ? 0 : 1;
|
|
19
|
+
return Math.max(min, parsed);
|
|
20
|
+
}
|
|
21
|
+
import { Eye, Pencil, Plus, Star, Trash2 } from 'lucide-react';
|
|
22
|
+
import { WordOrderingGroupClient } from './WordOrderingGroupClient';
|
|
23
|
+
import { WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/word-ordering-group.type';
|
|
24
|
+
import { createEmptyWordOrderingGroupItem, mapQuestionToWordOrderingGroupData, } from './map-word-ordering-group-data';
|
|
25
|
+
function withMinTokens(tokens, min = 2) {
|
|
26
|
+
if (tokens.length >= min) {
|
|
27
|
+
return tokens;
|
|
28
|
+
}
|
|
29
|
+
return [...tokens, ...Array.from({ length: min - tokens.length }, () => '')];
|
|
30
|
+
}
|
|
31
|
+
function setTokenAt(tokens, index, value) {
|
|
32
|
+
const next = tokens.length > index
|
|
33
|
+
? [...tokens]
|
|
34
|
+
: [...tokens, ...Array.from({ length: index - tokens.length + 1 }, () => '')];
|
|
35
|
+
next[index] = value;
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
function TokenBoxRow({ tokens, onChange, onAdd, onRemove, }) {
|
|
39
|
+
return (_jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-2", children: [tokens.map((token, index) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-gray-100 text-xs font-semibold text-gray-600", children: index + 1 }), _jsx(Input, { className: "h-9 w-28", value: token, onChange: (event) => onChange(index, event.target.value), placeholder: `Token ${index + 1}` }), tokens.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "h-8 w-8 text-red-500 hover:bg-red-50 hover:text-red-700", onClick: () => onRemove(index), children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, index))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: onAdd, children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), "Th\u00EAm"] })] }));
|
|
40
|
+
}
|
|
41
|
+
function sumPoints(items) {
|
|
42
|
+
return items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0);
|
|
43
|
+
}
|
|
44
|
+
export function WordOrderingGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
|
|
45
|
+
const seeded = mapQuestionToWordOrderingGroupData(initialData ? { answer: initialData } : null);
|
|
46
|
+
const [instruction, setInstruction] = useState(initialData?.instruction || seeded.instruction || WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION);
|
|
47
|
+
const [items, setItems] = useState(() => (initialData?.items?.length ? initialData.items : seeded.items));
|
|
48
|
+
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
49
|
+
const [isClientMode, setIsClientMode] = useState(false);
|
|
50
|
+
const [errors, setErrors] = useState([]);
|
|
51
|
+
const onChangeRef = useRef(onChange);
|
|
52
|
+
const previousPayloadRef = useRef('');
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
onChangeRef.current = onChange;
|
|
55
|
+
}, [onChange]);
|
|
56
|
+
const buildPayload = (nextInstruction = instruction, nextItems = items, nextExplanation = explanation) => ({
|
|
57
|
+
instruction: nextInstruction,
|
|
58
|
+
items: nextItems,
|
|
59
|
+
explanation: nextExplanation,
|
|
60
|
+
points: sumPoints(nextItems),
|
|
61
|
+
});
|
|
62
|
+
const debouncedOnChange = useDebouncedCallback((payload) => {
|
|
63
|
+
onChangeRef.current?.(payload);
|
|
64
|
+
}, 300);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const payload = buildPayload();
|
|
67
|
+
const serialized = JSON.stringify(payload);
|
|
68
|
+
if (serialized !== previousPayloadRef.current) {
|
|
69
|
+
previousPayloadRef.current = serialized;
|
|
70
|
+
debouncedOnChange(payload);
|
|
71
|
+
onUnsavedChangesChange?.(true);
|
|
72
|
+
}
|
|
73
|
+
}, [instruction, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
74
|
+
const validate = () => {
|
|
75
|
+
const nextErrors = [];
|
|
76
|
+
if (!instruction.trim())
|
|
77
|
+
nextErrors.push('Hướng dẫn không được để trống.');
|
|
78
|
+
if (items.length < 1)
|
|
79
|
+
nextErrors.push('Cần ít nhất một câu.');
|
|
80
|
+
items.forEach((item, index) => {
|
|
81
|
+
if (item.words.filter(Boolean).length < 2) {
|
|
82
|
+
nextErrors.push(`Câu ${index + 1}: cần ít nhất 2 token (đã xáo).`);
|
|
83
|
+
}
|
|
84
|
+
if (item.answer.filter(Boolean).length < 2) {
|
|
85
|
+
nextErrors.push(`Câu ${index + 1}: cần thứ tự đúng.`);
|
|
86
|
+
}
|
|
87
|
+
const normalizeSet = (tokens) => tokens.map((token) => token.trim().toLowerCase()).filter(Boolean).sort().join('\0');
|
|
88
|
+
if (item.words.filter(Boolean).length >= 2 &&
|
|
89
|
+
item.answer.filter(Boolean).length >= 2 &&
|
|
90
|
+
normalizeSet(item.words) !== normalizeSet(item.answer)) {
|
|
91
|
+
nextErrors.push(`Câu ${index + 1}: thứ tự đúng phải dùng cùng bộ token đã xáo.`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
setErrors(nextErrors);
|
|
95
|
+
return nextErrors.length === 0;
|
|
96
|
+
};
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (validationRef) {
|
|
99
|
+
validationRef.current = {
|
|
100
|
+
triggerValidation: async () => validate(),
|
|
101
|
+
getFormData: () => buildPayload(),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}, [validationRef, instruction, items, explanation]);
|
|
105
|
+
const updateItem = (index, patch) => {
|
|
106
|
+
setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item)));
|
|
107
|
+
};
|
|
108
|
+
const currentPayload = buildPayload();
|
|
109
|
+
const displayErrors = externalErrors && externalErrors.length > 0 ? externalErrors : errors;
|
|
110
|
+
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-medium text-gray-900", children: "T\u1EA1o c\u00E2u h\u1ECFi S\u1EAFp x\u1EBFp t\u1EEB (nh\u00F3m)" }), _jsx("p", { className: "text-sm text-gray-500", children: "M\u1ED7i d\u00F2ng m\u1ED9t c\u00E2u x\u00E1o tr\u1ED9n. M\u1ED7i token \u0111i\u1EC1n m\u1ED9t \u00F4." })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsClientMode((prev) => !prev), className: "gap-2", children: [isClientMode ? _jsx(Pencil, { className: "h-4 w-4" }) : _jsx(Eye, { className: "h-4 w-4" }), isClientMode ? 'Chế độ chỉnh sửa' : 'Xem trước học sinh'] })] }), displayErrors.length > 0 && (_jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-5", children: displayErrors.map((err, i) => _jsx("li", { children: err }, i)) }) })), isClientMode ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "Xem tr\u01B0\u1EDBc hi\u1EC3n th\u1ECB h\u1ECDc sinh" }) }), _jsx(CardContent, { children: _jsx(WordOrderingGroupClient, { questionData: currentPayload }) })] })) : (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "H\u01B0\u1EDBng d\u1EABn" }) }), _jsx(CardContent, { children: _jsx(Input, { value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION }) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base font-semibold", children: ["C\u00E2u h\u1ECFi (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setItems((prev) => [...prev, createEmptyWordOrderingGroupItem(prev.length + 1)]), children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), "Th\u00EAm c\u00E2u"] })] }), _jsx(CardContent, { className: "space-y-4", children: items.map((item, index) => (_jsxs("div", { className: "space-y-3 rounded-lg border border-gray-200 p-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-sm font-medium", children: ["C\u00E2u ", item.questionNumber || index + 1] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { className: "flex items-center gap-1 text-xs", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "Example"] }), !item.isExample && (_jsx("div", { className: "w-24", children: _jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
|
|
111
|
+
points: parsePointsValue(event.target.value, true),
|
|
112
|
+
}) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setItems((prev) => prev.filter((_, i) => i !== index).map((entry, i) => ({ ...entry, questionNumber: i + 1 }))), children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Token x\u00E1o tr\u1ED9n" }), _jsx(TokenBoxRow, { tokens: withMinTokens(item.words), onChange: (tokenIndex, value) => updateItem(index, {
|
|
113
|
+
words: setTokenAt(item.words, tokenIndex, value),
|
|
114
|
+
}), onAdd: () => updateItem(index, { words: [...item.words, ''] }), onRemove: (tokenIndex) => updateItem(index, {
|
|
115
|
+
words: item.words.filter((_, current) => current !== tokenIndex),
|
|
116
|
+
}) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Th\u1EE9 t\u1EF1 \u0111\u00FAng" }), _jsx(TokenBoxRow, { tokens: withMinTokens(item.answer), onChange: (tokenIndex, value) => updateItem(index, {
|
|
117
|
+
answer: setTokenAt(item.answer, tokenIndex, value),
|
|
118
|
+
}), onAdd: () => updateItem(index, { answer: [...item.answer, ''] }), onRemove: (tokenIndex) => updateItem(index, {
|
|
119
|
+
answer: item.answer.filter((_, current) => current !== tokenIndex),
|
|
120
|
+
}) })] })] }, index))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gi\u1EA3i th\u00EDch" }) }), _jsx(CardContent, { children: _jsx(Textarea, { value: explanation, onChange: (event) => setExplanation(event.target.value), rows: 3 }) })] })] }))] }));
|
|
121
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type WordOrderingGroupData, type WordOrderingGroupItemData } from '../../_shared/types/word-ordering-group.type';
|
|
2
|
+
export declare function createEmptyWordOrderingGroupItem(questionNumber: number): WordOrderingGroupItemData;
|
|
3
|
+
export declare function mapWordOrderingGroupItem(item: unknown, index: number, fallbackAnswer?: unknown): WordOrderingGroupItemData;
|
|
4
|
+
export declare function mapQuestionToWordOrderingGroupData(question: Record<string, unknown> | null | undefined): WordOrderingGroupData;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION, } from '../../_shared/types/word-ordering-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 asStringList(value) {
|
|
12
|
+
if (!Array.isArray(value))
|
|
13
|
+
return [];
|
|
14
|
+
return value
|
|
15
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
export function createEmptyWordOrderingGroupItem(questionNumber) {
|
|
19
|
+
return {
|
|
20
|
+
words: ['', ''],
|
|
21
|
+
answer: ['', ''],
|
|
22
|
+
isExample: false,
|
|
23
|
+
points: 1,
|
|
24
|
+
questionNumber,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function mapWordOrderingGroupItem(item, index, fallbackAnswer) {
|
|
28
|
+
const record = asRecord(item);
|
|
29
|
+
const content = asRecord(record.content);
|
|
30
|
+
const correctAnswer = asRecord(record.correctAnswer);
|
|
31
|
+
const isExample = record.isExample === true;
|
|
32
|
+
const words = asStringList(content.words).length
|
|
33
|
+
? asStringList(content.words)
|
|
34
|
+
: asStringList(record.words);
|
|
35
|
+
const answer = asStringList(correctAnswer.answer).length
|
|
36
|
+
? asStringList(correctAnswer.answer)
|
|
37
|
+
: asStringList(record.answer).length
|
|
38
|
+
? asStringList(record.answer)
|
|
39
|
+
: asStringList(fallbackAnswer);
|
|
40
|
+
return {
|
|
41
|
+
words,
|
|
42
|
+
answer,
|
|
43
|
+
isExample,
|
|
44
|
+
points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
|
|
45
|
+
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function mapQuestionToWordOrderingGroupData(question) {
|
|
49
|
+
const source = asRecord(question);
|
|
50
|
+
const content = asRecord(source.content);
|
|
51
|
+
const meta = asRecord(content.meta);
|
|
52
|
+
const answer = asRecord(source.answer);
|
|
53
|
+
const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
|
|
54
|
+
const explanation = asString(source.explanation) || asString(answer.explanation);
|
|
55
|
+
const fallbackAnswers = Array.isArray(correctAnswer.answer) ? correctAnswer.answer : [];
|
|
56
|
+
if (Array.isArray(content.items) || Array.isArray(answer.items)) {
|
|
57
|
+
const rawItems = Array.isArray(content.items) ? content.items : answer.items;
|
|
58
|
+
const items = rawItems.map((item, index) => mapWordOrderingGroupItem(item, index, fallbackAnswers[index]));
|
|
59
|
+
return {
|
|
60
|
+
instruction: asString(meta.instruction) ||
|
|
61
|
+
asString(answer.instruction) ||
|
|
62
|
+
WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION,
|
|
63
|
+
items,
|
|
64
|
+
explanation,
|
|
65
|
+
points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
instruction: WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION,
|
|
70
|
+
items: [createEmptyWordOrderingGroupItem(1)],
|
|
71
|
+
explanation,
|
|
72
|
+
points: 1,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { WordOrderingGroupCreator } from './WordOrderingGroupCreator';
|
|
2
|
+
import { mapQuestionToWordOrderingGroupData } from './map-word-ordering-group-data';
|
|
3
|
+
export const wordOrderingGroupRegistration = {
|
|
4
|
+
component: WordOrderingGroupCreator,
|
|
5
|
+
transformInitialData: (initialData) => mapQuestionToWordOrderingGroupData(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,33 @@
|
|
|
1
|
+
import { WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION, WORD_ORDERING_ITEM_TYPE, } from '../../_shared/types/word-ordering-group.type';
|
|
2
|
+
function tokens(value) {
|
|
3
|
+
if (!Array.isArray(value))
|
|
4
|
+
return [];
|
|
5
|
+
return value
|
|
6
|
+
.filter((item) => typeof item === 'string')
|
|
7
|
+
.map((item) => item.trim())
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
export const transformWordOrderingGroup = (question) => {
|
|
11
|
+
const answerData = question.answer;
|
|
12
|
+
const items = Array.isArray(answerData?.items) ? answerData.items : [];
|
|
13
|
+
const apiItems = items.map((item, index) => ({
|
|
14
|
+
questionType: WORD_ORDERING_ITEM_TYPE,
|
|
15
|
+
...(item.isExample ? { isExample: true } : {}),
|
|
16
|
+
content: { words: tokens(item.words) },
|
|
17
|
+
correctAnswer: { answer: tokens(item.answer) },
|
|
18
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
19
|
+
questionNumber: item.questionNumber || index + 1,
|
|
20
|
+
}));
|
|
21
|
+
return {
|
|
22
|
+
apiContent: {
|
|
23
|
+
meta: {
|
|
24
|
+
instruction: answerData?.instruction || WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION,
|
|
25
|
+
},
|
|
26
|
+
items: apiItems,
|
|
27
|
+
},
|
|
28
|
+
apiCorrectAnswer: {
|
|
29
|
+
answer: apiItems.map((item) => item.correctAnswer.answer),
|
|
30
|
+
},
|
|
31
|
+
totalPoints: items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0),
|
|
32
|
+
};
|
|
33
|
+
};
|
|
@@ -4,10 +4,10 @@ import { useState, useRef, useEffect } from 'react';
|
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
5
|
import { Label } from '../../../../components/ui/label';
|
|
6
6
|
import { Input } from '../../../../components/ui/input';
|
|
7
|
-
import { Textarea } from '../../../../components/ui/textarea';
|
|
8
7
|
import { PointsInput } from '../../../../components/ui/points-input';
|
|
9
8
|
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
10
9
|
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
10
|
+
import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
11
11
|
import { Pencil, Eye, Plus, Trash2, Lightbulb, Mail, ImageIcon, X } from 'lucide-react';
|
|
12
12
|
import { WriteAShortLetterClient } from './WriteAShortLetterClient';
|
|
13
13
|
import { useReactHookForm } from '../../../../shared/lib/hooks/useReactHookForm';
|
|
@@ -316,10 +316,10 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
|
|
|
316
316
|
setMaxWords(numValue < 1 ? 1 : numValue);
|
|
317
317
|
}
|
|
318
318
|
form.clearErrors('maxWords');
|
|
319
|
-
}, placeholder: "\u221E", className: `mt-1 ${errors.maxWords ? 'border-red-500 focus:ring-red-500' : ''}` }), errors.maxWords && (_jsx("p", { className: "mt-1 text-xs text-red-600", children: errors.maxWords }))] })] })] })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-medium", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
319
|
+
}, placeholder: "\u221E", className: `mt-1 ${errors.maxWords ? 'border-red-500 focus:ring-red-500' : ''}` }), errors.maxWords && (_jsx("p", { className: "mt-1 text-xs text-red-600", children: errors.maxWords }))] })] })] })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-medium", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx("div", { className: `mt-1.5 ${errors.instruction ? 'rounded-md ring-1 ring-red-500' : ''}`, children: _jsx(RichTextEditor, { value: instruction, onChange: (html) => {
|
|
320
|
+
setInstruction(html);
|
|
321
|
+
form.clearErrors('instruction');
|
|
322
|
+
}, minHeightClassName: "min-h-[120px]" }) }), errors.instruction && (_jsx("p", { className: "mt-1 text-sm text-red-600", children: errors.instruction }))] }), _jsxs("div", { className: "space-y-3 rounded-lg border border-blue-100 bg-blue-50/30 p-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-full bg-blue-100", children: _jsx("span", { className: "text-xs font-semibold text-blue-600", children: "i" }) }), _jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "Th\u00F4ng tin c\u1EA7n bao g\u1ED3m" })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addInformation, className: "h-8 gap-1.5 border-blue-200 bg-white text-blue-600 hover:bg-blue-50 hover:text-blue-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm"] })] }), errors.informations && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-3", children: _jsx("p", { className: "text-sm text-red-600", children: errors.informations }) })), _jsx("div", { className: "space-y-2", children: informations.map((info, index) => (_jsx("div", { className: "space-y-1", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600", children: index + 1 }), _jsx(Input, { value: info, onChange: (e) => updateInformation(index, e.target.value), placeholder: `Thông tin ${index + 1}...`, className: "flex-1 border-gray-200" }), informations.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeInformation(index), className: "h-8 w-8 flex-shrink-0 p-0 text-gray-400 hover:bg-red-50 hover:text-red-500", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }) }, index))) }), _jsx("p", { className: "text-xs text-gray-500 italic", children: "\uD83D\uDCA1 Li\u1EC7t k\u00EA c\u00E1c th\u00F4ng tin h\u1ECDc sinh c\u1EA7n \u0111\u1EC1 c\u1EADp trong b\u00E0i vi\u1EBFt" })] }), _jsxs("div", { className: "space-y-3 rounded-lg border border-violet-100 bg-violet-50/30 p-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-full bg-violet-100", children: _jsx(ImageIcon, { className: "h-3.5 w-3.5 text-violet-600" }) }), _jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "\u1EA2nh h\u01B0\u1EDBng d\u1EABn" }), _jsx("span", { className: "text-xs text-gray-400" })] }), guideImages.length > 0 && (_jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4", children: guideImages.map((imageKey, index) => (_jsx(GuideImageItem, { imageKey: imageKey, index: index, localPreviewUrl: guideImagePreviewUrls[imageKey], onRemove: removeGuideImage }, `${imageKey}-${index}`))) })), _jsx(FileUpload, { label: "Th\u00EAm \u1EA3nh h\u01B0\u1EDBng d\u1EABn", accept: "image/*", value: "", onChange: handleGuideImageUpload, onPresignedUrlChange: handleGuideImagePresignedUrl, autoUpload: true, prefix: "write-short-letter/guide", placeholder: "Ch\u1ECDn ho\u1EB7c k\u00E9o th\u1EA3 \u1EA3nh v\u00E0o \u0111\u00E2y", icon: _jsx(ImageIcon, { className: "h-4 w-4" }) }), _jsx("p", { className: "text-xs text-gray-500 italic", children: "\uD83D\uDCF7 Upload nhi\u1EC1u \u1EA3nh \u0111\u1EC3 h\u01B0\u1EDBng d\u1EABn h\u1ECDc sinh vi\u1EBFt b\u00E0i (v\u00ED d\u1EE5: m\u1EABu th\u01B0, h\u00ECnh \u1EA3nh minh h\u1ECDa)" })] })] })] })] }));
|
|
323
323
|
}
|
|
324
324
|
export function WriteAShortLetterCreator(props) {
|
|
325
325
|
return _jsx(WriteAShortLetterCreatorContent, { ...props });
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Question } from '../../../shared/types/entities/question.types';
|
|
2
|
+
import type { MatchColumnsStudentItemAnswer } from '../_shared/types/match-columns-to-make-sentences.type';
|
|
3
|
+
export declare function MatchColumnsToMakeSentencesViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
|
|
4
|
+
question: Question;
|
|
5
|
+
isReviewMode?: boolean;
|
|
6
|
+
userAnswer?: unknown;
|
|
7
|
+
onSubmitAnswer?: (answer: MatchColumnsStudentItemAnswer[]) => void;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { MatchColumnsToMakeSentencesClient } from '../types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient';
|
|
5
|
+
import { mapQuestionToMatchColumnsGroupData } from '../types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data';
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function parseAnswers(value, length) {
|
|
10
|
+
const source = Array.isArray(value)
|
|
11
|
+
? value
|
|
12
|
+
: isRecord(value) && Array.isArray(value.answer)
|
|
13
|
+
? value.answer
|
|
14
|
+
: null;
|
|
15
|
+
if (!source)
|
|
16
|
+
return undefined;
|
|
17
|
+
return Array.from({ length }, (_, index) => {
|
|
18
|
+
const raw = source[index];
|
|
19
|
+
if (!isRecord(raw))
|
|
20
|
+
return { fragmentIds: [], sentence: '' };
|
|
21
|
+
return {
|
|
22
|
+
fragmentIds: Array.isArray(raw.fragmentIds)
|
|
23
|
+
? raw.fragmentIds.filter((id) => typeof id === 'string')
|
|
24
|
+
: [],
|
|
25
|
+
sentence: typeof raw.sentence === 'string' ? raw.sentence : '',
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function MatchColumnsToMakeSentencesViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
30
|
+
const questionData = mapQuestionToMatchColumnsGroupData(question);
|
|
31
|
+
const itemCount = questionData.items.length;
|
|
32
|
+
const parsedUser = parseAnswers(userAnswer, itemCount);
|
|
33
|
+
const questionKey = `${question.id}:${itemCount}`;
|
|
34
|
+
const [cachedKey, setCachedKey] = useState(questionKey);
|
|
35
|
+
const [localAnswers, setLocalAnswers] = useState(() => parsedUser ?? questionData.items.map((item) => (item.isExample
|
|
36
|
+
? { fragmentIds: item.fragmentIds, sentence: item.sentence }
|
|
37
|
+
: { fragmentIds: [item.stemFragmentId], sentence: '' })));
|
|
38
|
+
if (cachedKey !== questionKey) {
|
|
39
|
+
setCachedKey(questionKey);
|
|
40
|
+
setLocalAnswers(parsedUser ?? questionData.items.map((item) => (item.isExample
|
|
41
|
+
? { fragmentIds: item.fragmentIds, sentence: item.sentence }
|
|
42
|
+
: { fragmentIds: [item.stemFragmentId], sentence: '' })));
|
|
43
|
+
}
|
|
44
|
+
const userAnswers = parsedUser ?? localAnswers;
|
|
45
|
+
return (_jsx(MatchColumnsToMakeSentencesClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: isReviewMode ? undefined : (next) => {
|
|
46
|
+
setLocalAnswers(next);
|
|
47
|
+
onSubmitAnswer?.(next);
|
|
48
|
+
} }));
|
|
49
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
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';
|
|
3
|
+
import { MatchWordToPictureGroupClient } from '../types/match-word-to-picture-group/MatchWordToPictureGroupClient';
|
|
4
|
+
import { mapQuestionToMatchWordToPictureGroupData } from '../types/match-word-to-picture-group/map-match-word-to-picture-group-data';
|
|
5
5
|
export function MatchWordToPictureGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
6
6
|
const questionData = mapQuestionToMatchWordToPictureGroupData(question);
|
|
7
7
|
const reviewAnswers = isReviewMode && !userAnswer
|
|
@@ -34,6 +34,8 @@ import { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
|
34
34
|
import { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
35
35
|
import { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
36
36
|
import { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|
|
37
|
+
import { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
|
|
38
|
+
import { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
|
|
37
39
|
export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false, userAnswer, }) {
|
|
38
40
|
const [selectedAnswer, setSelectedAnswer] = useState(userAnswer || null);
|
|
39
41
|
const [playCount, setPlayCount] = useState(0);
|
|
@@ -641,6 +643,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
641
643
|
return (_jsx(FillInBlankGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
642
644
|
case 'MATCHING_WITH_LINES_GROUP':
|
|
643
645
|
return (_jsx(MatchingWithLinesGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
646
|
+
case 'MATCH_COLUMNS_TO_MAKE_SENTENCES':
|
|
647
|
+
return (_jsx(MatchColumnsToMakeSentencesViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
644
648
|
case 'CROSS_OUT_WORD_GROUP':
|
|
645
649
|
return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode }));
|
|
646
650
|
case 'ANSWER_THE_QUESTION_GROUP':
|
|
@@ -650,6 +654,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
650
654
|
case 'WORD_ORDER_AND_MATCH_GROUP':
|
|
651
655
|
case 'WORD_ORDER_AND_MATCH':
|
|
652
656
|
return (_jsx(WordOrderAndMatchGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
657
|
+
case 'WORD_ORDERING_GROUP':
|
|
658
|
+
return (_jsx(WordOrderingGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
653
659
|
case 'MATCH_WORD_TO_PICTURE_GROUP':
|
|
654
660
|
return (_jsx(MatchWordToPictureGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
655
661
|
case 'MATCH_WORD_TO_PICTURE':
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Question } from '../../../shared/types/entities/question.types';
|
|
2
|
+
export declare function WordOrderingGroupViewer({ 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,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { WordOrderingGroupClient } from '../types/word-ordering-group/WordOrderingGroupClient';
|
|
5
|
+
import { mapQuestionToWordOrderingGroupData } from '../types/word-ordering-group/map-word-ordering-group-data';
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function parseAnswers(value, length) {
|
|
10
|
+
const source = Array.isArray(value)
|
|
11
|
+
? value
|
|
12
|
+
: isRecord(value) && Array.isArray(value.answer)
|
|
13
|
+
? value.answer
|
|
14
|
+
: null;
|
|
15
|
+
if (!source)
|
|
16
|
+
return undefined;
|
|
17
|
+
return Array.from({ length }, (_, index) => Array.isArray(source[index])
|
|
18
|
+
? source[index].filter((token) => typeof token === 'string')
|
|
19
|
+
: []);
|
|
20
|
+
}
|
|
21
|
+
export function WordOrderingGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
22
|
+
const questionData = mapQuestionToWordOrderingGroupData(question);
|
|
23
|
+
const parsedUser = parseAnswers(userAnswer, questionData.items.length);
|
|
24
|
+
const [localAnswers, setLocalAnswers] = useState(() => parsedUser ?? questionData.items.map((item) => (item.isExample ? item.answer : [])));
|
|
25
|
+
return (_jsx(WordOrderingGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: parsedUser ?? localAnswers, onAnswerChange: isReviewMode ? undefined : (next) => {
|
|
26
|
+
setLocalAnswers(next);
|
|
27
|
+
onSubmitAnswer?.(next);
|
|
28
|
+
} }));
|
|
29
|
+
}
|
|
@@ -12,3 +12,5 @@ export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
|
12
12
|
export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
13
13
|
export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
14
14
|
export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|
|
15
|
+
export { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
|
|
16
|
+
export { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
|
|
@@ -12,3 +12,5 @@ export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
|
12
12
|
export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
13
13
|
export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
14
14
|
export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|
|
15
|
+
export { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
|
|
16
|
+
export { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
|
|
@@ -40,7 +40,7 @@ export function renderDedicatedReviewBody(ctx) {
|
|
|
40
40
|
}
|
|
41
41
|
case 'CHOOSE_THE_CORRECT_ANSWER_GROUP': {
|
|
42
42
|
const data = transformChooseTheCorrectAnswerGroup(apiQuestions);
|
|
43
|
-
return (_jsx(ReviewChooseBestAnswerRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl, questions: data.questions, answers: answers, isCorrectMap: isCorrectMap, correctOptionIdMap: correctOptionIdMap, example: data.example }));
|
|
43
|
+
return (_jsx(ReviewChooseBestAnswerRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl, imageUrl: data.imageUrl, questions: data.questions, answers: answers, isCorrectMap: isCorrectMap, correctOptionIdMap: correctOptionIdMap, example: data.example }));
|
|
44
44
|
}
|
|
45
45
|
case 'CHOOSE_CORRECT_ADJECTIVE': {
|
|
46
46
|
const data = transformChooseCorrectAdjective(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, 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';
|
|
7
|
+
import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, 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, transformWordOrderingGroupTake, 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;
|
|
@@ -189,6 +189,10 @@ export function renderMoversReviewBody(ctx) {
|
|
|
189
189
|
const data = transformMatchWordToPicture(apiQuestions);
|
|
190
190
|
return (_jsx(MoversMatchWordToPictureRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, wordBank: data.wordBank, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerTextMap: correctAnswerTextMap, isCorrectMap: isCorrectMap }));
|
|
191
191
|
}
|
|
192
|
+
case 'WORD_ORDERING_GROUP': {
|
|
193
|
+
const data = transformWordOrderingGroupTake(apiQuestions);
|
|
194
|
+
return (_jsx(MoversWordOrderingGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: data.questions.filter((q) => !q.isExample).length, data: data, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true }));
|
|
195
|
+
}
|
|
192
196
|
case 'WORD_ORDERING': {
|
|
193
197
|
const data = transformWordOrdering(apiQuestions);
|
|
194
198
|
return (_jsx(MoversWordOrderingRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, rawCorrectAnswerMap: rawCorrectAnswerMap, isCorrectMap: isCorrectMap }));
|
|
@@ -231,6 +231,19 @@ function transformStudentAnswer(questionId, questionType, value) {
|
|
|
231
231
|
return [`${questionId}-item-${index}`, { answers: [] }];
|
|
232
232
|
}));
|
|
233
233
|
}
|
|
234
|
+
if (questionType === 'WORD_ORDERING_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
|
+
Array.isArray(item)
|
|
243
|
+
? item.filter((token) => typeof token === 'string')
|
|
244
|
+
: [],
|
|
245
|
+
]));
|
|
246
|
+
}
|
|
234
247
|
if (questionType === 'MATCHING_WITH_LINES_GROUP') {
|
|
235
248
|
const answer = Array.isArray(record.answer)
|
|
236
249
|
? record.answer
|
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, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, 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, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, } 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, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, 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, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, } from './components/questions/viewer';
|
|
@@ -84,6 +84,9 @@ export const QUESTION_TYPE_SKILLS = {
|
|
|
84
84
|
'WORD_FILL_STRUCTURED_FORM': SkillEnum.READING,
|
|
85
85
|
'CROSSWORD_PUZZLE': SkillEnum.READING,
|
|
86
86
|
'FIND_WORDS_IN_MATRIX': SkillEnum.READING,
|
|
87
|
+
'MATCH_COLUMNS_TO_MAKE_SENTENCES': SkillEnum.READING,
|
|
88
|
+
'MATCH_WORD_TO_PICTURE_GROUP': SkillEnum.READING,
|
|
89
|
+
'WORD_ORDERING_GROUP': SkillEnum.READING,
|
|
87
90
|
// ========================================
|
|
88
91
|
// WRITING SKILLS (Viết)
|
|
89
92
|
// ========================================
|
|
@@ -216,6 +219,9 @@ export const QUESTION_TYPE_LABELS = {
|
|
|
216
219
|
'WORD_FILL_STRUCTURED_FORM': 'Điền từ vào biểu mẫu có cấu trúc',
|
|
217
220
|
'CROSSWORD_PUZZLE': 'Giải ô chữ (Crossword)',
|
|
218
221
|
'FIND_WORDS_IN_MATRIX': 'Tìm từ trong ma trận (Word Search)',
|
|
222
|
+
'MATCH_COLUMNS_TO_MAKE_SENTENCES': 'Ghép cột thành câu',
|
|
223
|
+
'MATCH_WORD_TO_PICTURE_GROUP': 'Nối từ với tranh (nhóm)',
|
|
224
|
+
'WORD_ORDERING_GROUP': 'Sắp xếp từ (nhóm)',
|
|
219
225
|
// ========================================
|
|
220
226
|
// WRITING SKILLS (Viết)
|
|
221
227
|
// ========================================
|