@tinyweb_dev/oe-exam-sdk 0.2.12 → 0.2.14
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 +13 -3
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.d.ts +14 -0
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +32 -0
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.d.ts +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +10 -2
- 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/components/question-renderers/SortWordsIntoCategoriesRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.js +23 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/index.js +2 -0
- package/dist/components/exams/take/utils/answer-transformers.js +34 -1
- package/dist/components/exams/take/utils/question-transformers.d.ts +43 -4
- package/dist/components/exams/take/utils/question-transformers.js +71 -1
- package/dist/components/questions/_shared/config/question-types.config.js +12 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +7 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.js +19 -1
- package/dist/components/questions/_shared/types/answer-the-question.type.d.ts +3 -1
- package/dist/components/questions/_shared/types/answer-the-question.type.js +19 -1
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +47 -0
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.js +2 -0
- package/dist/components/questions/_shared/types/fill-in-blank-group.type.d.ts +14 -2
- package/dist/components/questions/_shared/types/index.d.ts +2 -0
- package/dist/components/questions/_shared/types/index.js +2 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +10 -1
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +19 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +9 -2
- package/dist/components/questions/_shared/types/sort-words-into-categories.type.d.ts +43 -0
- package/dist/components/questions/_shared/types/sort-words-into-categories.type.js +1 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +2 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +2 -0
- package/dist/components/questions/creator/question-type-registry.js +4 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +30 -6
- package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.js +56 -0
- package/dist/components/questions/types/answer-the-question/transform.js +9 -3
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +47 -3
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +8 -2
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +21 -2
- package/dist/components/questions/types/answer-the-question-group/transform.js +10 -1
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +2 -0
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +34 -0
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +174 -0
- package/dist/components/questions/types/choose-then-answer-group/index.d.ts +5 -0
- package/dist/components/questions/types/choose-then-answer-group/index.js +5 -0
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +11 -0
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +127 -0
- package/dist/components/questions/types/choose-then-answer-group/register.d.ts +3 -0
- package/dist/components/questions/types/choose-then-answer-group/register.js +37 -0
- package/dist/components/questions/types/choose-then-answer-group/transform.d.ts +2 -0
- package/dist/components/questions/types/choose-then-answer-group/transform.js +37 -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 +41 -12
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +23 -8
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +11 -2
- package/dist/components/questions/types/match-word-to-picture-group/transform.js +25 -9
- 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/sort-words-into-categories/SortWordsIntoCategoriesBoard.d.ts +12 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.js +74 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.d.ts +2 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +24 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.d.ts +2 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.js +171 -0
- package/dist/components/questions/types/sort-words-into-categories/index.d.ts +4 -0
- package/dist/components/questions/types/sort-words-into-categories/index.js +4 -0
- package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.d.ts +11 -0
- package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.js +153 -0
- package/dist/components/questions/types/sort-words-into-categories/register.d.ts +3 -0
- package/dist/components/questions/types/sort-words-into-categories/register.js +37 -0
- package/dist/components/questions/types/sort-words-into-categories/transform.d.ts +2 -0
- package/dist/components/questions/types/sort-words-into-categories/transform.js +33 -0
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +18 -0
- package/dist/components/questions/viewer/AnswerTheQuestionViewer.d.ts +9 -0
- package/dist/components/questions/viewer/AnswerTheQuestionViewer.js +8 -0
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +10 -0
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +39 -0
- package/dist/components/questions/viewer/QuestionViewer.js +13 -2
- package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.d.ts +10 -0
- package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.js +27 -0
- package/dist/components/questions/viewer/index.d.ts +3 -0
- package/dist/components/questions/viewer/index.js +3 -0
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +2 -1
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +12 -3
- package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
- package/dist/components/results/renderers/review-question-body-movers.js +24 -2
- package/dist/components/results/review-data.js +28 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +4 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +126 -6
- package/dist/shared/lib/utils/question-transform-types.d.ts +1 -1
- package/dist/shared/lib/utils/question-transform.js +4 -0
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/answer-the-question-group.d.ts +3 -0
- package/dist/shared/types/questions/choose-then-answer-group.d.ts +46 -0
- package/dist/shared/types/questions/choose-then-answer-group.js +1 -0
- package/dist/shared/types/questions/fill-in-blank-group.d.ts +9 -2
- package/dist/shared/types/questions/index.d.ts +2 -0
- package/dist/shared/types/questions/index.js +3 -0
- package/dist/shared/types/questions/match-word-to-picture-group.d.ts +7 -1
- package/dist/shared/types/questions/matching-with-lines-group.d.ts +7 -2
- package/dist/shared/types/questions/sort-words-into-categories.d.ts +37 -0
- package/dist/shared/types/questions/sort-words-into-categories.js +1 -0
- package/package.json +1 -1
|
@@ -19,16 +19,21 @@ function parsePointsValue(raw, allowZero = false) {
|
|
|
19
19
|
return Math.max(min, parsed);
|
|
20
20
|
}
|
|
21
21
|
import { Eye, Pencil, Plus, Star, Trash2 } from 'lucide-react';
|
|
22
|
+
import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
22
23
|
import { MatchWordToPictureGroupClient } from './MatchWordToPictureGroupClient';
|
|
23
24
|
import { MatchWordToPictureGroupImageField } from './MatchWordToPictureGroupImageField';
|
|
24
|
-
import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/match-word-to-picture-group.type';
|
|
25
|
+
import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION, fromMatchWordToPictureImageUrls, toMatchWordToPictureImageUrls, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
25
26
|
import { createEmptyMatchWordToPictureGroupItem, mapQuestionToMatchWordToPictureGroupData, } from './map-match-word-to-picture-group-data';
|
|
26
27
|
function sumPoints(items) {
|
|
27
28
|
return items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0);
|
|
28
29
|
}
|
|
29
30
|
export function MatchWordToPictureGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
|
|
30
31
|
const seeded = mapQuestionToMatchWordToPictureGroupData(initialData ? { answer: initialData } : null);
|
|
32
|
+
const [title, setTitle] = useState(initialData?.title || '');
|
|
31
33
|
const [instruction, setInstruction] = useState(initialData?.instruction || seeded.instruction || MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION);
|
|
34
|
+
const [passage, setPassage] = useState(initialData?.passage || seeded.passage || '');
|
|
35
|
+
const [audioUrl] = useState(initialData?.audioUrl || seeded.audioUrl || '');
|
|
36
|
+
const [imageUrl, setImageUrl] = useState(toMatchWordToPictureImageUrls(initialData?.imageUrl)[0] || '');
|
|
32
37
|
const [wordBank, setWordBank] = useState(initialData?.wordBank?.length ? initialData.wordBank : seeded.wordBank);
|
|
33
38
|
const [items, setItems] = useState(() => (initialData?.items?.length ? initialData.items : seeded.items));
|
|
34
39
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
@@ -39,8 +44,12 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
39
44
|
useEffect(() => {
|
|
40
45
|
onChangeRef.current = onChange;
|
|
41
46
|
}, [onChange]);
|
|
42
|
-
const buildPayload = (nextInstruction = instruction, nextWordBank = wordBank, nextItems = items, nextExplanation = explanation) => ({
|
|
47
|
+
const buildPayload = (nextTitle = title, nextInstruction = instruction, nextPassage = passage, nextImageUrl = imageUrl, nextWordBank = wordBank, nextItems = items, nextExplanation = explanation) => ({
|
|
48
|
+
...(nextTitle.trim() ? { title: nextTitle } : {}),
|
|
43
49
|
instruction: nextInstruction,
|
|
50
|
+
...(nextPassage.trim() ? { passage: nextPassage } : {}),
|
|
51
|
+
...(audioUrl.trim() ? { audioUrl: audioUrl.trim() } : {}),
|
|
52
|
+
...(nextImageUrl.trim() ? { imageUrl: nextImageUrl } : {}),
|
|
44
53
|
wordBank: nextWordBank,
|
|
45
54
|
items: nextItems,
|
|
46
55
|
explanation: nextExplanation,
|
|
@@ -57,7 +66,7 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
57
66
|
debouncedOnChange(payload);
|
|
58
67
|
onUnsavedChangesChange?.(true);
|
|
59
68
|
}
|
|
60
|
-
}, [instruction, wordBank, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
69
|
+
}, [title, instruction, passage, audioUrl, imageUrl, wordBank, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
61
70
|
const validate = () => {
|
|
62
71
|
const nextErrors = [];
|
|
63
72
|
if (!instruction.trim())
|
|
@@ -68,8 +77,9 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
68
77
|
if (items.length < 1)
|
|
69
78
|
nextErrors.push('Cần ít nhất một câu hỏi.');
|
|
70
79
|
items.forEach((item, index) => {
|
|
71
|
-
if (!
|
|
72
|
-
nextErrors.push(`Câu ${index + 1}: cần ảnh.`);
|
|
80
|
+
if (!imageUrl.trim() && toMatchWordToPictureImageUrls(item.imageUrl).length === 0) {
|
|
81
|
+
nextErrors.push(`Câu ${index + 1}: cần ảnh (hoặc ảnh chung).`);
|
|
82
|
+
}
|
|
73
83
|
if (!item.correctAnswer.trim())
|
|
74
84
|
nextErrors.push(`Câu ${index + 1}: cần chọn từ đúng.`);
|
|
75
85
|
});
|
|
@@ -83,13 +93,18 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
83
93
|
getFormData: () => buildPayload(),
|
|
84
94
|
};
|
|
85
95
|
}
|
|
86
|
-
}, [validationRef, instruction, wordBank, items, explanation]);
|
|
96
|
+
}, [validationRef, title, instruction, passage, audioUrl, imageUrl, wordBank, items, explanation]);
|
|
87
97
|
const updateItem = (index, patch) => {
|
|
88
98
|
setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item)));
|
|
89
99
|
};
|
|
90
100
|
const currentPayload = buildPayload();
|
|
91
101
|
const displayErrors = externalErrors && externalErrors.length > 0 ? externalErrors : errors;
|
|
92
|
-
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 t\u1EEB v\u1EDBi tranh (nh\u00F3m)" }), _jsx("p", { className: "text-sm text-gray-500", children: "Word bank d\u00F9ng chung
|
|
102
|
+
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 t\u1EEB v\u1EDBi tranh (nh\u00F3m)" }), _jsx("p", { className: "text-sm text-gray-500", children: "Word bank d\u00F9ng chung. C\u00F3 th\u1EC3 1 \u1EA3nh chung + \u00F4 s\u1ED1, ho\u1EB7c m\u1ED7i d\u00F2ng m\u1ED9t tranh." })] }), _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(MatchWordToPictureGroupClient, { questionData: currentPayload }) })] })) : (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "Word bank" }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: "mwtp-group-title", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "mwtp-group-title", className: "mt-1", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "VOCABULARY" })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "mwtp-group-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "mwtp-group-instruction", className: "mt-1", value: instruction, onChange: (e) => setInstruction(e.target.value) })] }), _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(MatchWordToPictureGroupImageField, { id: "mwtp-group-shared-image", label: "\u1EA2nh chung (optional \u2014 1 b\u1EE9c tranh nhi\u1EC1u \u00F4 s\u1ED1)", value: imageUrl, onChange: setImageUrl }), wordBank.map((word, index) => (_jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { value: word, onChange: (e) => setWordBank((prev) => prev.map((item, i) => (i === index ? e.target.value : item))), placeholder: `Từ ${index + 1}` }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setWordBank((prev) => prev.filter((_, i) => i !== index)), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, index))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setWordBank((prev) => [...prev, '']), children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), "Th\u00EAm t\u1EEB"] })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base font-semibold", children: ["Tranh (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setItems((prev) => [...prev, createEmptyMatchWordToPictureGroupItem(prev.length + 1)]), children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), "Th\u00EAm tranh"] })] }), _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, {
|
|
93
103
|
points: parsePointsValue(event.target.value, true),
|
|
94
|
-
}) }) })), 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" }) }))] })] }), _jsx(MatchWordToPictureGroupImageField, { id: `mwtp-group-image-${index}`, label: "\u1EA2nh", value: item.imageUrl
|
|
104
|
+
}) }) })), 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" }) }))] })] }), _jsx(MatchWordToPictureGroupImageField, { id: `mwtp-group-image-${index}`, label: "\u1EA2nh", value: toMatchWordToPictureImageUrls(item.imageUrl)[0] || '', onChange: (url) => updateItem(index, {
|
|
105
|
+
imageUrl: fromMatchWordToPictureImageUrls(url),
|
|
106
|
+
}) }), _jsxs("select", { className: "rounded-md border px-3 py-2 text-sm", value: item.correctAnswer, onChange: (e) => updateItem(index, { correctAnswer: e.target.value }), children: [_jsx("option", { value: "", children: "Ch\u1ECDn t\u1EEB \u0111\u00FAng" }), Array.from(new Set([
|
|
107
|
+
...wordBank.map((word) => word.trim()).filter(Boolean),
|
|
108
|
+
item.correctAnswer.trim(),
|
|
109
|
+
].filter(Boolean))).map((word) => (_jsx("option", { value: word, children: word }, word)))] })] }, 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 }) })] })] }))] }));
|
|
95
110
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
1
|
+
import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION, fromMatchWordToPictureImageUrls, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
2
2
|
function isRecord(value) {
|
|
3
3
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
4
|
}
|
|
@@ -30,7 +30,7 @@ export function mapMatchWordToPictureGroupItem(item, index, fallbackAnswer) {
|
|
|
30
30
|
const correctAnswer = asRecord(record.correctAnswer);
|
|
31
31
|
const isExample = record.isExample === true;
|
|
32
32
|
return {
|
|
33
|
-
imageUrl:
|
|
33
|
+
imageUrl: fromMatchWordToPictureImageUrls(content.imageUrl ?? record.imageUrl),
|
|
34
34
|
correctAnswer: asString(correctAnswer.answer) ||
|
|
35
35
|
asString(record.correctAnswer) ||
|
|
36
36
|
asString(fallbackAnswer),
|
|
@@ -57,10 +57,19 @@ export function mapQuestionToMatchWordToPictureGroupData(question) {
|
|
|
57
57
|
: asStringList(answer.wordBank).length
|
|
58
58
|
? asStringList(answer.wordBank)
|
|
59
59
|
: Array.from(new Set(items.map((item) => item.correctAnswer).filter(Boolean)));
|
|
60
|
+
const title = asString(meta.title) || asString(answer.title);
|
|
61
|
+
const passage = asString(meta.passage).trim() || asString(answer.passage).trim();
|
|
62
|
+
const audioUrl = asString(meta.audioUrl).trim() || asString(answer.audioUrl).trim();
|
|
63
|
+
const imageUrl = fromMatchWordToPictureImageUrls(meta.imageUrl)
|
|
64
|
+
?? fromMatchWordToPictureImageUrls(answer.imageUrl);
|
|
60
65
|
return {
|
|
66
|
+
...(title ? { title } : {}),
|
|
61
67
|
instruction: asString(meta.instruction) ||
|
|
62
68
|
asString(answer.instruction) ||
|
|
63
69
|
MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION,
|
|
70
|
+
...(passage ? { passage } : {}),
|
|
71
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
72
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
64
73
|
wordBank,
|
|
65
74
|
items,
|
|
66
75
|
explanation,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION, MATCH_WORD_TO_PICTURE_ITEM_TYPE, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
1
|
+
import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION, MATCH_WORD_TO_PICTURE_ITEM_TYPE, fromMatchWordToPictureImageUrls, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
2
2
|
function sumGradablePoints(items) {
|
|
3
3
|
return items.reduce((total, item) => {
|
|
4
4
|
if (item.isExample)
|
|
@@ -12,18 +12,34 @@ export const transformMatchWordToPictureGroup = (question) => {
|
|
|
12
12
|
const wordBank = Array.isArray(answerData?.wordBank)
|
|
13
13
|
? answerData.wordBank.filter((word) => typeof word === 'string').map((word) => word.trim()).filter(Boolean)
|
|
14
14
|
: [];
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
const groupImageUrl = fromMatchWordToPictureImageUrls(answerData?.imageUrl);
|
|
16
|
+
const apiItems = items.map((item, index) => {
|
|
17
|
+
const itemImageUrl = fromMatchWordToPictureImageUrls(item.imageUrl);
|
|
18
|
+
return {
|
|
19
|
+
questionType: MATCH_WORD_TO_PICTURE_ITEM_TYPE,
|
|
20
|
+
...(item.isExample ? { isExample: true } : {}),
|
|
21
|
+
content: {
|
|
22
|
+
...(itemImageUrl ? { imageUrl: itemImageUrl } : {}),
|
|
23
|
+
},
|
|
24
|
+
correctAnswer: { answer: item.correctAnswer || '' },
|
|
25
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
26
|
+
questionNumber: item.questionNumber || index + 1,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
23
29
|
return {
|
|
24
30
|
apiContent: {
|
|
25
31
|
meta: {
|
|
26
32
|
instruction: answerData?.instruction || MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION,
|
|
33
|
+
...(typeof answerData?.title === 'string' && answerData.title.trim()
|
|
34
|
+
? { title: answerData.title.trim() }
|
|
35
|
+
: {}),
|
|
36
|
+
...(typeof answerData?.passage === 'string' && answerData.passage.trim()
|
|
37
|
+
? { passage: answerData.passage }
|
|
38
|
+
: {}),
|
|
39
|
+
...(typeof answerData?.audioUrl === 'string' && answerData.audioUrl.trim()
|
|
40
|
+
? { audioUrl: answerData.audioUrl.trim() }
|
|
41
|
+
: {}),
|
|
42
|
+
...(groupImageUrl ? { imageUrl: groupImageUrl } : {}),
|
|
27
43
|
wordBank,
|
|
28
44
|
},
|
|
29
45
|
items: apiItems,
|
package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export interface MatchingWithLinesBoardLeftItem {
|
|
2
|
-
imageUrl
|
|
2
|
+
imageUrl?: string;
|
|
3
|
+
text?: string;
|
|
3
4
|
isExample?: boolean;
|
|
4
5
|
questionNumber?: number;
|
|
5
6
|
}
|
|
6
7
|
export interface MatchingWithLinesBoardRightItem {
|
|
7
8
|
id: string;
|
|
8
|
-
text
|
|
9
|
+
text?: string;
|
|
10
|
+
imageUrl?: string;
|
|
9
11
|
}
|
|
10
12
|
interface MatchingWithLinesBoardProps {
|
|
11
13
|
leftItems: MatchingWithLinesBoardLeftItem[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Star } from 'lucide-react';
|
|
5
5
|
import { cn } from '../../../../shared/lib/utils';
|
|
@@ -94,7 +94,7 @@ export function MatchingWithLinesBoard({ leftItems, rightItems, values, correctV
|
|
|
94
94
|
return;
|
|
95
95
|
assignPair(selectedLeft, rightId);
|
|
96
96
|
};
|
|
97
|
-
return (_jsxs("div", { ref: containerRef, className: "relative", children: [_jsx("svg", { className: "pointer-events-none absolute inset-0 h-full w-full", "aria-hidden": true, children: lines.map((line, index) => (_jsx("line", { x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2, stroke: line.color, strokeWidth: 3, strokeLinecap: "round" }, `${line.x1}-${line.y1}-${index}`))) }), _jsxs("div", { className: "flex justify-between gap-6 sm:gap-10", children: [_jsx("div", { className: "flex w-max flex-col gap-2", children: leftItems.map((item, index) => (_jsxs("button", { type: "button", onClick: () => handleLeftClick(index), disabled: locked || item.isExample, "aria-label": `Connect left item ${index + 1}`, "aria-pressed": selectedLeft === index, className: cn('box-border flex h-14 w-max items-center gap-2 rounded-lg border bg-white px-1.5 text-left shadow-sm', item.isExample ? 'border-amber-200' : 'border-gray-200', selectedLeft === index && 'ring-2 ring-teal-500', !locked && !item.isExample && 'cursor-pointer hover:border-teal-400'), children: [_jsxs("div", { className: "relative size-11 shrink-0 overflow-hidden rounded-md bg-gray-50", children: [_jsx(ResolvedImage, { src: item.imageUrl, alt: `Match item ${item.questionNumber || index + 1}`, className: "h-full w-full object-contain" }), item.isExample && (_jsxs("span", { className: "absolute left-0.5 top-0.5 inline-flex items-center gap-0.5 rounded-full bg-amber-100 px-1 py-0.5 text-[10px] font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Ex"] }))] }), _jsx("span", { ref: (node) => {
|
|
97
|
+
return (_jsxs("div", { ref: containerRef, className: "relative", children: [_jsx("svg", { className: "pointer-events-none absolute inset-0 h-full w-full", "aria-hidden": true, children: lines.map((line, index) => (_jsx("line", { x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2, stroke: line.color, strokeWidth: 3, strokeLinecap: "round" }, `${line.x1}-${line.y1}-${index}`))) }), _jsxs("div", { className: "flex justify-between gap-6 sm:gap-10", children: [_jsx("div", { className: "flex w-max flex-col gap-2", children: leftItems.map((item, index) => (_jsxs("button", { type: "button", onClick: () => handleLeftClick(index), disabled: locked || item.isExample, "aria-label": `Connect left item ${item.text || index + 1}`, "aria-pressed": selectedLeft === index, className: cn('box-border flex h-auto min-h-14 w-max items-center gap-2 rounded-lg border bg-white px-1.5 py-1 text-left shadow-sm', item.isExample ? 'border-amber-200' : 'border-gray-200', selectedLeft === index && 'ring-2 ring-teal-500', !locked && !item.isExample && 'cursor-pointer hover:border-teal-400'), children: [item.imageUrl ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative size-11 shrink-0 overflow-hidden rounded-md bg-gray-50", children: [_jsx(ResolvedImage, { src: item.imageUrl, alt: item.text || `Match item ${item.questionNumber || index + 1}`, className: "h-full w-full object-contain" }), item.isExample && (_jsxs("span", { className: "absolute left-0.5 top-0.5 inline-flex items-center gap-0.5 rounded-full bg-amber-100 px-1 py-0.5 text-[10px] font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Ex"] }))] }), item.text?.trim() ? (_jsx("span", { className: "whitespace-nowrap text-sm font-medium text-gray-800", children: item.text })) : null] })) : (_jsxs("span", { className: "relative whitespace-nowrap pl-1 text-sm font-medium text-gray-800", children: [item.isExample && (_jsxs("span", { className: "mr-1 inline-flex items-center gap-0.5 rounded-full bg-amber-100 px-1 py-0.5 text-[10px] font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Ex"] })), item.text || '—'] })), _jsx("span", { ref: (node) => {
|
|
98
98
|
leftDotRefs.current[index] = node;
|
|
99
99
|
}, className: cn('h-4 w-4 shrink-0 rounded-full border-2 bg-white', selectedLeft === index
|
|
100
100
|
? 'border-teal-600 bg-teal-500'
|
|
@@ -102,8 +102,8 @@ export function MatchingWithLinesBoard({ leftItems, rightItems, values, correctV
|
|
|
102
102
|
? 'border-teal-600'
|
|
103
103
|
: 'border-gray-400'), "aria-hidden": true })] }, `left-${index}`))) }), _jsx("div", { className: "flex w-max flex-col gap-2", children: rightItems.map((item, index) => {
|
|
104
104
|
const used = values.includes(item.id);
|
|
105
|
-
return (_jsxs("button", { type: "button", onClick: () => handleRightClick(item.id), disabled: locked, "aria-label": `Connect right item ${item.text || index + 1}`, className: cn('box-border flex h-14 w-
|
|
105
|
+
return (_jsxs("button", { type: "button", onClick: () => handleRightClick(item.id), disabled: locked, "aria-label": `Connect right item ${item.text || index + 1}`, className: cn('box-border flex h-auto min-h-14 w-max items-center gap-2 rounded-lg border border-gray-200 bg-white px-2 py-1 text-left shadow-sm', selectedLeft !== null && !locked && 'cursor-pointer hover:border-teal-400', used && 'border-teal-200'), children: [_jsx("span", { ref: (node) => {
|
|
106
106
|
rightDotRefs.current[index] = node;
|
|
107
|
-
}, className: cn('h-4 w-4 shrink-0 rounded-full border-2 bg-white', used || selectedLeft !== null ? 'border-teal-600' : 'border-gray-400', selectedLeft !== null && 'bg-teal-100'), "aria-hidden": true }), _jsx("span", { className: "whitespace-nowrap text-sm font-medium text-gray-800", children: item.text || '—' })] }, item.id || `right-${index}`));
|
|
107
|
+
}, className: cn('h-4 w-4 shrink-0 rounded-full border-2 bg-white', used || selectedLeft !== null ? 'border-teal-600' : 'border-gray-400', selectedLeft !== null && 'bg-teal-100'), "aria-hidden": true }), item.imageUrl ? (_jsx("div", { className: "relative size-11 shrink-0 overflow-hidden rounded-md bg-gray-50", children: _jsx(ResolvedImage, { src: item.imageUrl, alt: item.text || `Match option ${index + 1}`, className: "h-full w-full object-contain" }) })) : (_jsx("span", { className: "whitespace-nowrap text-sm font-medium text-gray-800", children: item.text || '—' }))] }, item.id || `right-${index}`));
|
|
108
108
|
}) })] })] }));
|
|
109
109
|
}
|
package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Lightbulb } from 'lucide-react';
|
|
3
|
+
import { BookOpen, Lightbulb } from 'lucide-react';
|
|
4
|
+
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
5
|
+
import { ResolvedImage } from '../../../../components/common/ResolvedImage';
|
|
4
6
|
import { MatchingWithLinesBoard } from './MatchingWithLinesBoard';
|
|
7
|
+
function ClientAudio({ url }) {
|
|
8
|
+
const { previewUrl, isLoading } = usePresignedFileUrl(url);
|
|
9
|
+
const src = previewUrl ||
|
|
10
|
+
(typeof url === 'string' && (url.startsWith('http') || url.startsWith('blob:')) ? url : '');
|
|
11
|
+
if (!src && !isLoading)
|
|
12
|
+
return null;
|
|
13
|
+
return (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50/70 px-4 py-3", children: isLoading ? (_jsx("div", { className: "h-8 w-full animate-pulse rounded bg-indigo-100" })) : (_jsx("audio", { controls: true, src: src, className: "w-full", children: _jsx("track", { kind: "captions" }) })) }));
|
|
14
|
+
}
|
|
5
15
|
export function MatchingWithLinesGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, onAnswersChange, }) {
|
|
6
16
|
const items = questionData.items || [];
|
|
7
17
|
const rightItems = questionData.rightItems || [];
|
|
@@ -22,8 +32,9 @@ export function MatchingWithLinesGroupClient({ questionData, isReviewMode = fals
|
|
|
22
32
|
}
|
|
23
33
|
});
|
|
24
34
|
};
|
|
25
|
-
return (_jsxs("div", { className: "space-y-4", children: [(questionData.title || questionData.instruction) && (_jsxs("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3", children: [questionData.title && (_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-indigo-500", children: questionData.title })), questionData.instruction && (_jsx("p", { className: "text-sm font-medium text-indigo-800", children: questionData.instruction }))] })), _jsx(MatchingWithLinesBoard, { leftItems: items.map((item) => ({
|
|
35
|
+
return (_jsxs("div", { className: "space-y-4", children: [(questionData.title || questionData.instruction) && (_jsxs("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3", children: [questionData.title && (_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-indigo-500", children: questionData.title })), questionData.instruction && (_jsx("p", { className: "text-sm font-medium text-indigo-800", children: questionData.instruction }))] })), questionData.passage ? (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })) : null, questionData.audioUrl ? _jsx(ClientAudio, { url: questionData.audioUrl }) : null, questionData.imageUrl ? (_jsx("div", { className: "flex justify-center rounded-lg border border-slate-100 bg-slate-50 p-3", children: _jsx(ResolvedImage, { src: questionData.imageUrl, alt: "Match passage image", className: "max-h-44 w-auto rounded-md object-contain" }) })) : null, _jsx(MatchingWithLinesBoard, { leftItems: items.map((item) => ({
|
|
26
36
|
imageUrl: item.imageUrl,
|
|
37
|
+
text: item.text,
|
|
27
38
|
isExample: item.isExample,
|
|
28
39
|
questionNumber: item.questionNumber,
|
|
29
40
|
})), rightItems: rightItems, values: values, correctValues: isReviewMode ? correctValues : undefined, onChange: handleChange, isReviewMode: isReviewMode, disabled: isReviewMode || !canEdit }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
|
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/sort-words-into-categories/SortWordsIntoCategoriesBoard.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SortWordsCategory, SortWordsPlacement, SortWordsWordItem } from '../../_shared/types/sort-words-into-categories.type';
|
|
2
|
+
interface SortWordsIntoCategoriesBoardProps {
|
|
3
|
+
categories: SortWordsCategory[];
|
|
4
|
+
words: SortWordsWordItem[];
|
|
5
|
+
placements: SortWordsPlacement[];
|
|
6
|
+
correctPlacements?: SortWordsPlacement[];
|
|
7
|
+
onChange?: (next: SortWordsPlacement[]) => void;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
isReviewMode?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function SortWordsIntoCategoriesBoard({ categories, words, placements, correctPlacements, onChange, disabled, isReviewMode, }: SortWordsIntoCategoriesBoardProps): import("react").JSX.Element;
|
|
12
|
+
export {};
|