@tinyweb_dev/oe-exam-sdk 1.0.0 → 1.0.2
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/api/exam-taking.types.d.ts +1 -0
- package/dist/components/exams/ExamPreviewDialog.js +4 -2
- package/dist/components/exams/take/components/QuestionRenderer.js +2 -2
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +3 -1
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +5 -2
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +13 -20
- package/dist/components/exams/take/exam-taking.utils.d.ts +3 -2
- package/dist/components/exams/take/exam-taking.utils.js +3 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +11 -3
- package/dist/components/exams/take/utils/question-transformers.js +45 -2
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.d.ts +6 -2
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +26 -5
- package/dist/components/questions/_shared/types/fill-in-blank.type.d.ts +9 -0
- package/dist/components/questions/_shared/types/question-group.type.d.ts +11 -0
- package/dist/components/questions/_shared/types/question-group.type.js +4 -1
- package/dist/components/questions/_shared/types/spontaneous-qa.type.d.ts +7 -0
- package/dist/components/questions/_shared/types/word-fill-structured-form.type.d.ts +1 -1
- package/dist/components/questions/groups/ImageLabelGroupCard.d.ts +15 -0
- package/dist/components/questions/groups/ImageLabelGroupCard.js +42 -0
- package/dist/components/questions/groups/ImageLabelWordBank.d.ts +10 -0
- package/dist/components/questions/groups/ImageLabelWordBank.js +17 -0
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +33 -9
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +6 -2
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +28 -5
- package/dist/components/questions/types/fill-in-blank/register.js +5 -0
- package/dist/components/questions/types/fill-in-blank/transform.js +4 -1
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.d.ts +11 -0
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +27 -0
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQAClient.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQAClient.js +15 -0
- package/dist/components/questions/types/spontaneous-qa/map-spontaneous-qa-data.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa/map-spontaneous-qa-data.js +33 -0
- package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.d.ts +2 -2
- package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.js +15 -5
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +39 -15
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +35 -12
- package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +2 -10
- package/dist/components/questions/types/word-fill-structured-form/transform.js +2 -1
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +51 -33
- package/dist/components/questions/viewer/QuestionViewer.js +12 -68
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +3 -1
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +4 -2
- package/dist/components/results/renderers/review-question-body-dedicated.js +2 -2
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +12 -3
- package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +13 -20
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +8 -0
- package/dist/shared/constants/routes.d.ts +2 -2
- package/dist/shared/constants/routes.js +3 -3
- package/dist/shared/lib/rich-text.d.ts +4 -0
- package/dist/shared/lib/rich-text.js +25 -0
- package/dist/shared/lib/stores/examQuestionStore.d.ts +4 -0
- package/dist/shared/lib/utils/question-transform-types.d.ts +4 -0
- package/dist/shared/lib/utils/question-transform.js +1 -1
- package/dist/shared/lib/utils/wfsf-blank-match.d.ts +9 -0
- package/dist/shared/lib/utils/wfsf-blank-match.js +60 -0
- package/package.json +1 -1
- package/dist/components/themes/cambridge-yle/assets/images/bg-title-old.png +0 -0
- package/dist/components/themes/cambridge-yle/assets/images/border-old.png +0 -0
- package/dist/components/themes/cambridge-yle/assets/images/close_old.png +0 -0
- package/dist/components/themes/cambridge-yle/assets/images/frame-old.png +0 -0
|
@@ -60,9 +60,10 @@ export function WfsfWordBankEditor({ mode, onModeChange, sharedBank, onSharedBan
|
|
|
60
60
|
next[blankIndex] = { options };
|
|
61
61
|
onOptionsBankChange(next);
|
|
62
62
|
const key = blankKeys[blankIndex];
|
|
63
|
-
const removed = entry.options[optionIndex];
|
|
64
|
-
if (key && answers[key]
|
|
65
|
-
onAnswerChange(key,
|
|
63
|
+
const removed = entry.options[optionIndex]?.trim();
|
|
64
|
+
if (key && removed && (answers[key] || []).includes(removed)) {
|
|
65
|
+
onAnswerChange(key, (answers[key] || []).filter((alt) => alt !== removed));
|
|
66
|
+
}
|
|
66
67
|
};
|
|
67
68
|
return (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-indigo-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-indigo-400 to-purple-400" }), _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-indigo-100", children: _jsx(Library, { className: "h-3.5 w-3.5 text-indigo-600" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "Word Bank / g\u1EE3i \u00FD \u0111\u00E1p \u00E1n" }), _jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: "Ch\u1ECDn ch\u1EBF \u0111\u1ED9 hi\u1EC3n th\u1ECB cho h\u1ECDc sinh khi l\u00E0m b\u00E0i (WFSF plan 0024/0025)" })] })] }) }), _jsx(CardContent, { className: "space-y-3 px-4 pb-4", children: _jsx("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-3", children: MODE_OPTIONS.map((opt) => {
|
|
68
69
|
const active = mode === opt.id;
|
|
@@ -76,10 +77,19 @@ export function WfsfWordBankEditor({ mode, onModeChange, sharedBank, onSharedBan
|
|
|
76
77
|
: 'bg-gradient-to-br from-purple-500 to-pink-600'}`, children: index + 1 }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Checkbox, { id: `wfsf-ex-${index}`, checked: !!item.isExample, onCheckedChange: (c) => toggleSharedExample(index, c === true) }), _jsx(Label, { htmlFor: `wfsf-ex-${index}`, className: "cursor-pointer text-xs text-gray-600", children: "Example" })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeSharedWord(index), className: "h-7 w-7 p-0 text-gray-400 hover:bg-red-50 hover:text-red-500", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] })] }), _jsx(Input, { value: item.word, onChange: (e) => updateSharedWord(index, e.target.value), placeholder: "Nh\u1EADp t\u1EEB...", className: item.isExample ? 'bg-amber-50' : 'border-gray-200' })] }, index))) }))] })] })), mode === 'WITH_OPTIONS' && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-teal-400 to-cyan-400" }), _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-teal-100", children: _jsx(ListChecks, { className: "h-3.5 w-3.5 text-teal-600" }) }), _jsxs(CardTitle, { className: "text-base font-semibold text-gray-800", children: ["L\u1EF1a ch\u1ECDn theo ch\u1ED7 tr\u1ED1ng (", blankKeys.length, ")"] })] }) }), _jsx(CardContent, { className: "space-y-4 px-4 pb-4", children: blankKeys.length === 0 ? (_jsxs("p", { className: "rounded-lg border-2 border-dashed border-gray-200 p-6 text-center text-sm text-gray-500", children: ["Ch\u01B0a c\u00F3 ch\u1ED7 tr\u1ED1ng. Th\u00EAm ", '{0}', ", ", '{1}', "\u2026 trong n\u1ED9i dung tr\u01B0\u1EDBc."] })) : (blankKeys.map((key, blankIndex) => {
|
|
77
78
|
const entry = optionsBank[blankIndex] ?? null;
|
|
78
79
|
const isFree = entry == null;
|
|
79
|
-
const selected = answers[key] ||
|
|
80
|
+
const selected = (answers[key] || []).filter(Boolean);
|
|
80
81
|
return (_jsxs("div", { className: `rounded-xl border-2 p-4 ${isFree
|
|
81
82
|
? 'border-dashed border-gray-200 bg-gray-50/40'
|
|
82
|
-
: 'border-gray-200 bg-white'}`, children: [_jsxs("div", { className: "mb-3 flex flex-wrap items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-teal-500 to-cyan-600 text-sm font-bold text-white", children: key }), _jsxs("span", { className: "text-sm font-semibold text-gray-700", children: ["Ch\u1ED7 tr\u1ED1ng ", _jsx("code", { className: "text-teal-700", children: `{${key}}` })] }), selected && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700", children: [_jsx(CheckCircle2, { className: "h-3 w-3" }), selected] }))] }), _jsxs("label", { className: "flex items-center gap-2 text-xs text-gray-600", children: [_jsx(Checkbox, { checked: isFree, onCheckedChange: (c) => setBlankFreeType(blankIndex, c === true) }), "Free-type (kh\u00F4ng options)"] })] }), !isFree && entry && (_jsxs("div", { className: "space-y-2", children: [entry.options.map((option, optionIndex) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", title: "Ch\u1ECDn
|
|
83
|
+
: 'border-gray-200 bg-white'}`, children: [_jsxs("div", { className: "mb-3 flex flex-wrap items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-teal-500 to-cyan-600 text-sm font-bold text-white", children: key }), _jsxs("span", { className: "text-sm font-semibold text-gray-700", children: ["Ch\u1ED7 tr\u1ED1ng ", _jsx("code", { className: "text-teal-700", children: `{${key}}` })] }), selected.length > 0 && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700", children: [_jsx(CheckCircle2, { className: "h-3 w-3" }), selected.join(' / ')] }))] }), _jsxs("label", { className: "flex items-center gap-2 text-xs text-gray-600", children: [_jsx(Checkbox, { checked: isFree, onCheckedChange: (c) => setBlankFreeType(blankIndex, c === true) }), "Free-type (kh\u00F4ng options)"] })] }), !isFree && entry && (_jsxs("div", { className: "space-y-2", children: [entry.options.map((option, optionIndex) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", title: "Ch\u1ECDn / b\u1ECF \u0111\u00E1p \u00E1n \u0111\u00FAng", onClick: () => {
|
|
84
|
+
const opt = option.trim();
|
|
85
|
+
if (!opt)
|
|
86
|
+
return;
|
|
87
|
+
const current = answers[key] || [];
|
|
88
|
+
const next = current.includes(opt)
|
|
89
|
+
? current.filter((alt) => alt !== opt)
|
|
90
|
+
: [...current, opt];
|
|
91
|
+
onAnswerChange(key, next);
|
|
92
|
+
}, className: `flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-xs font-bold ${selected.includes(option.trim()) && option.trim()
|
|
83
93
|
? 'bg-green-500 text-white'
|
|
84
94
|
: 'bg-gray-100 text-gray-500 hover:bg-green-100 hover:text-green-600'}`, children: String.fromCharCode(65 + optionIndex) }), _jsx(Input, { value: option, onChange: (e) => updateOption(blankIndex, optionIndex, e.target.value), placeholder: `Lựa chọn ${String.fromCharCode(65 + optionIndex)}`, className: "flex-1 border-gray-200 text-sm" }), entry.options.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeOption(blankIndex, optionIndex), className: "h-8 w-8 p-0 text-red-400 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, optionIndex))), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => addOption(blankIndex), className: "mt-1 gap-1 text-xs text-teal-600 hover:bg-teal-50", children: [_jsx(Plus, { className: "h-3 w-3" }), "Th\u00EAm l\u1EF1a ch\u1ECDn"] })] }))] }, key));
|
|
85
95
|
})) })] }))] }));
|
package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js
CHANGED
|
@@ -5,12 +5,15 @@ import { Check, X, FileText, Library } from 'lucide-react';
|
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
6
|
import { cn } from '../../../../shared/lib/utils';
|
|
7
7
|
import { detectWfsfBankMode, isOptionsWordEntry, normalizeSharedWordBank, } from './wfsf-word-bank.utils';
|
|
8
|
+
import { expandWfsfCorrectAlts, formatWfsfCorrectLabel, isWfsfBlankCorrect, primaryWfsfAnswers, } from '../../../../shared/lib/utils/wfsf-blank-match';
|
|
8
9
|
function normalizeWfsfBlankStyle(raw) {
|
|
9
10
|
const v = String(raw || '').toLowerCase();
|
|
10
11
|
if (v === 'letter' || v === 'box')
|
|
11
12
|
return 'letter';
|
|
12
13
|
if (v === 'phrase' || v === 'sentence')
|
|
13
14
|
return 'phrase';
|
|
15
|
+
if (v === 'translate')
|
|
16
|
+
return 'translate';
|
|
14
17
|
return 'word';
|
|
15
18
|
}
|
|
16
19
|
function parseStyleAttribute(styleValue) {
|
|
@@ -59,7 +62,10 @@ function ClientAudioItem({ url }) {
|
|
|
59
62
|
}
|
|
60
63
|
const DEFAULT_WFSF_HEADER = 'Điền từ vào biểu mẫu có cấu trúc';
|
|
61
64
|
export function WordFillStructuredFormClient({ questionData, isReviewMode = false, userAnswer, onAnswerChange, autoFillCorrectAnswers = false, instruction, questionNumber, explanation, }) {
|
|
62
|
-
const { title = '', content = '', answers = {}, caseSensitive = false, viewMode, wordBank = [], instruction: dataInstruction, } = questionData;
|
|
65
|
+
const { title = '', content = '', answers = {}, caseSensitive = false, blankStyle: rawBlankStyle, viewMode, wordBank = [], instruction: dataInstruction, } = questionData;
|
|
66
|
+
const blankStyle = normalizeWfsfBlankStyle(rawBlankStyle);
|
|
67
|
+
const isLetterBlank = blankStyle === 'letter';
|
|
68
|
+
const isPhraseBlank = blankStyle === 'phrase' || blankStyle === 'translate';
|
|
63
69
|
const allImages = useMemo(() => {
|
|
64
70
|
if (Array.isArray(questionData.imageUrls) && questionData.imageUrls.length > 0) {
|
|
65
71
|
return questionData.imageUrls.filter(Boolean);
|
|
@@ -80,7 +86,7 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
80
86
|
if (userAnswer)
|
|
81
87
|
return { ...userAnswer };
|
|
82
88
|
if (autoFillCorrectAnswers)
|
|
83
|
-
return
|
|
89
|
+
return primaryWfsfAnswers(answers);
|
|
84
90
|
return {};
|
|
85
91
|
});
|
|
86
92
|
const [expandedBlank, setExpandedBlank] = useState(null);
|
|
@@ -127,11 +133,7 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
127
133
|
});
|
|
128
134
|
}, [content]);
|
|
129
135
|
const allFilled = expectedBlankKeys.every(k => filledAnswers[k]?.trim());
|
|
130
|
-
const isAllCorrect = isReviewMode && expectedBlankKeys.every(k =>
|
|
131
|
-
const userVal = (filledAnswers[k] || '').trim();
|
|
132
|
-
const correctVal = (answers[k] || '').trim();
|
|
133
|
-
return caseSensitive ? userVal === correctVal : userVal.toLowerCase() === correctVal.toLowerCase();
|
|
134
|
-
});
|
|
136
|
+
const isAllCorrect = isReviewMode && expectedBlankKeys.every((k) => isWfsfBlankCorrect(filledAnswers[k] || '', answers[k], caseSensitive));
|
|
135
137
|
// Header = page rubric. Pearson preview maps draft.instruction → explanation.
|
|
136
138
|
const rawExplanation = String(explanation || questionData.explanation || '').trim();
|
|
137
139
|
const headerInstruction = String(instruction || dataInstruction || rawExplanation || '').trim();
|
|
@@ -153,11 +155,8 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
153
155
|
}, [isReviewMode, onAnswerChange]);
|
|
154
156
|
const renderBlank = useCallback((key, reactKey) => {
|
|
155
157
|
const filled = filledAnswers[key] || '';
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const normalizedCorrect = caseSensitive ? correctAnswer.trim() : correctAnswer.trim().toLowerCase();
|
|
159
|
-
const isCorrectFill = isReviewMode && normalizedFilled === normalizedCorrect;
|
|
160
|
-
const isWrongFill = isReviewMode && !!filled && normalizedFilled !== normalizedCorrect;
|
|
158
|
+
const isCorrectFill = isReviewMode && isWfsfBlankCorrect(filled, answers[key], caseSensitive);
|
|
159
|
+
const isWrongFill = isReviewMode && !!filled && !isCorrectFill;
|
|
161
160
|
const blankOptions = optionsByKey.get(key);
|
|
162
161
|
if (bankMode === 'WITH_OPTIONS' && blankOptions && blankOptions.length >= 2) {
|
|
163
162
|
const isExpanded = expandedBlank === key;
|
|
@@ -173,13 +172,36 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
173
172
|
? 'border-teal-500 bg-teal-100 text-teal-800'
|
|
174
173
|
: 'border-gray-200 bg-white text-gray-700 hover:border-teal-300 hover:bg-teal-50'), children: opt }, opt))) }))] }, reactKey));
|
|
175
174
|
}
|
|
176
|
-
return (_jsxs("span", { className: "relative mx-1 inline-flex items-baseline align-baseline", children: [_jsx("span", { className: "absolute -top-4 left-1 text-[10px] font-semibold text-gray-400", children: key }), _jsx("input", { type: "text", value: filled, onChange: (e) => handleInputChange(key, e.target.value), disabled: isReviewMode, placeholder: "",
|
|
175
|
+
return (_jsxs("span", { className: "relative mx-1 inline-flex items-baseline align-baseline", children: [_jsx("span", { className: "absolute -top-4 left-1 text-[10px] font-semibold text-gray-400", children: key }), _jsx("input", { type: "text", value: filled, onChange: (e) => handleInputChange(key, e.target.value), disabled: isReviewMode, placeholder: "", maxLength: isLetterBlank
|
|
176
|
+
? Math.max(1, expandWfsfCorrectAlts(answers[key]).reduce((max, alt) => Math.max(max, alt.trim().length), 0) || 1)
|
|
177
|
+
: undefined, size: isLetterBlank ? undefined : isPhraseBlank ? 28 : 10, className: cn('inline-block bg-slate-50/40 text-center text-sm font-semibold outline-none transition-all align-baseline hover:bg-slate-100/60 focus:bg-teal-50/60 focus:ring-0', isLetterBlank
|
|
178
|
+
? 'h-7 w-7 rounded border-2 px-0 uppercase'
|
|
179
|
+
: isPhraseBlank
|
|
180
|
+
? 'min-w-[16rem] max-w-[36rem] w-[28ch] rounded-t border-b-2 px-2 py-0.5'
|
|
181
|
+
: 'min-w-[4.5rem] max-w-[8rem] w-[10ch] rounded-t border-b-2 px-1 py-0.5', isReviewMode
|
|
177
182
|
? isCorrectFill
|
|
178
183
|
? 'border-emerald-500 bg-emerald-50 text-emerald-800 font-bold'
|
|
179
184
|
: isWrongFill
|
|
180
185
|
? 'border-rose-500 bg-rose-50 text-rose-800 font-bold'
|
|
181
186
|
: 'border-amber-500 bg-amber-50 text-amber-800'
|
|
182
|
-
: 'border-slate-400 text-slate-800 focus:border-teal-600')
|
|
187
|
+
: 'border-slate-400 text-slate-800 focus:border-teal-600'), style: isLetterBlank
|
|
188
|
+
? {
|
|
189
|
+
display: 'inline-block',
|
|
190
|
+
width: '1.75rem',
|
|
191
|
+
height: '1.75rem',
|
|
192
|
+
minWidth: '1.75rem',
|
|
193
|
+
maxWidth: '1.75rem',
|
|
194
|
+
padding: 0,
|
|
195
|
+
margin: '0 2px',
|
|
196
|
+
borderWidth: 2,
|
|
197
|
+
borderStyle: 'solid',
|
|
198
|
+
borderRadius: 4,
|
|
199
|
+
textAlign: 'center',
|
|
200
|
+
fontWeight: 700,
|
|
201
|
+
textTransform: 'uppercase',
|
|
202
|
+
boxSizing: 'border-box',
|
|
203
|
+
}
|
|
204
|
+
: undefined }), isReviewMode && filled && (isCorrectFill
|
|
183
205
|
? _jsx(Check, { className: "ml-0.5 h-3.5 w-3.5 text-green-600" })
|
|
184
206
|
: _jsx(X, { className: "ml-0.5 h-3.5 w-3.5 text-red-600" }))] }, reactKey));
|
|
185
207
|
}, [
|
|
@@ -190,6 +212,8 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
190
212
|
filledAnswers,
|
|
191
213
|
handleInputChange,
|
|
192
214
|
handleSelectOption,
|
|
215
|
+
isLetterBlank,
|
|
216
|
+
isPhraseBlank,
|
|
193
217
|
isReviewMode,
|
|
194
218
|
optionsByKey,
|
|
195
219
|
]);
|
|
@@ -260,5 +284,5 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
260
284
|
: used
|
|
261
285
|
? 'border-purple-200 bg-white/60 text-purple-400 line-through'
|
|
262
286
|
: 'border-purple-300 bg-white text-purple-800'), children: [item.word, item.isExample ? ' (ex)' : ''] }, `${item.word}-${index}`));
|
|
263
|
-
}) })] })), _jsx("div", { className: "structured-form-content space-y-2 rounded-lg border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-5 text-base leading-loose text-gray-800", children: renderedContent }), isReviewMode && !isAllCorrect && (_jsxs("div", { className: "rounded-lg border border-green-200 bg-green-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-green-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: expectedBlankKeys.map((key) => (_jsxs("span", { className: "rounded-lg border border-green-300 bg-green-100 px-3 py-1.5 text-sm font-semibold text-green-800", children: [`{${key}}`, ": ", answers[key]] }, key))) })] })), isReviewMode && displayExplanation && (_jsxs("div", { className: "rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-blue-600", children: "Gi\u1EA3i th\u00EDch" }), _jsx("p", { className: "mt-1 text-sm text-blue-800", children: displayExplanation })] }))] })] }) }));
|
|
287
|
+
}) })] })), _jsx("div", { className: "structured-form-content space-y-2 rounded-lg border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-5 text-base leading-loose text-gray-800", children: renderedContent }), isReviewMode && !isAllCorrect && (_jsxs("div", { className: "rounded-lg border border-green-200 bg-green-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-green-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: expectedBlankKeys.map((key) => (_jsxs("span", { className: "rounded-lg border border-green-300 bg-green-100 px-3 py-1.5 text-sm font-semibold text-green-800", children: [`{${key}}`, ": ", formatWfsfCorrectLabel(answers[key])] }, key))) })] })), isReviewMode && displayExplanation && (_jsxs("div", { className: "rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-blue-600", children: "Gi\u1EA3i th\u00EDch" }), _jsx("p", { className: "mt-1 text-sm text-blue-800", children: displayExplanation })] }))] })] }) }));
|
|
264
288
|
}
|
package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js
CHANGED
|
@@ -20,6 +20,7 @@ import { ImagePreview } from '../../../../components/ui/image-preview';
|
|
|
20
20
|
import { WordFillStructuredFormClient } from './WordFillStructuredFormClient';
|
|
21
21
|
import { WfsfWordBankEditor } from './WfsfWordBankEditor';
|
|
22
22
|
import { useDebouncedCallback, usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
23
|
+
import { expandWfsfCorrectAlts, hasWfsfBlankAnswer, normalizeWfsfAnswersMap, primaryWfsfAnswers, } from '../../../../shared/lib/utils/wfsf-blank-match';
|
|
23
24
|
import { BasicQuestionGroupCard } from '../../groups/BasicQuestionGroupCard';
|
|
24
25
|
import { DocumentGroupCard } from '../../groups/DocumentGroupCard';
|
|
25
26
|
import { useBasicQuestionGroup, getGroupNumberForIndex } from '../../_shared/hooks/useBasicQuestionGroup';
|
|
@@ -173,7 +174,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
173
174
|
// ── TipTap ─────────────────────────────────────────────────────────────────
|
|
174
175
|
const initialContent = initialData?.content || '<p></p>';
|
|
175
176
|
const [content, setContent] = useState(initialContent);
|
|
176
|
-
const [answers, setAnswers] = useState(initialData?.answers || {});
|
|
177
|
+
const [answers, setAnswers] = useState(() => normalizeWfsfAnswersMap(initialData?.answers || {}));
|
|
177
178
|
const previousPayloadRef = useRef('');
|
|
178
179
|
const previousInitialDataRef = useRef(initialData);
|
|
179
180
|
const isInitializingRef = useRef(false);
|
|
@@ -248,7 +249,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
248
249
|
? nextExplanation
|
|
249
250
|
: '');
|
|
250
251
|
}
|
|
251
|
-
setAnswers(initialData?.answers || {});
|
|
252
|
+
setAnswers(normalizeWfsfAnswersMap(initialData?.answers || {}));
|
|
252
253
|
setBlankStyle(initialData?.blankStyle);
|
|
253
254
|
const nextMode = detectWfsfBankMode(initialData?.viewMode ?? viewModeProp, initialData?.wordBank);
|
|
254
255
|
setBankMode(nextMode);
|
|
@@ -299,12 +300,31 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
299
300
|
changed = true;
|
|
300
301
|
} });
|
|
301
302
|
expectedBlankKeys.forEach(k => { if (!(k in next)) {
|
|
302
|
-
next[k] = '';
|
|
303
|
+
next[k] = [''];
|
|
303
304
|
changed = true;
|
|
304
305
|
} });
|
|
305
306
|
return changed ? next : prev;
|
|
306
307
|
});
|
|
307
308
|
}, [expectedBlankKeys]);
|
|
309
|
+
const updateAnswerAlt = useCallback((key, index, value) => {
|
|
310
|
+
setAnswers((prev) => {
|
|
311
|
+
const current = [...(prev[key] || [''])];
|
|
312
|
+
current[index] = value;
|
|
313
|
+
return { ...prev, [key]: current };
|
|
314
|
+
});
|
|
315
|
+
}, []);
|
|
316
|
+
const addAnswerAlt = useCallback((key) => {
|
|
317
|
+
setAnswers((prev) => ({ ...prev, [key]: [...(prev[key] || ['']), ''] }));
|
|
318
|
+
}, []);
|
|
319
|
+
const removeAnswerAlt = useCallback((key, index) => {
|
|
320
|
+
setAnswers((prev) => {
|
|
321
|
+
const current = [...(prev[key] || [''])];
|
|
322
|
+
if (current.length <= 1)
|
|
323
|
+
return { ...prev, [key]: [''] };
|
|
324
|
+
current.splice(index, 1);
|
|
325
|
+
return { ...prev, [key]: current };
|
|
326
|
+
});
|
|
327
|
+
}, []);
|
|
308
328
|
// Keep WITH_OPTIONS bank length aligned to blank keys
|
|
309
329
|
useEffect(() => {
|
|
310
330
|
if (bankMode !== 'WITH_OPTIONS')
|
|
@@ -413,7 +433,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
413
433
|
if (expectedBlankKeys.length === 0)
|
|
414
434
|
return false;
|
|
415
435
|
for (const key of expectedBlankKeys) {
|
|
416
|
-
if (!answers[key]
|
|
436
|
+
if (!hasWfsfBlankAnswer(answers[key]))
|
|
417
437
|
return false;
|
|
418
438
|
}
|
|
419
439
|
if (bankMode === 'WITH_WORD_BANK') {
|
|
@@ -432,8 +452,10 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
432
452
|
if (opts.length < 2)
|
|
433
453
|
return false;
|
|
434
454
|
const key = expectedBlankKeys[i];
|
|
435
|
-
const
|
|
436
|
-
if (
|
|
455
|
+
const alts = expandWfsfCorrectAlts(answers[key]);
|
|
456
|
+
if (alts.length === 0)
|
|
457
|
+
return false;
|
|
458
|
+
if (alts.some((alt) => !opts.includes(alt)))
|
|
437
459
|
return false;
|
|
438
460
|
}
|
|
439
461
|
}
|
|
@@ -453,10 +475,10 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
453
475
|
]);
|
|
454
476
|
// ── Preview mode ───────────────────────────────────────────────────────────
|
|
455
477
|
if (isPreviewMode) {
|
|
456
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsx(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-teal-500 to-cyan-600 shadow-sm", children: _jsx(Eye, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n t\u1EEB \u2013 Xem tr\u01B0\u1EDBc" }), _jsx("p", { className: "text-xs text-gray-500", children: "Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc v\u1EDBi \u0111\u00E1p \u00E1n \u0111\u00FAng \u0111\u00E3 \u0111\u01B0\u1EE3c \u0111i\u1EC1n s\u1EB5n" })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsPreviewMode(false), className: "gap-2 border-teal-200 text-teal-700 hover:bg-teal-50", children: [_jsx(Pencil, { className: "h-4 w-4" }), "Quay l\u1EA1i Edit"] })] }) }) }), _jsx(WordFillStructuredFormClient, { questionData: buildFormData(), isReviewMode: true, userAnswer: answers, instruction: instruction })] }));
|
|
478
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-teal-500 to-cyan-600 shadow-sm", children: _jsx(Eye, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n t\u1EEB \u2013 Xem tr\u01B0\u1EDBc" }), _jsx("p", { className: "text-xs text-gray-500", children: "Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc v\u1EDBi \u0111\u00E1p \u00E1n \u0111\u00FAng \u0111\u00E3 \u0111\u01B0\u1EE3c \u0111i\u1EC1n s\u1EB5n" })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsPreviewMode(false), className: "gap-2 border-teal-200 text-teal-700 hover:bg-teal-50", children: [_jsx(Pencil, { className: "h-4 w-4" }), "Quay l\u1EA1i Edit"] })] }) }) }), _jsx(WordFillStructuredFormClient, { questionData: buildFormData(), isReviewMode: true, userAnswer: primaryWfsfAnswers(answers), instruction: instruction })] }));
|
|
457
479
|
}
|
|
458
480
|
// ── Edit mode ──────────────────────────────────────────────────────────────
|
|
459
|
-
return (_jsxs("div", { className: "space-y-4", children: [isGrouped && effectiveGroupType === 'BASIC' && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `word-fill-structured-${partId ?? 'group'}` })), isGrouped && effectiveGroupType === 'DOCUMENT' && (_jsx(DocumentGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `word-fill-structured-${partId ?? 'document'}` })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-teal-500 via-cyan-500 to-emerald-500" }), _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100", children: _jsx(PenLine, { className: "h-4 w-4 text-teal-600" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n t\u1EEB v\u00E0o bi\u1EC3u m\u1EABu c\u00F3 c\u1EA5u tr\u00FAc" }), _jsxs("p", { className: "text-xs text-gray-500", children: ["Nh\u1EADp n\u1ED9i dung, \u0111\u1EB7t ch\u1ED7 tr\u1ED1ng b\u1EB1ng ", `{0}`, " ", `{1}`, "\u2026 r\u1ED3i \u0111i\u1EC1n \u0111\u00E1p \u00E1n b\u00EAn d\u01B0\u1EDBi"] })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsPreviewMode(true), disabled: expectedBlankKeys.length === 0 || expectedBlankKeys.some(k => !answers[k]
|
|
481
|
+
return (_jsxs("div", { className: "space-y-4", children: [isGrouped && effectiveGroupType === 'BASIC' && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `word-fill-structured-${partId ?? 'group'}` })), isGrouped && effectiveGroupType === 'DOCUMENT' && (_jsx(DocumentGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `word-fill-structured-${partId ?? 'document'}` })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-teal-500 via-cyan-500 to-emerald-500" }), _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100", children: _jsx(PenLine, { className: "h-4 w-4 text-teal-600" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n t\u1EEB v\u00E0o bi\u1EC3u m\u1EABu c\u00F3 c\u1EA5u tr\u00FAc" }), _jsxs("p", { className: "text-xs text-gray-500", children: ["Nh\u1EADp n\u1ED9i dung, \u0111\u1EB7t ch\u1ED7 tr\u1ED1ng b\u1EB1ng ", `{0}`, " ", `{1}`, "\u2026 r\u1ED3i \u0111i\u1EC1n \u0111\u00E1p \u00E1n b\u00EAn d\u01B0\u1EDBi"] })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsPreviewMode(true), disabled: expectedBlankKeys.length === 0 || expectedBlankKeys.some(k => !hasWfsfBlankAnswer(answers[k])), className: "gap-2 border-teal-200 text-teal-600 hover:bg-teal-50 hover:text-teal-700", children: [_jsx(Eye, { className: "h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] })] }) }), _jsxs(CardContent, { className: "space-y-4 px-4 pb-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "wfsf-instruction", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Info, { className: "h-4 w-4 text-teal-500" }), "Instruction", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(h\u01B0\u1EDBng d\u1EABn b\u00E0i t\u1EADp)" })] }), _jsx(Textarea, { id: "wfsf-instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: "VD: Complete the words in the sentences. Use the words in the box.", rows: 2, className: "min-h-[64px] resize-y border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx(PointsInput, { value: points, onChange: (e) => setPoints(Number(e.target.value) || 1) }), _jsxs("div", { role: "button", tabIndex: 0, onClick: () => setCaseSensitive(!caseSensitive), onKeyDown: (e) => { if (e.key === ' ' || e.key === 'Enter')
|
|
460
482
|
setCaseSensitive(!caseSensitive); }, style: { paddingBlock: 'calc(var(--spacing) * 1)' }, className: `inline-flex cursor-pointer items-center gap-2 rounded-md border px-3.5 text-sm font-medium transition-colors select-none ${caseSensitive
|
|
461
483
|
? 'border-teal-300 bg-teal-50 text-teal-700'
|
|
462
484
|
: 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'}`, children: [_jsx(Switch, { checked: caseSensitive, onCheckedChange: setCaseSensitive, className: caseSensitive ? 'data-[state=checked]:bg-teal-500 pointer-events-none' : 'pointer-events-none', tabIndex: -1 }), "Ph\u00E2n bi\u1EC7t Hoa / th\u01B0\u1EDDng"] })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "wfsf-title", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-teal-500" }), "Ti\u00EAu \u0111\u1EC1", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(Input, { id: "wfsf-title", value: effectiveTitle, onChange: (e) => setEffectiveTitle(e.target.value), placeholder: "Nh\u1EADp ti\u00EAu \u0111\u1EC1 c\u00E2u h\u1ECFi...", className: "border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100" })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Volume2, { className: "h-4 w-4 text-teal-500" }), "File \u00E2m thanh (Audio)", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setAudioUrls((prev) => [...prev, '']), className: "gap-1.5 border-teal-200 text-teal-600 hover:bg-teal-50 hover:text-teal-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm audio"] })] }), audioUrls.length === 0 ? (_jsx("div", { className: "flex items-center justify-between rounded-lg border border-dashed border-gray-200 bg-gray-50/50 p-3 text-xs text-gray-500", children: _jsx("span", { children: "Ch\u01B0a c\u00F3 file audio. Nh\u1EA5n \"Th\u00EAm audio\" \u0111\u1EC3 upload file nghe cho c\u00E2u h\u1ECFi." }) })) : (_jsx("div", { className: "space-y-2.5", children: audioUrls.map((audioUrl, index) => (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50/30 p-2.5", children: [_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-teal-100 text-xs font-semibold text-teal-700", children: index + 1 }), _jsx("div", { className: "flex-1", children: _jsx(FileUpload, { id: `wfsf-audio-${index}`, label: "", accept: "audio/*", value: audioUrl, onChange: (url) => {
|
|
@@ -475,10 +497,11 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
475
497
|
});
|
|
476
498
|
}, onRemove: () => {
|
|
477
499
|
setImageUrls((prev) => prev.filter((_, idx) => idx !== index));
|
|
478
|
-
} }, index))) }))] }), externalErrors && externalErrors.length > 0 && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4", children: [_jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-red-100", children: _jsx("span", { className: "text-lg", children: "\u274C" }) }), _jsxs("div", { className: "text-sm", children: [_jsx("p", { className: "font-semibold text-red-800", children: "L\u1ED7i:" }), _jsx("ul", { className: "mt-1 list-inside list-disc text-red-700", children: externalErrors.map((error, i) => _jsx("li", { children: error }, i)) })] })] }))] })] }), showContent && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-teal-100", children: _jsx(ListOrdered, { className: "h-3.5 w-3.5 text-teal-600" }) }), _jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "N\u1ED9i dung c\u1EA5u tr\u00FAc" })] }), expectedBlankKeys.length > 0 && (_jsxs("span", { className: "inline-flex items-center rounded-full border border-teal-200 bg-teal-50 px-2.5 py-0.5 text-xs font-semibold text-teal-700", children: [expectedBlankKeys.length, " ch\u1ED7 tr\u1ED1ng"] }))] }) }), _jsxs(CardContent, { className: "space-y-3 px-4 pb-4", children: [_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 transition-all focus-within:border-teal-300 focus-within:ring-2 focus-within:ring-teal-100", children: [_jsx(EditorToolbar, { editor: editor }), _jsx(EditorContent, { editor: editor })] }), _jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-teal-100 bg-teal-50/60 px-3 py-2.5", children: [_jsx(Info, { className: "mt-0.5 h-4 w-4 shrink-0 text-teal-500" }), _jsxs("p", { className: "text-xs text-teal-700", children: ["Nh\u1EADp ", _jsx("strong", { children: `{0}` }), ", ", _jsx("strong", { children: `{1}` }), ", ", _jsx("strong", { children: `{2}` }), "\u2026 v\u00E0o v\u0103n b\u1EA3n \u0111\u1EC3 t\u1EA1o \u00F4 tr\u1ED1ng. \u0110\u00E1p \u00E1n s\u1EBD t\u1EF1 \u0111\u1ED9ng xu\u1EA5t hi\u1EC7n b\u00EAn d\u01B0\u1EDBi \u0111\u1EC3 b\u1EA1n nh\u1EADp."] })] })] })] })), _jsx(WfsfWordBankEditor, { mode: bankMode, onModeChange: handleBankModeChange, sharedBank: sharedBank, onSharedBankChange: setSharedBank, optionsBank: optionsBank, onOptionsBankChange: setOptionsBank, blankKeys: expectedBlankKeys, answers: answers, onAnswerChange: (key, value) => setAnswers((prev) => ({ ...prev, [key]: value })) }), expectedBlankKeys.length > 0 && bankMode !== 'WITH_OPTIONS' && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-teal-100", children: _jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-teal-600" }) }), _jsxs(CardTitle, { className: "text-base font-semibold text-gray-800", children: ["\u0110\u00E1p \u00E1n ", _jsxs("span", { className: "text-xs font-normal text-gray-500", children: ["(", expectedBlankKeys.length, " ch\u1ED7 tr\u1ED1ng)"] })] })] }) }), _jsxs(CardContent, { className: "px-4 pb-4", children: [_jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4", children: expectedBlankKeys.map((key) => {
|
|
479
|
-
const
|
|
500
|
+
} }, index))) }))] }), externalErrors && externalErrors.length > 0 && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4", children: [_jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-red-100", children: _jsx("span", { className: "text-lg", children: "\u274C" }) }), _jsxs("div", { className: "text-sm", children: [_jsx("p", { className: "font-semibold text-red-800", children: "L\u1ED7i:" }), _jsx("ul", { className: "mt-1 list-inside list-disc text-red-700", children: externalErrors.map((error, i) => _jsx("li", { children: error }, i)) })] })] }))] })] }), showContent && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-teal-100", children: _jsx(ListOrdered, { className: "h-3.5 w-3.5 text-teal-600" }) }), _jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "N\u1ED9i dung c\u1EA5u tr\u00FAc" })] }), expectedBlankKeys.length > 0 && (_jsxs("span", { className: "inline-flex items-center rounded-full border border-teal-200 bg-teal-50 px-2.5 py-0.5 text-xs font-semibold text-teal-700", children: [expectedBlankKeys.length, " ch\u1ED7 tr\u1ED1ng"] }))] }) }), _jsxs(CardContent, { className: "space-y-3 px-4 pb-4", children: [_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 transition-all focus-within:border-teal-300 focus-within:ring-2 focus-within:ring-teal-100", children: [_jsx(EditorToolbar, { editor: editor }), _jsx(EditorContent, { editor: editor })] }), _jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-teal-100 bg-teal-50/60 px-3 py-2.5", children: [_jsx(Info, { className: "mt-0.5 h-4 w-4 shrink-0 text-teal-500" }), _jsxs("p", { className: "text-xs text-teal-700", children: ["Nh\u1EADp ", _jsx("strong", { children: `{0}` }), ", ", _jsx("strong", { children: `{1}` }), ", ", _jsx("strong", { children: `{2}` }), "\u2026 v\u00E0o v\u0103n b\u1EA3n \u0111\u1EC3 t\u1EA1o \u00F4 tr\u1ED1ng. \u0110\u00E1p \u00E1n s\u1EBD t\u1EF1 \u0111\u1ED9ng xu\u1EA5t hi\u1EC7n b\u00EAn d\u01B0\u1EDBi \u0111\u1EC3 b\u1EA1n nh\u1EADp."] })] })] })] })), _jsx(WfsfWordBankEditor, { mode: bankMode, onModeChange: handleBankModeChange, sharedBank: sharedBank, onSharedBankChange: setSharedBank, optionsBank: optionsBank, onOptionsBankChange: setOptionsBank, blankKeys: expectedBlankKeys, answers: answers, onAnswerChange: (key, value) => setAnswers((prev) => ({ ...prev, [key]: value })) }), expectedBlankKeys.length > 0 && bankMode !== 'WITH_OPTIONS' && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-teal-100", children: _jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-teal-600" }) }), _jsxs(CardTitle, { className: "text-base font-semibold text-gray-800", children: ["\u0110\u00E1p \u00E1n ", _jsxs("span", { className: "text-xs font-normal text-gray-500", children: ["(", expectedBlankKeys.length, " ch\u1ED7 tr\u1ED1ng)"] })] })] }) }), _jsxs(CardContent, { className: "px-4 pb-4", children: [_jsx("p", { className: "mb-3 text-xs text-gray-500", children: "M\u1ED7i ch\u1ED7 tr\u1ED1ng c\u00F3 th\u1EC3 c\u00F3 nhi\u1EC1u \u0111\u00E1p \u00E1n \u0111\u00FAng. H\u1ECDc vi\u00EAn \u0111\u00FAng n\u1EBFu kh\u1EDBp m\u1ED9t trong c\u00E1c \u0111\u00E1p \u00E1n." }), _jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4", children: expectedBlankKeys.map((key) => {
|
|
501
|
+
const alts = answers[key]?.length ? answers[key] : [''];
|
|
502
|
+
const isEmpty = !hasWfsfBlankAnswer(alts);
|
|
480
503
|
return (_jsxs("div", { className: `rounded-xl border-2 p-3 transition-all duration-200 ${isEmpty
|
|
481
504
|
? 'border-gray-200 bg-white'
|
|
482
|
-
: 'border-teal-200 bg-gradient-to-br from-teal-50 to-cyan-50 shadow-sm shadow-teal-100'}`, children: [_jsxs(Label, { className: "mb-1.5 flex items-center justify-between text-sm font-semibold text-gray-700", children: [_jsxs("span", { children: ["Ch\u1ED7 tr\u1ED1ng ", _jsx("code", { className: "rounded bg-gray-100 px-1 text-teal-700", children: `{${key}}` })] }), isEmpty && (_jsx("span", { className: "text-[10px] font-normal italic text-red-500", children: "*B\u1EAFt bu\u1ED9c" }))] }), _jsx(
|
|
483
|
-
}) }), expectedBlankKeys.some(k => !answers[k]
|
|
505
|
+
: 'border-teal-200 bg-gradient-to-br from-teal-50 to-cyan-50 shadow-sm shadow-teal-100'}`, children: [_jsxs(Label, { className: "mb-1.5 flex items-center justify-between text-sm font-semibold text-gray-700", children: [_jsxs("span", { children: ["Ch\u1ED7 tr\u1ED1ng ", _jsx("code", { className: "rounded bg-gray-100 px-1 text-teal-700", children: `{${key}}` })] }), isEmpty && (_jsx("span", { className: "text-[10px] font-normal italic text-red-500", children: "*B\u1EAFt bu\u1ED9c" }))] }), _jsx("div", { className: "space-y-1.5", children: alts.map((alt, altIndex) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Input, { value: alt, onChange: (e) => updateAnswerAlt(key, altIndex, e.target.value), placeholder: altIndex === 0 ? 'Đáp án đúng...' : `Đáp án thay thế ${altIndex + 1}`, className: `border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100 ${!isEmpty ? 'border-teal-200' : ''}` }), alts.length > 1 && (_jsx("button", { type: "button", title: "X\u00F3a \u0111\u00E1p \u00E1n n\u00E0y", onClick: () => removeAnswerAlt(key, altIndex), className: "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-gray-400 hover:bg-rose-50 hover:text-rose-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, `${key}-${altIndex}`))) }), _jsxs("button", { type: "button", onClick: () => addAnswerAlt(key), className: "mt-2 inline-flex items-center gap-1 text-[11px] font-medium text-teal-700 hover:text-teal-800", children: [_jsx(Plus, { className: "h-3 w-3" }), "Th\u00EAm \u0111\u00E1p \u00E1n \u0111\u00FAng"] })] }, key));
|
|
506
|
+
}) }), expectedBlankKeys.some(k => !hasWfsfBlankAnswer(answers[k])) && (_jsxs("div", { className: "mt-4 flex items-start gap-3 rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-4", children: [_jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-amber-100", children: _jsx("span", { className: "text-lg", children: "\u26A0\uFE0F" }) }), _jsxs("div", { className: "text-sm text-amber-800", children: [_jsx("p", { className: "font-semibold", children: "C\u00F2n ch\u1ED7 tr\u1ED1ng ch\u01B0a c\u00F3 \u0111\u00E1p \u00E1n" }), _jsxs("p", { className: "mt-0.5 text-amber-700", children: ["Vui l\u00F2ng \u0111i\u1EC1n \u0111\u00E1p \u00E1n \u0111\u00FAng cho t\u1EA5t c\u1EA3 c\u00E1c \u00F4 \u0111\u01B0\u1EE3c \u0111\u00E1nh d\u1EA5u ", _jsx("span", { className: "text-red-500 font-medium", children: "*B\u1EAFt bu\u1ED9c" }), "."] })] })] }))] })] })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", 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-amber-500 to-orange-500 shadow-sm", children: _jsx(BookOpen, { className: "h-3.5 w-3.5 text-white" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" }), _jsx("span", { className: "inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-600", children: "Kh\u00F4ng b\u1EAFt bu\u1ED9c" })] }), _jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: "Gi\u1EA3i th\u00EDch cho \u0111\u00E1p \u00E1n \u0111\u00FAng \u2014 hi\u1EC3n th\u1ECB sau khi h\u1ECDc sinh ho\u00E0n th\u00E0nh c\u00E2u h\u1ECFi" })] })] }) }), _jsx(CardContent, { className: "px-4 pb-4", children: _jsx(Textarea, { value: explanation, onChange: (e) => setExplanation(e.target.value), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", rows: 3, className: "min-h-[80px] resize-none border-gray-200 bg-white transition-all focus:border-amber-300 focus:ring-amber-200" }) })] })] }));
|
|
484
507
|
}
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
+
import { normalizeWfsfAnswersMap } from '../../../../shared/lib/utils/wfsf-blank-match';
|
|
1
2
|
import { detectWfsfBankMode, normalizeOptionsWordBank, normalizeSharedWordBank, } from './wfsf-word-bank.utils';
|
|
2
3
|
function asRecord(value) {
|
|
3
4
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
4
5
|
? value
|
|
5
6
|
: {};
|
|
6
7
|
}
|
|
7
|
-
function asStringAnswers(value) {
|
|
8
|
-
const record = asRecord(value);
|
|
9
|
-
const answers = {};
|
|
10
|
-
for (const [key, entry] of Object.entries(record)) {
|
|
11
|
-
if (typeof entry === 'string')
|
|
12
|
-
answers[key] = entry;
|
|
13
|
-
}
|
|
14
|
-
return answers;
|
|
15
|
-
}
|
|
16
8
|
function asBlankStyle(value) {
|
|
17
9
|
if (value === 'letter' ||
|
|
18
10
|
value === 'word' ||
|
|
@@ -97,7 +89,7 @@ export function mapQuestionToWfsfData(question) {
|
|
|
97
89
|
audioUrl: audioUrls[0] || String(apiContent.audioUrl || answer.audioUrl || source.audio_url || ''),
|
|
98
90
|
audioUrls,
|
|
99
91
|
content,
|
|
100
|
-
answers:
|
|
92
|
+
answers: normalizeWfsfAnswersMap(correctAnswer.answers || answer.answers),
|
|
101
93
|
caseSensitive: Boolean(correctAnswer.caseSensitive ?? answer.caseSensitive ?? false),
|
|
102
94
|
blankStyle: asBlankStyle(apiContent.blankStyle ?? answer.blankStyle),
|
|
103
95
|
viewMode,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compactWfsfAnswersMap } from '../../../../shared/lib/utils/wfsf-blank-match';
|
|
1
2
|
/**
|
|
2
3
|
* Transform WORD_FILL_STRUCTURED_FORM creator data → API format
|
|
3
4
|
*
|
|
@@ -57,7 +58,7 @@ export const transformWordFillStructuredForm = (question) => {
|
|
|
57
58
|
apiContent.wordBank = answerData.wordBank;
|
|
58
59
|
}
|
|
59
60
|
const apiCorrectAnswer = {
|
|
60
|
-
answers: answerData.answers,
|
|
61
|
+
answers: compactWfsfAnswersMap((answerData.answers || {})),
|
|
61
62
|
caseSensitive: answerData.caseSensitive ?? false,
|
|
62
63
|
};
|
|
63
64
|
const basicGroupData = hasGroupData
|
|
@@ -1,49 +1,67 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
4
4
|
import { Check, X, ArrowUpDown, Lightbulb } from 'lucide-react';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
6
|
import { cn } from '../../../../shared/lib/utils';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// Remove used words from available pool
|
|
16
|
-
const remaining = [...words];
|
|
17
|
-
initial.forEach((w) => {
|
|
18
|
-
const idx = remaining.indexOf(w);
|
|
19
|
-
if (idx >= 0)
|
|
20
|
-
remaining.splice(idx, 1);
|
|
21
|
-
});
|
|
22
|
-
return remaining;
|
|
7
|
+
function toWordList(value) {
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value.filter((item) => typeof item === 'string');
|
|
10
|
+
}
|
|
11
|
+
if (value && typeof value === 'object') {
|
|
12
|
+
const nested = value.answer;
|
|
13
|
+
if (Array.isArray(nested)) {
|
|
14
|
+
return nested.filter((item) => typeof item === 'string');
|
|
23
15
|
}
|
|
24
|
-
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
function resolveSelectedWords(userAnswer, answer, autoFillCorrectAnswers) {
|
|
20
|
+
if (Array.isArray(userAnswer)) {
|
|
21
|
+
return toWordList(userAnswer);
|
|
22
|
+
}
|
|
23
|
+
const nested = toWordList(userAnswer);
|
|
24
|
+
if (nested.length > 0) {
|
|
25
|
+
return nested;
|
|
26
|
+
}
|
|
27
|
+
return autoFillCorrectAnswers ? answer : [];
|
|
28
|
+
}
|
|
29
|
+
function remainingWords(words, selected) {
|
|
30
|
+
const remaining = [...words];
|
|
31
|
+
selected.forEach((word) => {
|
|
32
|
+
const idx = remaining.indexOf(word);
|
|
33
|
+
if (idx >= 0)
|
|
34
|
+
remaining.splice(idx, 1);
|
|
25
35
|
});
|
|
36
|
+
return remaining;
|
|
37
|
+
}
|
|
38
|
+
export function WordOrderingClient({ questionData, onSubmit, isReviewMode = false, userAnswer, autoFillCorrectAnswers = false, explanation, }) {
|
|
39
|
+
const words = useMemo(() => toWordList(questionData.words), [questionData.words]);
|
|
40
|
+
const answer = useMemo(() => toWordList(questionData.answer), [questionData.answer]);
|
|
41
|
+
const { isExample, imageUrl } = questionData;
|
|
42
|
+
const initialSelected = resolveSelectedWords(userAnswer, answer, autoFillCorrectAnswers);
|
|
43
|
+
const [selectedWords, setSelectedWords] = useState(initialSelected);
|
|
44
|
+
const [availableWords, setAvailableWords] = useState(() => remainingWords(words, initialSelected));
|
|
26
45
|
const { previewUrl: imagePreviewUrl, isLoading } = usePresignedFileUrl(imageUrl || '');
|
|
27
|
-
// Sync with userAnswer changes
|
|
28
46
|
useEffect(() => {
|
|
29
|
-
if (userAnswer)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
if (userAnswer == null)
|
|
48
|
+
return;
|
|
49
|
+
if (!Array.isArray(userAnswer) && toWordList(userAnswer).length === 0)
|
|
50
|
+
return;
|
|
51
|
+
const next = resolveSelectedWords(userAnswer, answer, false);
|
|
52
|
+
setSelectedWords(next);
|
|
53
|
+
setAvailableWords(remainingWords(words, next));
|
|
54
|
+
}, [userAnswer, words, answer]);
|
|
55
|
+
const wordsString = JSON.stringify(words);
|
|
56
|
+
const hasUserAnswer = Array.isArray(userAnswer)
|
|
57
|
+
? true
|
|
58
|
+
: toWordList(userAnswer).length > 0;
|
|
41
59
|
useEffect(() => {
|
|
42
|
-
if (!
|
|
60
|
+
if (!hasUserAnswer && !autoFillCorrectAnswers) {
|
|
43
61
|
setSelectedWords([]);
|
|
44
62
|
setAvailableWords([...words]);
|
|
45
63
|
}
|
|
46
|
-
}, [
|
|
64
|
+
}, [wordsString, hasUserAnswer, autoFillCorrectAnswers, words]);
|
|
47
65
|
const handleSelectWord = useCallback((word, poolIndex) => {
|
|
48
66
|
if (isReviewMode)
|
|
49
67
|
return;
|
|
@@ -18,7 +18,6 @@ import { WriteAShortLetterClient } from '../types/write-a-short-letter/WriteASho
|
|
|
18
18
|
import { LookPictureChooseCorrectAnswerClient } from '../types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient';
|
|
19
19
|
import { LookPictureFillBlankChooseAnswerClient } from '../types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient';
|
|
20
20
|
import { FillMissingWordsInGridClient } from '../types/fill-missing-words-in-grid/FillMissingWordsInGridClient';
|
|
21
|
-
import { WordFillStructuredFormClient } from '../types/word-fill-structured-form/WordFillStructuredFormClient';
|
|
22
21
|
import { CrosswordPuzzleClient } from '../types/crossword-puzzle/CrosswordPuzzleClient';
|
|
23
22
|
import { FindWordsInMatrixClient } from '../types/find-words-in-matrix/FindWordsInMatrixClient';
|
|
24
23
|
import { SortWordsIntoCategoriesViewer } from './SortWordsIntoCategoriesViewer';
|
|
@@ -32,6 +31,9 @@ import { FillInBlankGroupViewer } from './FillInBlankGroupViewer';
|
|
|
32
31
|
import { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
|
|
33
32
|
import { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
|
|
34
33
|
import { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
|
|
34
|
+
import { SpontaneousQAClient } from '../types/spontaneous-qa/SpontaneousQAClient';
|
|
35
|
+
import { mapQuestionToSpontaneousQAData } from '../types/spontaneous-qa/map-spontaneous-qa-data';
|
|
36
|
+
import { SpeakingCueCardClient, mapQuestionToSpeakingCueCardData, } from '../types/speaking-cue-card/SpeakingCueCardClient';
|
|
35
37
|
import { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
36
38
|
import { AnswerTheQuestionViewer } from './AnswerTheQuestionViewer';
|
|
37
39
|
import { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
@@ -514,72 +516,6 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
514
516
|
onSubmitAnswer?.(answers);
|
|
515
517
|
} }));
|
|
516
518
|
};
|
|
517
|
-
const renderWordFillStructuredForm = () => {
|
|
518
|
-
const content = (question.content && typeof question.content === 'object'
|
|
519
|
-
? question.content
|
|
520
|
-
: {});
|
|
521
|
-
// Pearson preview maps correctAnswer → answer; bank may use either shape.
|
|
522
|
-
const answer = (question.answer ??
|
|
523
|
-
question.correctAnswer ??
|
|
524
|
-
{});
|
|
525
|
-
const answers = answer?.answers && typeof answer.answers === 'object' && !Array.isArray(answer.answers)
|
|
526
|
-
? answer.answers
|
|
527
|
-
: {};
|
|
528
|
-
const html = String(content.content || '');
|
|
529
|
-
const blankRaw = String(content.blankStyle || '').toLowerCase();
|
|
530
|
-
const blankStyle = blankRaw === 'letter' || blankRaw === 'box'
|
|
531
|
-
? 'letter'
|
|
532
|
-
: blankRaw === 'phrase' || blankRaw === 'sentence'
|
|
533
|
-
? 'phrase'
|
|
534
|
-
: 'word';
|
|
535
|
-
const userMap = userAnswer && typeof userAnswer === 'object' && !Array.isArray(userAnswer)
|
|
536
|
-
? userAnswer
|
|
537
|
-
: selectedAnswer && typeof selectedAnswer === 'object' && !Array.isArray(selectedAnswer)
|
|
538
|
-
? selectedAnswer
|
|
539
|
-
: isReviewMode
|
|
540
|
-
? answers
|
|
541
|
-
: {};
|
|
542
|
-
const qNum = typeof question.questionNumber === 'number' && question.questionNumber > 0
|
|
543
|
-
? question.questionNumber
|
|
544
|
-
: undefined;
|
|
545
|
-
const rawWordBank = Array.isArray(content.wordBank) ? content.wordBank : undefined;
|
|
546
|
-
const wordBank = rawWordBank?.map((entry) => {
|
|
547
|
-
if (entry == null || typeof entry !== 'object')
|
|
548
|
-
return null;
|
|
549
|
-
const rec = entry;
|
|
550
|
-
if (Array.isArray(rec.options)) {
|
|
551
|
-
const options = rec.options.map((o) => String(o ?? '').trim()).filter(Boolean);
|
|
552
|
-
return options.length >= 2 ? { options } : null;
|
|
553
|
-
}
|
|
554
|
-
const word = String(rec.word ?? '').trim();
|
|
555
|
-
if (word)
|
|
556
|
-
return rec.isExample ? { word, isExample: true } : { word };
|
|
557
|
-
return null;
|
|
558
|
-
});
|
|
559
|
-
const hasWordOptions = Boolean(wordBank?.some((e) => e != null &&
|
|
560
|
-
'options' in e &&
|
|
561
|
-
Array.isArray(e.options) &&
|
|
562
|
-
(e.options?.length ?? 0) >= 2));
|
|
563
|
-
const hasSharedWords = Boolean(wordBank?.some((e) => e != null && 'word' in e && e.word));
|
|
564
|
-
const viewMode = String(content.viewMode || '').trim() ||
|
|
565
|
-
(hasWordOptions ? 'WITH_OPTIONS' : hasSharedWords ? 'WITH_WORD_BANK' : undefined);
|
|
566
|
-
return (_jsx(WordFillStructuredFormClient, { questionData: {
|
|
567
|
-
title: String(content.title || ''),
|
|
568
|
-
imageUrl: content.imageUrl ? String(content.imageUrl) : undefined,
|
|
569
|
-
content: html,
|
|
570
|
-
blankStyle,
|
|
571
|
-
answers,
|
|
572
|
-
caseSensitive: Boolean(answer?.caseSensitive),
|
|
573
|
-
explanation: question.explanation,
|
|
574
|
-
...(viewMode ? { viewMode } : {}),
|
|
575
|
-
...((hasWordOptions || hasSharedWords) && wordBank ? { wordBank } : {}),
|
|
576
|
-
}, userAnswer: userMap, isReviewMode: isReviewMode, autoFillCorrectAnswers: isReviewMode && Object.keys(userMap).length === 0, explanation: question.explanation, questionNumber: qNum, onAnswerChange: (key, value) => {
|
|
577
|
-
const prev = selectedAnswer && typeof selectedAnswer === 'object' && !Array.isArray(selectedAnswer)
|
|
578
|
-
? selectedAnswer
|
|
579
|
-
: {};
|
|
580
|
-
setSelectedAnswer({ ...prev, [key]: value });
|
|
581
|
-
} }));
|
|
582
|
-
};
|
|
583
519
|
const renderWordFillParagraph = () => {
|
|
584
520
|
const content = (question.content && typeof question.content === 'object'
|
|
585
521
|
? question.content
|
|
@@ -633,7 +569,11 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
633
569
|
return (_jsxs("div", { className: "space-y-3", children: [content.title ? _jsx("h3", { className: "text-center text-lg font-bold uppercase text-slate-800", children: content.title }) : null, content.imageUrl ? (_jsx("div", { className: "flex justify-center", children: _jsx("img", { src: String(content.imageUrl), alt: "", className: "max-h-[180px] rounded-lg object-contain" }) })) : null, _jsx("div", { className: "space-y-2 text-base leading-relaxed text-slate-800", children: paragraph.split('\n').map((line, li) => (_jsx("div", { className: "flex flex-wrap items-baseline", children: renderLine(line, `wfp-${li}`) }, `wfp-l${li}`))) }), isReviewMode && correctAnswers.length > 0 ? (_jsxs("div", { className: "rounded-lg border border-emerald-200 bg-emerald-50/80 p-2.5 text-sm text-emerald-800", children: [_jsx("span", { className: "font-semibold", children: "\u0110\u00E1p \u00E1n: " }), correctAnswers.map((a, i) => (_jsxs("span", { className: "mr-2", children: ["(", i + 1, ") ", _jsx("span", { className: "font-bold underline", children: a || '—' })] }, `wfp-a${i}`)))] })) : null] }));
|
|
634
570
|
};
|
|
635
571
|
const renderQuestion = () => {
|
|
636
|
-
|
|
572
|
+
const resolvedType = String(question.type ||
|
|
573
|
+
question.questionType ||
|
|
574
|
+
question.question_type ||
|
|
575
|
+
'');
|
|
576
|
+
switch (resolvedType) {
|
|
637
577
|
case 'CHOOSE_THE_CORRECT_ANSWER':
|
|
638
578
|
return renderChooseTheCorrectAnswer();
|
|
639
579
|
case 'CHOOSE_THE_CORRECT_ANSWER_GROUP':
|
|
@@ -654,6 +594,10 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
654
594
|
return (_jsx(AnswerTheQuestionViewer, { question: question, isReviewMode: isReviewMode, userAnswer: typeof userAnswer === 'string' ? userAnswer : undefined, onSubmitAnswer: onSubmitAnswer }));
|
|
655
595
|
case 'ANSWER_THE_QUESTION_GROUP':
|
|
656
596
|
return (_jsx(AnswerTheQuestionGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
597
|
+
case 'SPEAKING_CUE_CARD':
|
|
598
|
+
return (_jsx(SpeakingCueCardClient, { questionData: mapQuestionToSpeakingCueCardData(question), isReviewMode: isReviewMode }));
|
|
599
|
+
case 'SPONTANEOUS_QA':
|
|
600
|
+
return (_jsx(SpontaneousQAClient, { questionData: mapQuestionToSpontaneousQAData(question), isReviewMode: isReviewMode }));
|
|
657
601
|
case 'SPONTANEOUS_QA_GROUP':
|
|
658
602
|
return (_jsx(SpontaneousQaGroupViewer, { question: question, isReviewMode: isReviewMode }));
|
|
659
603
|
case 'WORD_ORDER_AND_MATCH_GROUP':
|