@tinyweb_dev/oe-exam-sdk 1.0.1 → 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 (50) hide show
  1. package/dist/api/exam-questions.d.ts +1 -1
  2. package/dist/api/exam-questions.js +1 -39
  3. package/dist/api/exam-questions.types.d.ts +0 -28
  4. package/dist/components/exams/ExamCreator.d.ts +1 -7
  5. package/dist/components/exams/ExamCreator.js +21 -57
  6. package/dist/components/exams/ExamQuestionsPageContainer.d.ts +0 -5
  7. package/dist/components/exams/ExamQuestionsPageContainer.js +2 -2
  8. package/dist/components/exams/api.d.ts +1 -1
  9. package/dist/components/exams/index.d.ts +1 -1
  10. package/dist/components/exams/take/components/QuestionRenderer.d.ts +1 -6
  11. package/dist/components/exams/take/components/QuestionRenderer.js +2 -2
  12. package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.d.ts +1 -7
  13. package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +22 -38
  14. package/dist/components/exams/take/exam-taking.utils.d.ts +2 -2
  15. package/dist/components/exams/take/exam-taking.utils.js +2 -0
  16. package/dist/components/exams/take/index.d.ts +0 -6
  17. package/dist/components/exams/take/index.js +0 -5
  18. package/dist/components/exams/take/utils/question-transformers.d.ts +2 -2
  19. package/dist/components/exams/take/utils/question-transformers.js +4 -25
  20. package/dist/components/questions/_shared/types/word-fill-structured-form.type.d.ts +1 -1
  21. package/dist/components/questions/question-bank/index.d.ts +0 -1
  22. package/dist/components/questions/question-bank/index.js +0 -1
  23. package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.d.ts +2 -2
  24. package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.js +15 -5
  25. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +39 -15
  26. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +35 -12
  27. package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +3 -18
  28. package/dist/components/questions/types/word-fill-structured-form/transform.js +2 -5
  29. package/dist/components/questions/viewer/QuestionViewer.js +0 -67
  30. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +12 -3
  31. package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +13 -20
  32. package/dist/index.d.ts +3 -4
  33. package/dist/index.js +1 -2
  34. package/dist/shared/constants/ApiConstant.d.ts +0 -1
  35. package/dist/shared/constants/ApiConstant.js +0 -1
  36. package/dist/shared/constants/routes.d.ts +2 -2
  37. package/dist/shared/constants/routes.js +3 -3
  38. package/dist/shared/lib/utils/wfsf-blank-match.d.ts +9 -0
  39. package/dist/shared/lib/utils/wfsf-blank-match.js +60 -0
  40. package/package.json +1 -1
  41. package/dist/components/questions/question-bank/QuestionBankPickerDialog.d.ts +0 -45
  42. package/dist/components/questions/question-bank/QuestionBankPickerDialog.js +0 -209
  43. package/dist/components/questions/question-bank/QuestionBankPickerRow.d.ts +0 -9
  44. package/dist/components/questions/question-bank/QuestionBankPickerRow.js +0 -20
  45. package/dist/components/questions/question-bank/question-bank-picker.utils.d.ts +0 -55
  46. package/dist/components/questions/question-bank/question-bank-picker.utils.js +0 -408
  47. package/dist/components/themes/cambridge-yle/assets/images/bg-title-old.png +0 -0
  48. package/dist/components/themes/cambridge-yle/assets/images/border-old.png +0 -0
  49. package/dist/components/themes/cambridge-yle/assets/images/close_old.png +0 -0
  50. package/dist/components/themes/cambridge-yle/assets/images/frame-old.png +0 -0
@@ -1,10 +1,5 @@
1
1
  export { ExamTakingPageContainer } from './ExamTakingPageContainer';
2
2
  export { GenericQuestionRenderer } from './GenericQuestionRenderer';
3
- /** Per-type dispatcher used by ExamTakingPageContainer and host lite flows (BTVN). */
4
- export { QuestionRenderer } from './components/QuestionRenderer';
5
- export { groupQuestionsByPart } from './utils/question-transformers';
6
- /** Normalize UI answer map → API answer payloads (option index wrap, group composites). */
7
- export { transformAnswersForSubmission } from './utils/answer-transformers';
8
3
  export { useExamCountdown } from './hooks/useExamCountdown';
9
4
  export { useAnswerAutosave } from './hooks/useAnswerAutosave';
10
5
  export { useProctoringSnapshot } from './hooks/useProctoringSnapshot';
@@ -395,7 +395,7 @@ export declare function transformWriteSentences(questions: ApiQuestion[]): {
395
395
  title?: string;
396
396
  groupContent?: string;
397
397
  };
398
- export type WfsfBlankStyle = 'letter' | 'word' | 'phrase';
398
+ export type WfsfBlankStyle = 'letter' | 'word' | 'phrase' | 'translate';
399
399
  export type WfsfWordBankEntry = {
400
400
  options: string[];
401
401
  } | {
@@ -408,7 +408,7 @@ export interface WordFillStructuredFormQuestion {
408
408
  title: string;
409
409
  imageUrl?: string;
410
410
  content: string;
411
- answers: Record<string, string>;
411
+ answers: Record<string, string | string[]>;
412
412
  caseSensitive: boolean;
413
413
  /** letter = square boxes; word / phrase = underline width */
414
414
  blankStyle: WfsfBlankStyle;
@@ -1209,6 +1209,8 @@ function normalizeWfsfBlankStyle(raw) {
1209
1209
  return 'letter';
1210
1210
  if (v === 'phrase' || v === 'sentence')
1211
1211
  return 'phrase';
1212
+ if (v === 'translate')
1213
+ return 'translate';
1212
1214
  return 'word';
1213
1215
  }
1214
1216
  function normalizeWfsfWordBank(raw) {
@@ -1240,20 +1242,6 @@ function normalizeWfsfWordBank(raw) {
1240
1242
  export function transformWordFillStructuredForm(questions) {
1241
1243
  const firstQuestion = questions[0];
1242
1244
  const groupPayload = firstQuestion?.questionGroup?.payload;
1243
- const firstContent = firstQuestion?.content;
1244
- const firstMetaRaw = firstQuestion?.meta;
1245
- const firstMeta = firstMetaRaw &&
1246
- typeof firstMetaRaw === 'object' &&
1247
- !Array.isArray(firstMetaRaw)
1248
- ? firstMetaRaw
1249
- : undefined;
1250
- // Bank: meta.instruction; school: content.instruction; exam group: payload.instructions.
1251
- const topLevelInstruction = typeof firstQuestion?.instruction === 'string'
1252
- ? String(firstQuestion.instruction).trim()
1253
- : '';
1254
- const metaInstruction = String(firstMeta?.instruction || '').trim();
1255
- const contentInstruction = String(firstContent?.instruction || firstContent?.instructions || '').trim();
1256
- const groupInstruction = String(groupPayload?.instructions || groupPayload?.instruction || '').trim();
1257
1245
  const transformed = questions.map((q) => {
1258
1246
  const content = q.content;
1259
1247
  const correctAnswer = q.correct_answer || {};
@@ -1273,16 +1261,11 @@ export function transformWordFillStructuredForm(questions) {
1273
1261
  const hasWords = Boolean(wordBank?.some((e) => e != null && 'word' in e && e.word));
1274
1262
  const viewMode = content?.viewMode ||
1275
1263
  (hasOptions ? 'WITH_OPTIONS' : hasWords ? 'WITH_WORD_BANK' : undefined);
1276
- // Empty string imageUrl breaks ResolvedImage in some hosts — treat as absent.
1277
- const imageUrlRaw = content?.imageUrl;
1278
- const imageUrl = typeof imageUrlRaw === 'string' && imageUrlRaw.trim().length > 0
1279
- ? imageUrlRaw.trim()
1280
- : undefined;
1281
1264
  return {
1282
1265
  id: q.id,
1283
1266
  questionNumber: q.question_number,
1284
1267
  title: content?.title || '',
1285
- imageUrl,
1268
+ imageUrl: content?.imageUrl,
1286
1269
  content: resolvedContent,
1287
1270
  answers: correctAnswer?.answers || {},
1288
1271
  caseSensitive: correctAnswer?.caseSensitive ?? content?.caseSensitive ?? false,
@@ -1293,11 +1276,7 @@ export function transformWordFillStructuredForm(questions) {
1293
1276
  });
1294
1277
  return {
1295
1278
  questions: transformed,
1296
- instruction: groupInstruction ||
1297
- contentInstruction ||
1298
- topLevelInstruction ||
1299
- metaInstruction ||
1300
- 'COMPLETE THE FORM BELOW',
1279
+ instruction: groupPayload?.instructions || 'COMPLETE THE FORM BELOW',
1301
1280
  groupTitle: groupPayload?.title,
1302
1281
  groupContent: groupPayload?.content,
1303
1282
  groupImageUrl: groupPayload?.imageUrl,
@@ -22,7 +22,7 @@ export interface WordFillStructuredFormData {
22
22
  audioUrl?: string;
23
23
  audioUrls?: string[];
24
24
  content: string;
25
- answers: Record<string, string>;
25
+ answers: Record<string, string[]>;
26
26
  /**
27
27
  * letter = square 1-char box; word = single-word underline; phrase = multi-word; translate = LLM grading.
28
28
  * Default when omitted: word.
@@ -1,5 +1,4 @@
1
1
  export { EditQuestionBankDialog } from './EditQuestionBankDialog';
2
2
  export { QuestionEditorRenderer, QuestionBankEditorType } from './QuestionEditorRenderer';
3
- export { QuestionBankPickerDialog, type QuestionBankLessonSuggestion, type QuestionBankPickerDefaultFilters, type QuestionBankPickerDialogProps, } from './QuestionBankPickerDialog';
4
3
  export { DifficultyLevel, DIFFICULTY_LEVELS, parseDifficultyLevel } from './difficulty';
5
4
  export type { EditQuestionBankDialogProps, QuestionBankEditItem, QuestionEditorChangeData, QuestionEditorRendererProps, SkillType, UpdateQuestionBankDto, } from './types';
@@ -1,4 +1,3 @@
1
1
  export { EditQuestionBankDialog } from './EditQuestionBankDialog';
2
2
  export { QuestionEditorRenderer, QuestionBankEditorType } from './QuestionEditorRenderer';
3
- export { QuestionBankPickerDialog, } from './QuestionBankPickerDialog';
4
3
  export { DifficultyLevel, DIFFICULTY_LEVELS, parseDifficultyLevel } from './difficulty';
@@ -8,7 +8,7 @@ export type WfsfWordBankEditorProps = {
8
8
  optionsBank: WfsfOptionsWordBankEntry[];
9
9
  onOptionsBankChange: (next: WfsfOptionsWordBankEntry[]) => void;
10
10
  blankKeys: string[];
11
- answers: Record<string, string>;
12
- onAnswerChange: (key: string, value: string) => void;
11
+ answers: Record<string, string[]>;
12
+ onAnswerChange: (key: string, value: string[]) => void;
13
13
  };
14
14
  export declare function WfsfWordBankEditor({ mode, onModeChange, sharedBank, onSharedBankChange, optionsBank, onOptionsBankChange, blankKeys, answers, onAnswerChange, }: WfsfWordBankEditorProps): import("react").JSX.Element;
@@ -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' ||
@@ -58,7 +50,6 @@ export function mapQuestionToWfsfData(question) {
58
50
  const source = asRecord(question);
59
51
  const apiContent = asRecord(source.content);
60
52
  const answer = asRecord(source.answer);
61
- const meta = asRecord(source.meta);
62
53
  const correctAnswer = asRecord(source.correctAnswer ??
63
54
  source.correct_answer ??
64
55
  answer.correctAnswer ??
@@ -67,13 +58,7 @@ export function mapQuestionToWfsfData(question) {
67
58
  ? parseFloat(String(source.total_points))
68
59
  : answer.points ?? source.points ?? 1;
69
60
  const points = Number.isFinite(Number(pointsRaw)) ? Number(pointsRaw) : 1;
70
- // Pearson bank stores rubric on meta.instruction; school/API use content.instruction.
71
- const instruction = String(apiContent.instruction ||
72
- answer.instruction ||
73
- source.instruction ||
74
- meta.instruction ||
75
- source.explanation ||
76
- '');
61
+ const instruction = String(apiContent.instruction || answer.instruction || source.instruction || source.explanation || '');
77
62
  const explanation = String(answer.explanation || '');
78
63
  const content = resolveHtmlContent(apiContent, answer);
79
64
  const rawWordBank = apiContent.wordBank ?? answer.wordBank;
@@ -104,7 +89,7 @@ export function mapQuestionToWfsfData(question) {
104
89
  audioUrl: audioUrls[0] || String(apiContent.audioUrl || answer.audioUrl || source.audio_url || ''),
105
90
  audioUrls,
106
91
  content,
107
- answers: asStringAnswers(correctAnswer.answers || answer.answers),
92
+ answers: normalizeWfsfAnswersMap(correctAnswer.answers || answer.answers),
108
93
  caseSensitive: Boolean(correctAnswer.caseSensitive ?? answer.caseSensitive ?? false),
109
94
  blankStyle: asBlankStyle(apiContent.blankStyle ?? answer.blankStyle),
110
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
  *
@@ -34,10 +35,6 @@ export const transformWordFillStructuredForm = (question) => {
34
35
  audioUrls,
35
36
  content: answerData.content,
36
37
  };
37
- const instructionText = String(answerData.instruction ?? '').trim();
38
- if (instructionText) {
39
- apiContent.instruction = instructionText;
40
- }
41
38
  if (answerData.blankStyle) {
42
39
  apiContent.blankStyle = answerData.blankStyle;
43
40
  }
@@ -61,7 +58,7 @@ export const transformWordFillStructuredForm = (question) => {
61
58
  apiContent.wordBank = answerData.wordBank;
62
59
  }
63
60
  const apiCorrectAnswer = {
64
- answers: answerData.answers,
61
+ answers: compactWfsfAnswersMap((answerData.answers || {})),
65
62
  caseSensitive: answerData.caseSensitive ?? false,
66
63
  };
67
64
  const basicGroupData = hasGroupData