@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
|
@@ -75,6 +75,42 @@ export function splitStem(question) {
|
|
|
75
75
|
}
|
|
76
76
|
return parts;
|
|
77
77
|
}
|
|
78
|
+
const STEM_TICK_MARKS = new Set(['✓', '✔', '✅']);
|
|
79
|
+
const STEM_CROSS_MARKS = new Set(['✗', '✘', '❌', '×']);
|
|
80
|
+
const STEM_DECORATION_RE = /[✓✔✅✗✘❌×]|\n/g;
|
|
81
|
+
/** Split a stem text fragment into ticks, crosses, line breaks, and plain text. */
|
|
82
|
+
export function splitStemTextDecorations(text) {
|
|
83
|
+
if (!text)
|
|
84
|
+
return [];
|
|
85
|
+
const normalized = text.replace(/\r\n/g, '\n');
|
|
86
|
+
const parts = [];
|
|
87
|
+
let lastIndex = 0;
|
|
88
|
+
let match;
|
|
89
|
+
const regex = new RegExp(STEM_DECORATION_RE.source, 'g');
|
|
90
|
+
while ((match = regex.exec(normalized)) !== null) {
|
|
91
|
+
if (match.index > lastIndex) {
|
|
92
|
+
parts.push({ type: 'text', value: normalized.slice(lastIndex, match.index) });
|
|
93
|
+
}
|
|
94
|
+
const token = match[0];
|
|
95
|
+
if (token === '\n') {
|
|
96
|
+
parts.push({ type: 'br' });
|
|
97
|
+
}
|
|
98
|
+
else if (STEM_TICK_MARKS.has(token)) {
|
|
99
|
+
parts.push({ type: 'tick', value: token });
|
|
100
|
+
}
|
|
101
|
+
else if (STEM_CROSS_MARKS.has(token)) {
|
|
102
|
+
parts.push({ type: 'cross', value: token });
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
parts.push({ type: 'text', value: token });
|
|
106
|
+
}
|
|
107
|
+
lastIndex = match.index + token.length;
|
|
108
|
+
}
|
|
109
|
+
if (lastIndex < normalized.length) {
|
|
110
|
+
parts.push({ type: 'text', value: normalized.slice(lastIndex) });
|
|
111
|
+
}
|
|
112
|
+
return parts;
|
|
113
|
+
}
|
|
78
114
|
export function normalizeStudentText(value) {
|
|
79
115
|
return value.toString().trim().toLowerCase().replace(/\s+/g, ' ');
|
|
80
116
|
}
|
|
@@ -84,6 +120,59 @@ export function isBlankMatch(studentValue, alts) {
|
|
|
84
120
|
return false;
|
|
85
121
|
return alts.some((alt) => normalizeStudentText(alt) === user);
|
|
86
122
|
}
|
|
123
|
+
export function parseWordBankEntry(raw) {
|
|
124
|
+
if (raw == null)
|
|
125
|
+
return null;
|
|
126
|
+
const options = Array.isArray(raw)
|
|
127
|
+
? raw.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
128
|
+
: typeof raw === 'object' && Array.isArray(raw.options)
|
|
129
|
+
? raw.options
|
|
130
|
+
.map((item) => String(item ?? '').trim())
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
: [];
|
|
133
|
+
return options.length >= 2 ? { options } : null;
|
|
134
|
+
}
|
|
135
|
+
export function normalizeWordBank(raw, requiredLength) {
|
|
136
|
+
if (!Array.isArray(raw))
|
|
137
|
+
return undefined;
|
|
138
|
+
let result = raw.map((entry) => parseWordBankEntry(entry));
|
|
139
|
+
if (typeof requiredLength === 'number' && requiredLength > 0) {
|
|
140
|
+
result = result.slice(0, requiredLength);
|
|
141
|
+
while (result.length < requiredLength)
|
|
142
|
+
result.push(null);
|
|
143
|
+
}
|
|
144
|
+
return result.some((entry) => entry && entry.options.length >= 2) ? result : undefined;
|
|
145
|
+
}
|
|
146
|
+
export function resolveBlankOptions(blankIndex, itemWordBank, groupWordBank) {
|
|
147
|
+
const entry = itemWordBank?.[blankIndex] ?? groupWordBank?.[blankIndex];
|
|
148
|
+
if (entry && entry.options.length >= 2)
|
|
149
|
+
return entry.options;
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
export function hasAnyBlankOptions(items, groupWordBank) {
|
|
153
|
+
if (normalizeWordBank(groupWordBank))
|
|
154
|
+
return true;
|
|
155
|
+
return items.some((item) => Boolean(normalizeWordBank(item.wordBank)));
|
|
156
|
+
}
|
|
157
|
+
export function toFillInBlankImageUrls(value) {
|
|
158
|
+
if (Array.isArray(value)) {
|
|
159
|
+
return value
|
|
160
|
+
.filter((url) => typeof url === 'string')
|
|
161
|
+
.map((url) => url.trim())
|
|
162
|
+
.filter((url) => url.length > 0);
|
|
163
|
+
}
|
|
164
|
+
if (typeof value === 'string' && value.trim()) {
|
|
165
|
+
return [value.trim()];
|
|
166
|
+
}
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
export function fromFillInBlankImageUrls(urls) {
|
|
170
|
+
if (urls.length === 0)
|
|
171
|
+
return undefined;
|
|
172
|
+
if (urls.length === 1)
|
|
173
|
+
return urls[0];
|
|
174
|
+
return urls;
|
|
175
|
+
}
|
|
87
176
|
export function padStudentAnswers(raw, blankCount) {
|
|
88
177
|
const values = Array.isArray(raw?.answers)
|
|
89
178
|
? raw.answers.map((item) => String(item ?? ''))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/fill-in-blank-group.type';
|
|
2
|
-
import { blankSlotCount, extractPlaceholderIndices, normalizeAnswersMatrix, } from './blank-utils';
|
|
2
|
+
import { blankSlotCount, extractPlaceholderIndices, hasAnyBlankOptions, fromFillInBlankImageUrls, normalizeAnswersMatrix, normalizeWordBank, toFillInBlankImageUrls, } from './blank-utils';
|
|
3
3
|
function isRecord(value) {
|
|
4
4
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
5
|
}
|
|
@@ -39,16 +39,18 @@ export function mapFillInBlankGroupItem(item, index, fallbackAnswer) {
|
|
|
39
39
|
return [];
|
|
40
40
|
return alts.length > 0 ? alts : [''];
|
|
41
41
|
});
|
|
42
|
+
const wordBank = normalizeWordBank(content.wordBank ?? record.wordBank, blankSlotCount(question));
|
|
42
43
|
return {
|
|
43
44
|
question,
|
|
44
45
|
answers,
|
|
45
46
|
caseSensitive: correctAnswer.caseSensitive === true
|
|
46
47
|
|| record.caseSensitive === true
|
|
47
48
|
|| fallback.caseSensitive === true,
|
|
48
|
-
imageUrl:
|
|
49
|
+
imageUrl: fromFillInBlankImageUrls(toFillInBlankImageUrls(content.imageUrl ?? record.imageUrl)),
|
|
49
50
|
audioUrl: asString(content.audioUrl) || asString(record.audioUrl),
|
|
50
51
|
showHints: content.showHints === true,
|
|
51
52
|
hints: asStringArray(content.hints),
|
|
53
|
+
...(wordBank ? { wordBank } : {}),
|
|
52
54
|
isExample,
|
|
53
55
|
points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
|
|
54
56
|
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
@@ -66,33 +68,49 @@ export function mapQuestionToFillInBlankGroupData(question) {
|
|
|
66
68
|
const meta = asRecord(content.meta);
|
|
67
69
|
if (Array.isArray(content.items)) {
|
|
68
70
|
const items = content.items.map((item, index) => mapFillInBlankGroupItem(item, index, fallbackAnswers[index]));
|
|
71
|
+
const groupWordBank = normalizeWordBank(meta.wordBank ?? content.wordBank);
|
|
72
|
+
const viewMode = meta.viewMode === 'WITH_OPTIONS' || hasAnyBlankOptions(items, groupWordBank)
|
|
73
|
+
? 'WITH_OPTIONS'
|
|
74
|
+
: undefined;
|
|
69
75
|
return {
|
|
70
76
|
instruction: asString(meta.instruction) || FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION,
|
|
77
|
+
passage: asString(meta.passage),
|
|
71
78
|
audioUrl: asString(meta.audioUrl),
|
|
72
|
-
imageUrl:
|
|
79
|
+
imageUrl: fromFillInBlankImageUrls(toFillInBlankImageUrls(meta.imageUrl)),
|
|
73
80
|
showHints: meta.showHints === true,
|
|
74
81
|
hints: asStringArray(meta.hints),
|
|
82
|
+
...(viewMode ? { viewMode } : {}),
|
|
83
|
+
...(groupWordBank ? { wordBank: groupWordBank } : {}),
|
|
75
84
|
items,
|
|
76
85
|
explanation,
|
|
77
86
|
points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
|
|
78
87
|
};
|
|
79
88
|
}
|
|
80
89
|
if (Array.isArray(answer.items)) {
|
|
90
|
+
const items = answer.items.map((item, index) => mapFillInBlankGroupItem(item, index, fallbackAnswers[index]));
|
|
91
|
+
const groupWordBank = normalizeWordBank(answer.wordBank);
|
|
92
|
+
const viewMode = answer.viewMode === 'WITH_OPTIONS' || hasAnyBlankOptions(items, groupWordBank)
|
|
93
|
+
? 'WITH_OPTIONS'
|
|
94
|
+
: undefined;
|
|
81
95
|
return {
|
|
82
96
|
instruction: asString(answer.instruction, FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION),
|
|
97
|
+
passage: asString(answer.passage),
|
|
83
98
|
audioUrl: asString(answer.audioUrl),
|
|
84
|
-
imageUrl:
|
|
99
|
+
imageUrl: fromFillInBlankImageUrls(toFillInBlankImageUrls(answer.imageUrl)),
|
|
85
100
|
showHints: answer.showHints === true,
|
|
86
101
|
hints: asStringArray(answer.hints),
|
|
87
|
-
|
|
102
|
+
...(viewMode ? { viewMode } : {}),
|
|
103
|
+
...(groupWordBank ? { wordBank: groupWordBank } : {}),
|
|
104
|
+
items,
|
|
88
105
|
explanation,
|
|
89
106
|
points: typeof answer.points === 'number' ? answer.points : undefined,
|
|
90
107
|
};
|
|
91
108
|
}
|
|
92
109
|
return {
|
|
93
110
|
instruction: FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION,
|
|
111
|
+
passage: '',
|
|
94
112
|
audioUrl: '',
|
|
95
|
-
imageUrl:
|
|
113
|
+
imageUrl: undefined,
|
|
96
114
|
showHints: false,
|
|
97
115
|
hints: [],
|
|
98
116
|
items: [createEmptyFillInBlankGroupItem(1)],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION, FILL_IN_BLANK_ITEM_TYPE, } from '../../_shared/types/fill-in-blank-group.type';
|
|
2
|
-
import { blankSlotCount, extractPlaceholderIndices, normalizeAnswersMatrix, } from './blank-utils';
|
|
2
|
+
import { blankSlotCount, extractPlaceholderIndices, hasAnyBlankOptions, fromFillInBlankImageUrls, normalizeAnswersMatrix, normalizeWordBank, toFillInBlankImageUrls, } from './blank-utils';
|
|
3
3
|
function sumGradablePoints(items) {
|
|
4
4
|
return items.reduce((total, item) => {
|
|
5
5
|
if (item.isExample)
|
|
@@ -20,6 +20,9 @@ function toItemAnswer(item) {
|
|
|
20
20
|
caseSensitive: item.caseSensitive === true,
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
+
function isContentEmpty(html) {
|
|
24
|
+
return !html || html === '<p></p>' || html === '<p><br></p>' || html.trim() === '';
|
|
25
|
+
}
|
|
23
26
|
/**
|
|
24
27
|
* wrapOnSave nests creator fields under `answer`. getFormData() (Quiz Playground
|
|
25
28
|
* Update save) returns the same fields at the root. Prefer nested items, then root.
|
|
@@ -50,15 +53,18 @@ export const transformFillInBlankGroup = (question) => {
|
|
|
50
53
|
const hints = Array.isArray(item.hints)
|
|
51
54
|
? item.hints.map((hint) => String(hint ?? '').trim()).filter(Boolean)
|
|
52
55
|
: [];
|
|
56
|
+
const wordBank = normalizeWordBank(item.wordBank, blankSlotCount(item.question || ''));
|
|
57
|
+
const imageUrl = fromFillInBlankImageUrls(toFillInBlankImageUrls(item.imageUrl));
|
|
53
58
|
return {
|
|
54
59
|
questionType: FILL_IN_BLANK_ITEM_TYPE,
|
|
55
60
|
...(item.isExample ? { isExample: true } : {}),
|
|
56
61
|
content: {
|
|
57
62
|
question: item.question || '',
|
|
58
|
-
...(
|
|
63
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
59
64
|
...(item.audioUrl ? { audioUrl: item.audioUrl } : {}),
|
|
60
65
|
...(item.showHints ? { showHints: true } : {}),
|
|
61
66
|
...(hints.length > 0 ? { hints } : {}),
|
|
67
|
+
...(wordBank ? { wordBank } : {}),
|
|
62
68
|
},
|
|
63
69
|
correctAnswer,
|
|
64
70
|
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
@@ -68,14 +74,23 @@ export const transformFillInBlankGroup = (question) => {
|
|
|
68
74
|
const groupHints = Array.isArray(answerData?.hints)
|
|
69
75
|
? answerData.hints.map((hint) => String(hint ?? '').trim()).filter(Boolean)
|
|
70
76
|
: [];
|
|
77
|
+
const passage = answerData?.passage?.trim();
|
|
78
|
+
const groupWordBank = normalizeWordBank(answerData?.wordBank);
|
|
79
|
+
const viewMode = answerData?.viewMode === 'WITH_OPTIONS' || hasAnyBlankOptions(items, groupWordBank)
|
|
80
|
+
? 'WITH_OPTIONS'
|
|
81
|
+
: undefined;
|
|
82
|
+
const groupImageUrl = fromFillInBlankImageUrls(toFillInBlankImageUrls(answerData?.imageUrl));
|
|
71
83
|
return {
|
|
72
84
|
apiContent: {
|
|
73
85
|
meta: {
|
|
74
86
|
instruction: answerData?.instruction || FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION,
|
|
87
|
+
...(!isContentEmpty(passage) ? { passage } : {}),
|
|
75
88
|
...(answerData?.audioUrl ? { audioUrl: answerData.audioUrl } : {}),
|
|
76
|
-
...(
|
|
89
|
+
...(groupImageUrl !== undefined ? { imageUrl: groupImageUrl } : {}),
|
|
77
90
|
...(answerData?.showHints ? { showHints: true } : {}),
|
|
78
91
|
...(groupHints.length > 0 ? { hints: groupHints } : {}),
|
|
92
|
+
...(viewMode ? { viewMode } : {}),
|
|
93
|
+
...(groupWordBank ? { wordBank: groupWordBank } : {}),
|
|
79
94
|
},
|
|
80
95
|
items: apiItems,
|
|
81
96
|
},
|
package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js
CHANGED
|
@@ -6,7 +6,7 @@ import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
|
6
6
|
function normalizeWord(value) {
|
|
7
7
|
return value.trim().toLowerCase();
|
|
8
8
|
}
|
|
9
|
-
function ClientImageItem({ url, alt }) {
|
|
9
|
+
function ClientImageItem({ url, alt, large = false, }) {
|
|
10
10
|
const { previewUrl, isLoading } = usePresignedFileUrl(url);
|
|
11
11
|
const src = previewUrl ||
|
|
12
12
|
(typeof url === 'string' &&
|
|
@@ -19,7 +19,7 @@ function ClientImageItem({ url, alt }) {
|
|
|
19
19
|
if (!src && !isLoading) {
|
|
20
20
|
return (_jsx("div", { className: "flex h-28 w-28 items-center justify-center rounded-lg border border-gray-200 bg-gray-50 text-xs text-gray-400", children: "Kh\u00F4ng t\u1EA3i \u0111\u01B0\u1EE3c \u1EA3nh" }));
|
|
21
21
|
}
|
|
22
|
-
return (_jsx("div", { className:
|
|
22
|
+
return (_jsx("div", { className: cn('relative flex-shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-gray-50', large ? 'max-h-80 w-full max-w-xl' : 'h-28 w-28'), children: isLoading ? (_jsx("div", { className: "flex h-28 w-full items-center justify-center", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-violet-500" }) })) : (_jsx("img", { src: src, alt: alt || 'Picture', className: cn('object-contain', large ? 'mx-auto max-h-80 w-auto' : 'h-full w-full') })) }));
|
|
23
23
|
}
|
|
24
24
|
export function MatchWordToPictureGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
25
25
|
const items = questionData.items || [];
|
|
@@ -40,19 +40,19 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
|
|
|
40
40
|
usedWords.add(normalizeWord(value));
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (
|
|
43
|
+
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 }))] })), wordBank.length > 0 && (_jsxs("div", { className: "rounded-xl border border-violet-200 bg-gradient-to-r from-violet-50 to-indigo-50 p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-violet-700", children: "Word bank:" }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => {
|
|
44
44
|
const isUsed = usedWords.has(normalizeWord(word));
|
|
45
45
|
return (_jsx("span", { className: cn('rounded-full border px-3 py-1 text-sm font-semibold', isUsed
|
|
46
46
|
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
|
47
47
|
: 'border-violet-200 bg-white text-slate-700'), children: word }, `${word}-${index}`));
|
|
48
|
-
}) })] })), items.map((item, itemIndex) => {
|
|
48
|
+
}) })] })), questionData.imageUrl ? (_jsx("div", { className: "flex justify-center rounded-lg border border-slate-100 bg-slate-50 p-3", children: _jsx(ClientImageItem, { url: questionData.imageUrl, alt: "Look and match", large: true }) })) : null, items.map((item, itemIndex) => {
|
|
49
49
|
const selectedWord = item.isExample
|
|
50
50
|
? item.correctAnswer
|
|
51
51
|
: (userAnswers[itemIndex] ?? '');
|
|
52
52
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
53
53
|
const isCorrect = normalizeWord(selectedWord) === normalizeWord(item.correctAnswer) &&
|
|
54
54
|
Boolean(item.correctAnswer);
|
|
55
|
-
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "flex flex-col gap-4 p-4 sm:flex-row sm:items-center", children: [_jsx(ClientImageItem, { url: item.imageUrl, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}` }), _jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [
|
|
55
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "flex flex-col gap-4 p-4 sm:flex-row sm:items-center", children: [item.imageUrl && !questionData.imageUrl ? (_jsx(ClientImageItem, { url: item.imageUrl, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}` })) : null, _jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [_jsxs("p", { className: "text-sm font-semibold text-gray-800", children: [item.questionNumber || itemIndex + 1, ". Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p"] }), _jsxs("div", { className: "flex items-center gap-3", children: [item.isExample ? (_jsx("span", { className: "text-sm italic text-gray-800 underline decoration-gray-400", children: item.correctAnswer })) : (_jsxs("select", { value: selectedWord, disabled: !canSelect, onChange: (event) => handleSelect(itemIndex, event.target.value), className: cn('h-10 max-w-xs rounded-md border-2 bg-white px-3 text-sm font-medium', isReviewMode && isCorrect
|
|
56
56
|
? 'border-green-400 bg-green-50 text-green-700'
|
|
57
57
|
: isReviewMode && selectedWord && !isCorrect
|
|
58
58
|
? 'border-red-400 bg-red-50 text-red-700'
|
|
@@ -60,6 +60,6 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
|
|
|
60
60
|
const isUsedByOther = usedWords.has(normalizeWord(word)) &&
|
|
61
61
|
normalizeWord(word) !== normalizeWord(selectedWord);
|
|
62
62
|
return (_jsx("option", { value: word, disabled: isUsedByOther, children: word }, `${word}-${wordIndex}`));
|
|
63
|
-
})] }), isReviewMode && !item.isExample && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(Check, { className: "h-5 w-5 text-green-500" })) : (_jsx(X, { className: "h-5 w-5 text-red-500" })) }))] }), isReviewMode && !item.isExample && !isCorrect && (_jsxs("p", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: item.correctAnswer || '—' })] }))] })] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
63
|
+
})] })), isReviewMode && !item.isExample && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(Check, { className: "h-5 w-5 text-green-500" })) : (_jsx(X, { className: "h-5 w-5 text-red-500" })) }))] }), isReviewMode && !item.isExample && !isCorrect && (_jsxs("p", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: item.correctAnswer || '—' })] }))] })] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
64
64
|
}), 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] }))] }));
|
|
65
65
|
}
|
|
@@ -28,7 +28,9 @@ function sumPoints(items) {
|
|
|
28
28
|
}
|
|
29
29
|
export function MatchWordToPictureGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
|
|
30
30
|
const seeded = mapQuestionToMatchWordToPictureGroupData(initialData ? { answer: initialData } : null);
|
|
31
|
+
const [title, setTitle] = useState(initialData?.title || '');
|
|
31
32
|
const [instruction, setInstruction] = useState(initialData?.instruction || seeded.instruction || MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION);
|
|
33
|
+
const [imageUrl, setImageUrl] = useState(initialData?.imageUrl || '');
|
|
32
34
|
const [wordBank, setWordBank] = useState(initialData?.wordBank?.length ? initialData.wordBank : seeded.wordBank);
|
|
33
35
|
const [items, setItems] = useState(() => (initialData?.items?.length ? initialData.items : seeded.items));
|
|
34
36
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
@@ -39,8 +41,10 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
39
41
|
useEffect(() => {
|
|
40
42
|
onChangeRef.current = onChange;
|
|
41
43
|
}, [onChange]);
|
|
42
|
-
const buildPayload = (nextInstruction = instruction, nextWordBank = wordBank, nextItems = items, nextExplanation = explanation) => ({
|
|
44
|
+
const buildPayload = (nextTitle = title, nextInstruction = instruction, nextImageUrl = imageUrl, nextWordBank = wordBank, nextItems = items, nextExplanation = explanation) => ({
|
|
45
|
+
...(nextTitle.trim() ? { title: nextTitle } : {}),
|
|
43
46
|
instruction: nextInstruction,
|
|
47
|
+
...(nextImageUrl.trim() ? { imageUrl: nextImageUrl } : {}),
|
|
44
48
|
wordBank: nextWordBank,
|
|
45
49
|
items: nextItems,
|
|
46
50
|
explanation: nextExplanation,
|
|
@@ -57,7 +61,7 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
57
61
|
debouncedOnChange(payload);
|
|
58
62
|
onUnsavedChangesChange?.(true);
|
|
59
63
|
}
|
|
60
|
-
}, [instruction, wordBank, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
64
|
+
}, [title, instruction, imageUrl, wordBank, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
61
65
|
const validate = () => {
|
|
62
66
|
const nextErrors = [];
|
|
63
67
|
if (!instruction.trim())
|
|
@@ -68,8 +72,9 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
68
72
|
if (items.length < 1)
|
|
69
73
|
nextErrors.push('Cần ít nhất một câu hỏi.');
|
|
70
74
|
items.forEach((item, index) => {
|
|
71
|
-
if (!item.imageUrl
|
|
72
|
-
nextErrors.push(`Câu ${index + 1}: cần ảnh.`);
|
|
75
|
+
if (!imageUrl.trim() && !item.imageUrl?.trim()) {
|
|
76
|
+
nextErrors.push(`Câu ${index + 1}: cần ảnh (hoặc ảnh chung).`);
|
|
77
|
+
}
|
|
73
78
|
if (!item.correctAnswer.trim())
|
|
74
79
|
nextErrors.push(`Câu ${index + 1}: cần chọn từ đúng.`);
|
|
75
80
|
});
|
|
@@ -83,13 +88,16 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
83
88
|
getFormData: () => buildPayload(),
|
|
84
89
|
};
|
|
85
90
|
}
|
|
86
|
-
}, [validationRef, instruction, wordBank, items, explanation]);
|
|
91
|
+
}, [validationRef, title, instruction, imageUrl, wordBank, items, explanation]);
|
|
87
92
|
const updateItem = (index, patch) => {
|
|
88
93
|
setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item)));
|
|
89
94
|
};
|
|
90
95
|
const currentPayload = buildPayload();
|
|
91
96
|
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
|
|
97
|
+
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) })] }), _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
98
|
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, onChange: (url) => updateItem(index, { imageUrl: url }) }), _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" }),
|
|
99
|
+
}) }) })), 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, onChange: (url) => updateItem(index, { imageUrl: url }) }), _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([
|
|
100
|
+
...wordBank.map((word) => word.trim()).filter(Boolean),
|
|
101
|
+
item.correctAnswer.trim(),
|
|
102
|
+
].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
103
|
}
|
|
@@ -57,10 +57,14 @@ 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 imageUrl = asString(meta.imageUrl).trim() || asString(answer.imageUrl).trim();
|
|
60
62
|
return {
|
|
63
|
+
...(title ? { title } : {}),
|
|
61
64
|
instruction: asString(meta.instruction) ||
|
|
62
65
|
asString(answer.instruction) ||
|
|
63
66
|
MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION,
|
|
67
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
64
68
|
wordBank,
|
|
65
69
|
items,
|
|
66
70
|
explanation,
|
|
@@ -15,7 +15,9 @@ export const transformMatchWordToPictureGroup = (question) => {
|
|
|
15
15
|
const apiItems = items.map((item, index) => ({
|
|
16
16
|
questionType: MATCH_WORD_TO_PICTURE_ITEM_TYPE,
|
|
17
17
|
...(item.isExample ? { isExample: true } : {}),
|
|
18
|
-
content: {
|
|
18
|
+
content: {
|
|
19
|
+
...(item.imageUrl?.trim() ? { imageUrl: item.imageUrl.trim() } : {}),
|
|
20
|
+
},
|
|
19
21
|
correctAnswer: { answer: item.correctAnswer || '' },
|
|
20
22
|
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
21
23
|
questionNumber: item.questionNumber || index + 1,
|
|
@@ -24,6 +26,12 @@ export const transformMatchWordToPictureGroup = (question) => {
|
|
|
24
26
|
apiContent: {
|
|
25
27
|
meta: {
|
|
26
28
|
instruction: answerData?.instruction || MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION,
|
|
29
|
+
...(typeof answerData?.title === 'string' && answerData.title.trim()
|
|
30
|
+
? { title: answerData.title.trim() }
|
|
31
|
+
: {}),
|
|
32
|
+
...(typeof answerData?.imageUrl === 'string' && answerData.imageUrl.trim()
|
|
33
|
+
? { imageUrl: answerData.imageUrl.trim() }
|
|
34
|
+
: {}),
|
|
27
35
|
wordBank,
|
|
28
36
|
},
|
|
29
37
|
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] }))] }));
|