@tinyweb_dev/oe-exam-sdk 0.2.12 → 0.2.13
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/question-renderers/MoversFillInBlankGroupRenderer.d.ts +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -2
- package/dist/components/exams/take/utils/question-transformers.d.ts +1 -1
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.js +12 -1
- package/dist/components/questions/_shared/types/fill-in-blank-group.type.d.ts +14 -2
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +4 -1
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +9 -2
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +12 -1
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +6 -2
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +5 -0
- package/dist/components/questions/types/answer-the-question-group/transform.js +3 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +51 -16
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +52 -20
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +23 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +89 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +24 -6
- package/dist/components/questions/types/fill-in-blank-group/transform.js +18 -3
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +6 -6
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +15 -7
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +4 -0
- package/dist/components/questions/types/match-word-to-picture-group/transform.js +9 -1
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts +4 -2
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +4 -4
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +13 -2
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +25 -13
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +19 -2
- package/dist/components/questions/types/matching-with-lines-group/transform.js +29 -13
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +18 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +29 -5
- package/dist/shared/types/questions/fill-in-blank-group.d.ts +9 -2
- package/dist/shared/types/questions/match-word-to-picture-group.d.ts +3 -1
- package/dist/shared/types/questions/matching-with-lines-group.d.ts +7 -2
- package/package.json +1 -1
package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js
CHANGED
|
@@ -10,6 +10,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '../../../../components
|
|
|
10
10
|
import { Switch } from '../../../../components/ui/switch';
|
|
11
11
|
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
12
12
|
import { Eye, Pencil, Plus, Star, Trash2 } from 'lucide-react';
|
|
13
|
+
import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
13
14
|
import { MatchingWithLinesGroupClient } from './MatchingWithLinesGroupClient';
|
|
14
15
|
import { MatchingWithLinesImageField } from './MatchingWithLinesImageField';
|
|
15
16
|
import { createEmptyMatchingPair, nextRightId } from './map-matching-with-lines-group-data';
|
|
@@ -32,13 +33,15 @@ function normalizeRightItems(raw) {
|
|
|
32
33
|
}
|
|
33
34
|
return raw.map((item, index) => ({
|
|
34
35
|
id: item.id || `r${index + 1}`,
|
|
35
|
-
text: item.text
|
|
36
|
+
...(item.text?.trim() ? { text: item.text.trim() } : {}),
|
|
37
|
+
...(item.imageUrl?.trim() ? { imageUrl: item.imageUrl.trim() } : {}),
|
|
36
38
|
}));
|
|
37
39
|
}
|
|
38
40
|
function normalizeItems(rawItems, rightItems) {
|
|
39
41
|
if (!Array.isArray(rawItems) || rawItems.length === 0) {
|
|
40
42
|
return rightItems.map((right, index) => ({
|
|
41
43
|
imageUrl: '',
|
|
44
|
+
text: '',
|
|
42
45
|
correctAnswer: right.id,
|
|
43
46
|
isExample: false,
|
|
44
47
|
points: 1,
|
|
@@ -47,6 +50,7 @@ function normalizeItems(rawItems, rightItems) {
|
|
|
47
50
|
}
|
|
48
51
|
return rawItems.map((item, index) => ({
|
|
49
52
|
imageUrl: item.imageUrl || '',
|
|
53
|
+
text: item.text || '',
|
|
50
54
|
correctAnswer: item.correctAnswer || rightItems[index]?.id || '',
|
|
51
55
|
isExample: item.isExample === true,
|
|
52
56
|
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
@@ -61,6 +65,9 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
61
65
|
const [instruction, setInstruction] = useState(initialData?.instruction || MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION);
|
|
62
66
|
const [rightItems, setRightItems] = useState(() => normalizeRightItems(initialData?.rightItems));
|
|
63
67
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items, normalizeRightItems(initialData?.rightItems)));
|
|
68
|
+
const [passage, setPassage] = useState(initialData?.passage || '');
|
|
69
|
+
const [audioUrl] = useState(initialData?.audioUrl || '');
|
|
70
|
+
const [imageUrl, setImageUrl] = useState(initialData?.imageUrl || '');
|
|
64
71
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
65
72
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
66
73
|
const [errors, setErrors] = useState([]);
|
|
@@ -69,9 +76,12 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
69
76
|
useEffect(() => {
|
|
70
77
|
onChangeRef.current = onChange;
|
|
71
78
|
}, [onChange]);
|
|
72
|
-
const buildPayload = (nextTitle = title, nextInstruction = instruction, nextRightItems = rightItems, nextItems = items, nextExplanation = explanation) => ({
|
|
79
|
+
const buildPayload = (nextTitle = title, nextInstruction = instruction, nextPassage = passage, nextRightItems = rightItems, nextItems = items, nextImageUrl = imageUrl, nextExplanation = explanation) => ({
|
|
73
80
|
title: nextTitle,
|
|
74
81
|
instruction: nextInstruction,
|
|
82
|
+
...(nextPassage.trim() ? { passage: nextPassage } : {}),
|
|
83
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
84
|
+
...(nextImageUrl.trim() ? { imageUrl: nextImageUrl } : {}),
|
|
75
85
|
rightItems: nextRightItems,
|
|
76
86
|
items: nextItems,
|
|
77
87
|
explanation: nextExplanation,
|
|
@@ -88,7 +98,7 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
88
98
|
debouncedOnChange(payload);
|
|
89
99
|
onUnsavedChangesChange?.(true);
|
|
90
100
|
}
|
|
91
|
-
}, [title, instruction, rightItems, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
101
|
+
}, [title, instruction, passage, audioUrl, imageUrl, rightItems, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
92
102
|
const validate = () => {
|
|
93
103
|
const nextErrors = [];
|
|
94
104
|
if (!instruction.trim()) {
|
|
@@ -98,14 +108,15 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
98
108
|
nextErrors.push('Cần ít nhất 2 cặp để nối.');
|
|
99
109
|
}
|
|
100
110
|
if (items.length !== rightItems.length) {
|
|
101
|
-
nextErrors.push('Số
|
|
111
|
+
nextErrors.push('Số ô cột trái phải bằng số ô cột phải.');
|
|
102
112
|
}
|
|
103
113
|
const ids = new Set();
|
|
104
114
|
rightItems.forEach((item, index) => {
|
|
105
115
|
if (!item.id.trim())
|
|
106
116
|
nextErrors.push(`Từ ${index + 1}: id không được để trống.`);
|
|
107
|
-
if (!item.text.trim())
|
|
108
|
-
nextErrors.push(`
|
|
117
|
+
if (!item.text?.trim() && !item.imageUrl?.trim()) {
|
|
118
|
+
nextErrors.push(`Cột phải ${index + 1}: cần từ hoặc ảnh.`);
|
|
119
|
+
}
|
|
109
120
|
if (item.id.trim() && ids.has(item.id.trim())) {
|
|
110
121
|
nextErrors.push(`Từ ${index + 1}: id "${item.id}" bị trùng.`);
|
|
111
122
|
}
|
|
@@ -113,12 +124,13 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
113
124
|
});
|
|
114
125
|
const usedAnswers = new Set();
|
|
115
126
|
items.forEach((item, index) => {
|
|
116
|
-
if (!item.imageUrl.trim())
|
|
117
|
-
nextErrors.push(
|
|
127
|
+
if (!item.imageUrl?.trim() && !item.text?.trim()) {
|
|
128
|
+
nextErrors.push(`Cột trái ${index + 1}: cần ảnh hoặc chữ.`);
|
|
129
|
+
}
|
|
118
130
|
if (!item.correctAnswer.trim())
|
|
119
|
-
nextErrors.push(
|
|
131
|
+
nextErrors.push(`Cột trái ${index + 1}: chưa chọn đáp án.`);
|
|
120
132
|
if (item.correctAnswer && usedAnswers.has(item.correctAnswer)) {
|
|
121
|
-
nextErrors.push(
|
|
133
|
+
nextErrors.push(`Cột trái ${index + 1}: đáp án bị trùng (mỗi từ chỉ dùng 1 lần).`);
|
|
122
134
|
}
|
|
123
135
|
usedAnswers.add(item.correctAnswer);
|
|
124
136
|
});
|
|
@@ -132,7 +144,7 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
132
144
|
getFormData: () => buildPayload(),
|
|
133
145
|
};
|
|
134
146
|
}
|
|
135
|
-
}, [validationRef, title, instruction, rightItems, items, explanation]);
|
|
147
|
+
}, [validationRef, title, instruction, passage, audioUrl, imageUrl, rightItems, items, explanation]);
|
|
136
148
|
const updateItem = (index, patch) => {
|
|
137
149
|
setItems((prev) => {
|
|
138
150
|
const next = [...prev];
|
|
@@ -174,7 +186,7 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
174
186
|
};
|
|
175
187
|
const currentPayload = buildPayload();
|
|
176
188
|
const displayErrors = externalErrors && externalErrors.length > 0 ? externalErrors : errors;
|
|
177
|
-
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 N\u1ED1i b\u1EB1ng \u0111\u01B0\u1EDDng k\u1EBB (Nh\u00F3m)" }), _jsx("p", { className: "text-sm text-gray-500", children: "N\u1ED1i \u1EA3nh c\u1ED9t tr\u00E1i v\u1EDBi t\u1EEB c\u1ED9t ph\u1EA3i. M\u1ED7i t\u1EEB d\u00F9ng \u0111\u00FAng 1 l\u1EA7n." })] }), _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 && (_jsxs("div", { className: "rounded-lg border border-red-200 bg-red-50 p-4", children: [_jsx("h3", { className: "text-sm font-medium text-red-800", children: "C\u1EA7n s\u1EEDa c\u00E1c l\u1ED7i sau:" }), _jsx("ul", { className: "mt-2 list-disc space-y-1 pl-5 text-sm text-red-700", 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(MatchingWithLinesGroupClient, { questionData: currentPayload, userAnswers: currentPayload.items.map((item) => item.correctAnswer) }) })] })) : (_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 chung" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: "mwl-title", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "mwl-title", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "VOCABULARY", className: "mt-1" })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "mwl-instruction", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "mwl-instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION, className: "mt-1" })] })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base font-semibold", children: ["C\u1EB7p n\u1ED1i (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addPair, className: "gap-2", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm c\u1EB7p"] })] }), _jsx(CardContent, { className: "space-y-4", children: items.map((item, index) => (_jsxs("div", { className: "space-y-4 rounded-lg border border-gray-200 bg-gray-50/50 p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u1EB7p ", index + 1] }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `mwl-example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { htmlFor: `mwl-example-${index}`, className: "flex cursor-pointer 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, {
|
|
189
|
+
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 N\u1ED1i b\u1EB1ng \u0111\u01B0\u1EDDng k\u1EBB (Nh\u00F3m)" }), _jsx("p", { className: "text-sm text-gray-500", children: "N\u1ED1i \u1EA3nh c\u1ED9t tr\u00E1i v\u1EDBi t\u1EEB c\u1ED9t ph\u1EA3i. M\u1ED7i t\u1EEB d\u00F9ng \u0111\u00FAng 1 l\u1EA7n." })] }), _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 && (_jsxs("div", { className: "rounded-lg border border-red-200 bg-red-50 p-4", children: [_jsx("h3", { className: "text-sm font-medium text-red-800", children: "C\u1EA7n s\u1EEDa c\u00E1c l\u1ED7i sau:" }), _jsx("ul", { className: "mt-2 list-disc space-y-1 pl-5 text-sm text-red-700", 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(MatchingWithLinesGroupClient, { questionData: currentPayload, userAnswers: currentPayload.items.map((item) => item.correctAnswer) }) })] })) : (_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 chung" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: "mwl-title", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "mwl-title", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "VOCABULARY", className: "mt-1" })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "mwl-instruction", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "mwl-instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION, className: "mt-1" })] }), _jsxs("div", { children: [_jsx(Label, { children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage)" }), _jsx("div", { className: "mt-1", children: _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" }) })] }), _jsx(MatchingWithLinesImageField, { id: "mwl-group-image", label: "\u1EA2nh chung (optional)", value: imageUrl, onChange: setImageUrl })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base font-semibold", children: ["C\u1EB7p n\u1ED1i (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addPair, className: "gap-2", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm c\u1EB7p"] })] }), _jsx(CardContent, { className: "space-y-4", children: items.map((item, index) => (_jsxs("div", { className: "space-y-4 rounded-lg border border-gray-200 bg-gray-50/50 p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u1EB7p ", index + 1] }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `mwl-example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { htmlFor: `mwl-example-${index}`, className: "flex cursor-pointer 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, {
|
|
178
190
|
points: parsePointsValue(event.target.value, true),
|
|
179
|
-
}) }) })), items.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removePair(index), className: "text-red-500 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] })] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(MatchingWithLinesImageField, { id: `mwl-image-${index}`, label: "\u1EA2nh c\u1ED9t tr\u00E1i", value: item.imageUrl, onChange: (url) => updateItem(index, { imageUrl: url }) }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-right-text-${index}`, children: "T\u1EEB c\u1ED9t ph\u1EA3i" }), _jsx(Input, { id: `mwl-right-text-${index}`, value: rightItems[index]?.text || '', onChange: (e) => updateRightItem(index, { text: e.target.value }), placeholder: "paintbrush", className: "mt-1 bg-white" })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-answer-${index}`, children: "\u0110\u00E1p \u00E1n \u0111\u00FAng (id t\u1EEB)" }), _jsxs("select", { id: `mwl-answer-${index}`, value: item.correctAnswer, onChange: (e) => updateItem(index, { correctAnswer: e.target.value }), className: "mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm", children: [_jsx("option", { value: "", children: "Ch\u1ECDn t\u1EEB \u0111\u00FAng" }), rightItems.map((right) => (_jsxs("option", { value: right.id, children: [right.text || right.id, " (", right.id, ")"] }, right.id)))] })] })] })] })] }, `${rightItems[index]?.id || index}`))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gi\u1EA3i th\u00EDch" }) }), _jsx(CardContent, { children: _jsx(Textarea, { id: "explanation", value: explanation, onChange: (event) => setExplanation(event.target.value), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", rows: 3 }) })] })] }))] }));
|
|
191
|
+
}) }) })), items.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removePair(index), className: "text-red-500 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] })] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsxs("div", { className: "space-y-3", children: [_jsx(MatchingWithLinesImageField, { id: `mwl-image-${index}`, label: "\u1EA2nh c\u1ED9t tr\u00E1i", value: item.imageUrl || '', onChange: (url) => updateItem(index, { imageUrl: url }) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-left-text-${index}`, children: "Ch\u1EEF c\u1ED9t tr\u00E1i (text-to-text)" }), _jsx(Input, { id: `mwl-left-text-${index}`, value: item.text || '', onChange: (e) => updateItem(index, { text: e.target.value }), placeholder: "ball", className: "mt-1 bg-white" })] })] }), _jsxs("div", { className: "space-y-3", children: [_jsx(MatchingWithLinesImageField, { id: `mwl-right-image-${index}`, label: "\u1EA2nh c\u1ED9t ph\u1EA3i (image-to-image)", value: rightItems[index]?.imageUrl || '', onChange: (url) => updateRightItem(index, { imageUrl: url || undefined }) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-right-text-${index}`, children: "T\u1EEB c\u1ED9t ph\u1EA3i" }), _jsx(Input, { id: `mwl-right-text-${index}`, value: rightItems[index]?.text || '', onChange: (e) => updateRightItem(index, { text: e.target.value }), placeholder: "paintbrush", className: "mt-1 bg-white" })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-answer-${index}`, children: "\u0110\u00E1p \u00E1n \u0111\u00FAng (id t\u1EEB)" }), _jsxs("select", { id: `mwl-answer-${index}`, value: item.correctAnswer, onChange: (e) => updateItem(index, { correctAnswer: e.target.value }), className: "mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm", children: [_jsx("option", { value: "", children: "Ch\u1ECDn t\u1EEB \u0111\u00FAng" }), rightItems.map((right) => (_jsxs("option", { value: right.id, children: [right.text || right.id, " (", right.id, ")"] }, right.id)))] })] })] })] })] }, `${rightItems[index]?.id || index}`))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gi\u1EA3i th\u00EDch" }) }), _jsx(CardContent, { children: _jsx(Textarea, { id: "explanation", value: explanation, onChange: (event) => setExplanation(event.target.value), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", rows: 3 }) })] })] }))] }));
|
|
180
192
|
}
|
|
@@ -17,9 +17,10 @@ export function nextRightId(existing) {
|
|
|
17
17
|
}
|
|
18
18
|
export function createEmptyMatchingPair(questionNumber, rightId) {
|
|
19
19
|
return {
|
|
20
|
-
rightItem: { id: rightId
|
|
20
|
+
rightItem: { id: rightId },
|
|
21
21
|
item: {
|
|
22
22
|
imageUrl: '',
|
|
23
|
+
text: '',
|
|
23
24
|
correctAnswer: rightId,
|
|
24
25
|
isExample: false,
|
|
25
26
|
points: 1,
|
|
@@ -36,6 +37,7 @@ export function mapMatchingWithLinesGroupItem(item, index, fallbackAnswer) {
|
|
|
36
37
|
(typeof fallbackAnswer === 'string' ? fallbackAnswer : '');
|
|
37
38
|
return {
|
|
38
39
|
imageUrl: asString(content.imageUrl) || asString(record.imageUrl),
|
|
40
|
+
text: asString(content.text) || asString(record.text),
|
|
39
41
|
correctAnswer: answer,
|
|
40
42
|
isExample,
|
|
41
43
|
points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
|
|
@@ -47,9 +49,12 @@ function mapRightItems(raw) {
|
|
|
47
49
|
return [];
|
|
48
50
|
return raw.map((item, index) => {
|
|
49
51
|
const record = asRecord(item);
|
|
52
|
+
const imageUrl = asString(record.imageUrl).trim();
|
|
53
|
+
const text = asString(record.text).trim();
|
|
50
54
|
return {
|
|
51
55
|
id: asString(record.id, `r${index + 1}`),
|
|
52
|
-
text:
|
|
56
|
+
...(text ? { text } : {}),
|
|
57
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
53
58
|
};
|
|
54
59
|
});
|
|
55
60
|
}
|
|
@@ -67,11 +72,17 @@ export function mapQuestionToMatchingWithLinesGroupData(question) {
|
|
|
67
72
|
const meta = asRecord(content.meta ?? answer.meta);
|
|
68
73
|
if (Array.isArray(content.items)) {
|
|
69
74
|
const items = content.items.map((item, index) => mapMatchingWithLinesGroupItem(item, index, fallbackAnswers[index]));
|
|
75
|
+
const audioUrl = asString(meta.audioUrl).trim() || asString(answer.audioUrl).trim();
|
|
76
|
+
const passage = asString(meta.passage).trim() || asString(answer.passage).trim();
|
|
77
|
+
const imageUrl = asString(meta.imageUrl).trim() || asString(answer.imageUrl).trim();
|
|
70
78
|
return {
|
|
71
79
|
title: asString(meta.title) || asString(answer.title),
|
|
72
80
|
instruction: asString(meta.instruction) ||
|
|
73
81
|
asString(answer.instruction) ||
|
|
74
82
|
MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
|
|
83
|
+
...(passage ? { passage } : {}),
|
|
84
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
85
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
75
86
|
rightItems: mapRightItems(content.rightItems),
|
|
76
87
|
items,
|
|
77
88
|
explanation,
|
|
@@ -80,9 +91,15 @@ export function mapQuestionToMatchingWithLinesGroupData(question) {
|
|
|
80
91
|
}
|
|
81
92
|
if (Array.isArray(answer.items)) {
|
|
82
93
|
const items = answer.items.map((item, index) => mapMatchingWithLinesGroupItem(item, index, fallbackAnswers[index]));
|
|
94
|
+
const audioUrl = asString(answer.audioUrl).trim();
|
|
95
|
+
const passage = asString(answer.passage).trim();
|
|
96
|
+
const imageUrl = asString(answer.imageUrl).trim();
|
|
83
97
|
return {
|
|
84
98
|
title: asString(answer.title),
|
|
85
99
|
instruction: asString(answer.instruction) || MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
|
|
100
|
+
...(passage ? { passage } : {}),
|
|
101
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
102
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
86
103
|
rightItems: mapRightItems(answer.rightItems),
|
|
87
104
|
items,
|
|
88
105
|
explanation,
|
|
@@ -14,26 +14,42 @@ export const transformMatchingWithLinesGroup = (question) => {
|
|
|
14
14
|
if (items.length === 0) {
|
|
15
15
|
return { apiContent: { rightItems: [], items: [] }, apiCorrectAnswer: { answer: [] } };
|
|
16
16
|
}
|
|
17
|
-
const apiItems = items.map((item, index) =>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
const apiItems = items.map((item, index) => {
|
|
18
|
+
const imageUrl = item.imageUrl?.trim() || '';
|
|
19
|
+
const text = item.text?.trim() || '';
|
|
20
|
+
return {
|
|
21
|
+
...(item.isExample ? { isExample: true } : {}),
|
|
22
|
+
content: {
|
|
23
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
24
|
+
...(text ? { text } : {}),
|
|
25
|
+
},
|
|
26
|
+
correctAnswer: { answer: item.correctAnswer || '' },
|
|
27
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
28
|
+
questionNumber: item.questionNumber || index + 1,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
26
31
|
const title = typeof answerData?.title === 'string' ? answerData.title.trim() : '';
|
|
32
|
+
const passage = typeof answerData?.passage === 'string' ? answerData.passage.trim() : '';
|
|
33
|
+
const audioUrl = typeof answerData?.audioUrl === 'string' ? answerData.audioUrl.trim() : '';
|
|
34
|
+
const imageUrl = typeof answerData?.imageUrl === 'string' ? answerData.imageUrl.trim() : '';
|
|
27
35
|
return {
|
|
28
36
|
apiContent: {
|
|
29
37
|
meta: {
|
|
30
38
|
instruction: answerData?.instruction || MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
|
|
31
39
|
...(title ? { title } : {}),
|
|
40
|
+
...(passage ? { passage } : {}),
|
|
41
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
42
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
32
43
|
},
|
|
33
|
-
rightItems: rightItems.map((item) =>
|
|
34
|
-
|
|
35
|
-
text
|
|
36
|
-
|
|
44
|
+
rightItems: rightItems.map((item) => {
|
|
45
|
+
const imageUrl = item.imageUrl?.trim() || '';
|
|
46
|
+
const text = item.text?.trim() || '';
|
|
47
|
+
return {
|
|
48
|
+
id: item.id || '',
|
|
49
|
+
...(text ? { text } : {}),
|
|
50
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
51
|
+
};
|
|
52
|
+
}),
|
|
37
53
|
items: apiItems,
|
|
38
54
|
},
|
|
39
55
|
apiCorrectAnswer: {
|
package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js
CHANGED
|
@@ -35,6 +35,20 @@ function ClientImageItem({ url, alt }) {
|
|
|
35
35
|
return null;
|
|
36
36
|
return (_jsx("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-gray-50 max-w-sm", children: isLoading ? (_jsx("div", { className: "flex h-36 w-48 items-center justify-center", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-teal-500" }) })) : (_jsx("img", { src: src, alt: alt || 'Image', className: "max-h-56 w-auto object-contain p-2" })) }));
|
|
37
37
|
}
|
|
38
|
+
function WfsfHtmlImage({ src, alt, className, style, }) {
|
|
39
|
+
const { previewUrl, isLoading } = usePresignedFileUrl(src);
|
|
40
|
+
const resolved = previewUrl ||
|
|
41
|
+
(typeof src === 'string'
|
|
42
|
+
&& (src.startsWith('http') || src.startsWith('blob:') || src.startsWith('data:'))
|
|
43
|
+
? src
|
|
44
|
+
: '');
|
|
45
|
+
if (isLoading && !resolved) {
|
|
46
|
+
return _jsx("span", { className: "inline-block h-12 w-12 animate-pulse rounded bg-slate-100" });
|
|
47
|
+
}
|
|
48
|
+
if (!resolved)
|
|
49
|
+
return null;
|
|
50
|
+
return (_jsx("img", { src: resolved, alt: alt || '', className: className, style: style }));
|
|
51
|
+
}
|
|
38
52
|
function ClientAudioItem({ url }) {
|
|
39
53
|
const { previewUrl } = usePresignedFileUrl(url);
|
|
40
54
|
const src = previewUrl ||
|
|
@@ -214,6 +228,10 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
214
228
|
}
|
|
215
229
|
const element = node;
|
|
216
230
|
const tagName = element.tagName.toLowerCase();
|
|
231
|
+
if (tagName === 'img') {
|
|
232
|
+
const rawStyle = element.getAttribute('style');
|
|
233
|
+
return (_jsx(WfsfHtmlImage, { src: element.getAttribute('src') || '', alt: element.getAttribute('alt') || '', className: element.getAttribute('class') || undefined, style: rawStyle ? parseStyleAttribute(rawStyle) : undefined }, path));
|
|
234
|
+
}
|
|
217
235
|
const props = { key: path };
|
|
218
236
|
Array.from(element.attributes).forEach((attr) => {
|
|
219
237
|
if (attr.name === 'class') {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fromFillInBlankImageUrls, toFillInBlankImageUrls, } from '../../../components/questions/types/fill-in-blank-group/blank-utils';
|
|
1
2
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2
3
|
const asRecord = (value) => isRecord(value) ? value : {};
|
|
3
4
|
const asArray = (value) => Array.isArray(value) ? value : [];
|
|
@@ -309,11 +310,18 @@ const reverseAnswerTheQuestionGroup = (apiQuestion) => {
|
|
|
309
310
|
questionNumber: asNumber(item.questionNumber, index + 1),
|
|
310
311
|
};
|
|
311
312
|
});
|
|
313
|
+
const meta = asRecord(content.meta);
|
|
314
|
+
const imageUrl = asArray(meta.imageUrl)
|
|
315
|
+
.filter((value) => typeof value === 'string')
|
|
316
|
+
.map((value) => value.trim())
|
|
317
|
+
.filter((value) => value !== '');
|
|
318
|
+
const singleImage = asString(meta.imageUrl).trim();
|
|
312
319
|
return {
|
|
313
320
|
content: '',
|
|
314
321
|
answer: {
|
|
315
|
-
instruction: asString(
|
|
316
|
-
passage: asString(
|
|
322
|
+
instruction: asString(meta.instruction, 'Read the text again and answer the questions.'),
|
|
323
|
+
passage: asString(meta.passage),
|
|
324
|
+
...(imageUrl.length ? { imageUrl } : (singleImage ? { imageUrl: [singleImage] } : {})),
|
|
317
325
|
items,
|
|
318
326
|
explanation: asString(apiQuestion.explanation),
|
|
319
327
|
points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
|
|
@@ -402,27 +410,34 @@ const reverseFillInBlankGroup = (apiQuestion) => {
|
|
|
402
410
|
: [String(blank ?? '').trim()].filter(Boolean);
|
|
403
411
|
return alts.length > 0 ? alts : [''];
|
|
404
412
|
});
|
|
413
|
+
const wordBank = asArray(itemContent.wordBank);
|
|
405
414
|
return {
|
|
406
415
|
question,
|
|
407
416
|
answers: answers.length > 0 ? answers : [['']],
|
|
408
417
|
caseSensitive: itemAnswer.caseSensitive === true || fallback.caseSensitive === true,
|
|
409
|
-
imageUrl:
|
|
418
|
+
imageUrl: fromFillInBlankImageUrls(toFillInBlankImageUrls(itemContent.imageUrl)),
|
|
410
419
|
audioUrl: asString(itemContent.audioUrl),
|
|
411
420
|
showHints: itemContent.showHints === true,
|
|
412
421
|
hints: asArray(itemContent.hints).map((hint) => String(hint ?? '').trim()).filter(Boolean),
|
|
422
|
+
...(wordBank.length > 0 ? { wordBank } : {}),
|
|
413
423
|
isExample,
|
|
414
424
|
points: isExample ? 0 : asNumber(item.points, 1),
|
|
415
425
|
questionNumber: asNumber(item.questionNumber, index + 1),
|
|
416
426
|
};
|
|
417
427
|
});
|
|
428
|
+
const groupWordBank = asArray(meta.wordBank);
|
|
429
|
+
const viewMode = asString(meta.viewMode) === 'WITH_OPTIONS' ? 'WITH_OPTIONS' : undefined;
|
|
418
430
|
return {
|
|
419
431
|
content: '',
|
|
420
432
|
answer: {
|
|
421
433
|
instruction: asString(meta.instruction, 'Complete the sentences.'),
|
|
434
|
+
passage: asString(meta.passage),
|
|
422
435
|
audioUrl: asString(meta.audioUrl),
|
|
423
|
-
imageUrl:
|
|
436
|
+
imageUrl: fromFillInBlankImageUrls(toFillInBlankImageUrls(meta.imageUrl)),
|
|
424
437
|
showHints: meta.showHints === true,
|
|
425
438
|
hints: asArray(meta.hints).map((hint) => String(hint ?? '').trim()).filter(Boolean),
|
|
439
|
+
...(viewMode ? { viewMode } : {}),
|
|
440
|
+
...(groupWordBank.length > 0 ? { wordBank: groupWordBank } : {}),
|
|
426
441
|
items,
|
|
427
442
|
explanation: asString(apiQuestion.explanation),
|
|
428
443
|
points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
|
|
@@ -436,9 +451,12 @@ const reverseMatchingWithLinesGroup = (apiQuestion) => {
|
|
|
436
451
|
const meta = asRecord(content.meta);
|
|
437
452
|
const rightItems = asArray(content.rightItems).map((raw, index) => {
|
|
438
453
|
const item = asRecord(raw);
|
|
454
|
+
const imageUrl = asString(item.imageUrl).trim();
|
|
455
|
+
const text = asString(item.text).trim();
|
|
439
456
|
return {
|
|
440
457
|
id: asString(item.id, `r${index + 1}`),
|
|
441
|
-
text:
|
|
458
|
+
...(text ? { text } : {}),
|
|
459
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
442
460
|
};
|
|
443
461
|
});
|
|
444
462
|
const items = asArray(content.items).map((rawItem, index) => {
|
|
@@ -449,6 +467,7 @@ const reverseMatchingWithLinesGroup = (apiQuestion) => {
|
|
|
449
467
|
const isExample = item.isExample === true;
|
|
450
468
|
return {
|
|
451
469
|
imageUrl: asString(itemContent.imageUrl),
|
|
470
|
+
text: asString(itemContent.text),
|
|
452
471
|
correctAnswer: asString(itemAnswer.answer, typeof fallback === 'string' ? fallback : ''),
|
|
453
472
|
isExample,
|
|
454
473
|
points: isExample ? 0 : asNumber(item.points, 1),
|
|
@@ -460,6 +479,9 @@ const reverseMatchingWithLinesGroup = (apiQuestion) => {
|
|
|
460
479
|
answer: {
|
|
461
480
|
title: asString(meta.title),
|
|
462
481
|
instruction: asString(meta.instruction, 'Look and match.'),
|
|
482
|
+
...(asString(meta.passage).trim() ? { passage: asString(meta.passage).trim() } : {}),
|
|
483
|
+
...(asString(meta.audioUrl).trim() ? { audioUrl: asString(meta.audioUrl).trim() } : {}),
|
|
484
|
+
...(asString(meta.imageUrl).trim() ? { imageUrl: asString(meta.imageUrl).trim() } : {}),
|
|
463
485
|
rightItems,
|
|
464
486
|
items,
|
|
465
487
|
explanation: asString(apiQuestion.explanation),
|
|
@@ -669,7 +691,9 @@ const reverseMatchWordToPictureGroup = (apiQuestion) => {
|
|
|
669
691
|
return {
|
|
670
692
|
content: '',
|
|
671
693
|
answer: {
|
|
694
|
+
...(asString(meta.title).trim() ? { title: asString(meta.title).trim() } : {}),
|
|
672
695
|
instruction: asString(meta.instruction, 'Look and complete.'),
|
|
696
|
+
...(asString(meta.imageUrl).trim() ? { imageUrl: asString(meta.imageUrl).trim() } : {}),
|
|
673
697
|
wordBank: asArray(meta.wordBank).map((word) => asString(word)).filter(Boolean),
|
|
674
698
|
items: mappedItems,
|
|
675
699
|
explanation: asString(apiQuestion.explanation),
|
|
@@ -3,12 +3,16 @@ export interface FillInBlankGroupItemAnswer {
|
|
|
3
3
|
answers: string[][];
|
|
4
4
|
caseSensitive?: boolean;
|
|
5
5
|
}
|
|
6
|
+
export type FillInBlankWordBankEntry = {
|
|
7
|
+
options: string[];
|
|
8
|
+
} | null;
|
|
6
9
|
export interface FillInBlankGroupItemContent {
|
|
7
10
|
question: string;
|
|
8
|
-
imageUrl?: string;
|
|
11
|
+
imageUrl?: string | string[];
|
|
9
12
|
audioUrl?: string;
|
|
10
13
|
showHints?: boolean;
|
|
11
14
|
hints?: string[];
|
|
15
|
+
wordBank?: FillInBlankWordBankEntry[];
|
|
12
16
|
}
|
|
13
17
|
export interface FillInBlankGroupItem {
|
|
14
18
|
questionType?: typeof FILL_IN_BLANK_ITEM_TYPE;
|
|
@@ -21,10 +25,13 @@ export interface FillInBlankGroupItem {
|
|
|
21
25
|
export interface FillInBlankGroupContent {
|
|
22
26
|
meta?: {
|
|
23
27
|
instruction?: string;
|
|
28
|
+
passage?: string;
|
|
24
29
|
audioUrl?: string;
|
|
25
|
-
imageUrl?: string;
|
|
30
|
+
imageUrl?: string | string[];
|
|
26
31
|
showHints?: boolean;
|
|
27
32
|
hints?: string[];
|
|
33
|
+
viewMode?: 'WITH_OPTIONS';
|
|
34
|
+
wordBank?: FillInBlankWordBankEntry[];
|
|
28
35
|
};
|
|
29
36
|
items: FillInBlankGroupItem[];
|
|
30
37
|
}
|
|
@@ -3,7 +3,7 @@ export interface MatchWordToPictureGroupItem {
|
|
|
3
3
|
questionType?: typeof MATCH_WORD_TO_PICTURE_ITEM_TYPE;
|
|
4
4
|
isExample?: boolean;
|
|
5
5
|
content: {
|
|
6
|
-
imageUrl
|
|
6
|
+
imageUrl?: string;
|
|
7
7
|
};
|
|
8
8
|
correctAnswer?: {
|
|
9
9
|
answer: string;
|
|
@@ -13,7 +13,9 @@ export interface MatchWordToPictureGroupItem {
|
|
|
13
13
|
}
|
|
14
14
|
export interface MatchWordToPictureGroupContent {
|
|
15
15
|
meta?: {
|
|
16
|
+
title?: string;
|
|
16
17
|
instruction?: string;
|
|
18
|
+
imageUrl?: string;
|
|
17
19
|
wordBank?: string[];
|
|
18
20
|
};
|
|
19
21
|
items: MatchWordToPictureGroupItem[];
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export interface MatchingWithLinesRightItem {
|
|
2
2
|
id: string;
|
|
3
|
-
text
|
|
3
|
+
text?: string;
|
|
4
|
+
imageUrl?: string;
|
|
4
5
|
}
|
|
5
6
|
export interface MatchingWithLinesGroupItemContent {
|
|
6
|
-
imageUrl
|
|
7
|
+
imageUrl?: string;
|
|
8
|
+
text?: string;
|
|
7
9
|
}
|
|
8
10
|
export interface MatchingWithLinesGroupItem {
|
|
9
11
|
isExample?: boolean;
|
|
@@ -18,6 +20,9 @@ export interface MatchingWithLinesGroupContent {
|
|
|
18
20
|
meta?: {
|
|
19
21
|
instruction?: string;
|
|
20
22
|
title?: string;
|
|
23
|
+
audioUrl?: string;
|
|
24
|
+
passage?: string;
|
|
25
|
+
imageUrl?: string;
|
|
21
26
|
};
|
|
22
27
|
rightItems: MatchingWithLinesRightItem[];
|
|
23
28
|
items: MatchingWithLinesGroupItem[];
|