@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.
Files changed (60) hide show
  1. package/dist/api/exam-taking.types.d.ts +1 -0
  2. package/dist/components/exams/ExamPreviewDialog.js +4 -2
  3. package/dist/components/exams/take/components/QuestionRenderer.js +2 -2
  4. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +3 -1
  5. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +5 -2
  6. package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +13 -20
  7. package/dist/components/exams/take/exam-taking.utils.d.ts +3 -2
  8. package/dist/components/exams/take/exam-taking.utils.js +3 -0
  9. package/dist/components/exams/take/utils/question-transformers.d.ts +11 -3
  10. package/dist/components/exams/take/utils/question-transformers.js +45 -2
  11. package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.d.ts +6 -2
  12. package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +26 -5
  13. package/dist/components/questions/_shared/types/fill-in-blank.type.d.ts +9 -0
  14. package/dist/components/questions/_shared/types/question-group.type.d.ts +11 -0
  15. package/dist/components/questions/_shared/types/question-group.type.js +4 -1
  16. package/dist/components/questions/_shared/types/spontaneous-qa.type.d.ts +7 -0
  17. package/dist/components/questions/_shared/types/word-fill-structured-form.type.d.ts +1 -1
  18. package/dist/components/questions/groups/ImageLabelGroupCard.d.ts +15 -0
  19. package/dist/components/questions/groups/ImageLabelGroupCard.js +42 -0
  20. package/dist/components/questions/groups/ImageLabelWordBank.d.ts +10 -0
  21. package/dist/components/questions/groups/ImageLabelWordBank.js +17 -0
  22. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +33 -9
  23. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +6 -2
  24. package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +28 -5
  25. package/dist/components/questions/types/fill-in-blank/register.js +5 -0
  26. package/dist/components/questions/types/fill-in-blank/transform.js +4 -1
  27. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.d.ts +11 -0
  28. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +27 -0
  29. package/dist/components/questions/types/spontaneous-qa/SpontaneousQAClient.d.ts +2 -0
  30. package/dist/components/questions/types/spontaneous-qa/SpontaneousQAClient.js +15 -0
  31. package/dist/components/questions/types/spontaneous-qa/map-spontaneous-qa-data.d.ts +2 -0
  32. package/dist/components/questions/types/spontaneous-qa/map-spontaneous-qa-data.js +33 -0
  33. package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.d.ts +2 -2
  34. package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.js +15 -5
  35. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +39 -15
  36. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +35 -12
  37. package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +2 -10
  38. package/dist/components/questions/types/word-fill-structured-form/transform.js +2 -1
  39. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +51 -33
  40. package/dist/components/questions/viewer/QuestionViewer.js +12 -68
  41. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +3 -1
  42. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +4 -2
  43. package/dist/components/results/renderers/review-question-body-dedicated.js +2 -2
  44. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +12 -3
  45. package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +13 -20
  46. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +8 -0
  47. package/dist/shared/constants/routes.d.ts +2 -2
  48. package/dist/shared/constants/routes.js +3 -3
  49. package/dist/shared/lib/rich-text.d.ts +4 -0
  50. package/dist/shared/lib/rich-text.js +25 -0
  51. package/dist/shared/lib/stores/examQuestionStore.d.ts +4 -0
  52. package/dist/shared/lib/utils/question-transform-types.d.ts +4 -0
  53. package/dist/shared/lib/utils/question-transform.js +1 -1
  54. package/dist/shared/lib/utils/wfsf-blank-match.d.ts +9 -0
  55. package/dist/shared/lib/utils/wfsf-blank-match.js +60 -0
  56. package/package.json +1 -1
  57. package/dist/components/themes/cambridge-yle/assets/images/bg-title-old.png +0 -0
  58. package/dist/components/themes/cambridge-yle/assets/images/border-old.png +0 -0
  59. package/dist/components/themes/cambridge-yle/assets/images/close_old.png +0 -0
  60. 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] === removed)
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 l\u00E0m \u0111\u00E1p \u00E1n \u0111\u00FAng", onClick: () => option.trim() && onAnswerChange(key, option.trim()), className: `flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-xs font-bold ${selected === option.trim() && option.trim()
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
  })) })] }))] }));
@@ -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 { ...answers };
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 correctAnswer = answers[key] || '';
157
- const normalizedFilled = caseSensitive ? filled.trim() : filled.trim().toLowerCase();
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: "", className: cn('inline-block min-w-[90px] max-w-[200px] rounded-t border-b-2 bg-slate-50/40 px-2 py-0.5 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', isReviewMode
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') }), isReviewMode && filled && (isCorrectFill
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
  }
@@ -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] || answers[key].trim() === '')
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 ans = (answers[key] || '').trim();
436
- if (ans && !opts.includes(ans))
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]?.trim()), 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')
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 isEmpty = !answers[key] || answers[key].trim() === '';
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(Input, { value: answers[key] || '', onChange: e => setAnswers(prev => ({ ...prev, [key]: e.target.value })), placeholder: "\u0110\u00E1p \u00E1n \u0111\u00FAng...", className: `border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100 ${!isEmpty ? 'border-teal-200' : ''}` }), !isEmpty && (_jsxs("span", { className: "mt-1.5 inline-flex items-center gap-1 rounded-full bg-teal-100 px-2 py-0.5 text-[10px] font-semibold text-teal-700", children: [_jsx(CheckCircle2, { className: "h-3 w-3" }), "\u0110\u00E3 nh\u1EADp"] }))] }, key));
483
- }) }), expectedBlankKeys.some(k => !answers[k]?.trim()) && (_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" }) })] })] }));
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: asStringAnswers(correctAnswer.answers || answer.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
- export function WordOrderingClient({ questionData, onSubmit, isReviewMode = false, userAnswer, autoFillCorrectAnswers = false, explanation, }) {
8
- const { words = [], answer = [], isExample, imageUrl } = questionData;
9
- // State: the user's current arrangement
10
- const [selectedWords, setSelectedWords] = useState(userAnswer || (autoFillCorrectAnswers ? answer : []));
11
- // Words remaining in the pool
12
- const [availableWords, setAvailableWords] = useState(() => {
13
- const initial = userAnswer || (autoFillCorrectAnswers ? answer : []);
14
- if (initial.length > 0) {
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
- return [...words];
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
- setSelectedWords(userAnswer);
31
- const remaining = [...words];
32
- userAnswer.forEach((w) => {
33
- const idx = remaining.indexOf(w);
34
- if (idx >= 0)
35
- remaining.splice(idx, 1);
36
- });
37
- setAvailableWords(remaining);
38
- }
39
- }, [userAnswer]);
40
- // Reset when words change (question navigation)
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 (!userAnswer && !autoFillCorrectAnswers) {
60
+ if (!hasUserAnswer && !autoFillCorrectAnswers) {
43
61
  setSelectedWords([]);
44
62
  setAvailableWords([...words]);
45
63
  }
46
- }, [JSON.stringify(words)]);
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
- switch (question.type) {
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':