@tinyweb_dev/oe-exam-sdk 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +13 -20
- package/dist/components/exams/take/exam-taking.utils.d.ts +2 -2
- package/dist/components/exams/take/exam-taking.utils.js +2 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +2 -2
- package/dist/components/exams/take/utils/question-transformers.js +2 -0
- package/dist/components/questions/_shared/types/question-group.type.d.ts +0 -1
- package/dist/components/questions/_shared/types/question-group.type.js +1 -3
- package/dist/components/questions/_shared/types/question-group.utils.d.ts +2 -0
- package/dist/components/questions/_shared/types/question-group.utils.js +3 -0
- package/dist/components/questions/_shared/types/word-fill-structured-form.type.d.ts +1 -1
- package/dist/components/questions/groups/ImageLabelWordBank.d.ts +1 -1
- package/dist/components/questions/groups/ImageLabelWordBank.js +1 -1
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -1
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -1
- package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.d.ts +2 -2
- package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.js +15 -5
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +39 -15
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +35 -12
- package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +2 -10
- package/dist/components/questions/types/word-fill-structured-form/transform.js +2 -1
- package/dist/components/questions/viewer/QuestionViewer.js +0 -67
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +1 -1
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +12 -3
- package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +13 -20
- package/dist/shared/constants/routes.d.ts +2 -2
- package/dist/shared/constants/routes.js +3 -3
- package/dist/shared/lib/utils/wfsf-blank-match.d.ts +9 -0
- package/dist/shared/lib/utils/wfsf-blank-match.js +60 -0
- package/package.json +1 -1
- package/dist/components/themes/cambridge-yle/assets/images/bg-title-old.png +0 -0
- package/dist/components/themes/cambridge-yle/assets/images/border-old.png +0 -0
- package/dist/components/themes/cambridge-yle/assets/images/close_old.png +0 -0
- package/dist/components/themes/cambridge-yle/assets/images/frame-old.png +0 -0
package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FillBlankQuestion } from '../../types';
|
|
2
|
-
import {
|
|
2
|
+
import type { ImageLabelData } from '../../../../questions/_shared/types/question-group.type';
|
|
3
3
|
interface MoversListenAndWriteRendererProps {
|
|
4
4
|
partNumber: number;
|
|
5
5
|
partName?: string;
|
package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js
CHANGED
|
@@ -7,7 +7,7 @@ import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-
|
|
|
7
7
|
import CambridgeYleExampleText from '../../../../../components/themes/cambridge-yle/CambridgeYleExampleText';
|
|
8
8
|
import { countBlanks, formatAcceptedAnswers, getPrimaryAnswer, isBlankCorrect, toBlankValues, } from '../../../../../shared/lib/utils/fill-in-blank';
|
|
9
9
|
import { ImageLabelWordBank } from '../../../../questions/groups/ImageLabelWordBank';
|
|
10
|
-
import { hasImageLabels } from '../../../../questions/_shared/types/question-group.
|
|
10
|
+
import { hasImageLabels } from '../../../../questions/_shared/types/question-group.utils';
|
|
11
11
|
/** Preserve author newlines in fill-blank stems by mapping `\n` → `<br />`. */
|
|
12
12
|
function renderTextWithLineBreaks(text) {
|
|
13
13
|
const lines = text.split('\n');
|
|
@@ -6,6 +6,7 @@ import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-y
|
|
|
6
6
|
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
7
7
|
import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
|
|
8
8
|
import { cn } from '../../../../../shared/lib/utils';
|
|
9
|
+
import { expandWfsfCorrectAlts, formatWfsfCorrectLabel, isWfsfBlankCorrect, } from '../../../../../shared/lib/utils/wfsf-blank-match';
|
|
9
10
|
/**
|
|
10
11
|
* Replace `{key}` placeholders with inline blank controls.
|
|
11
12
|
* WITH_OPTIONS blanks become <select>; free-type stay <input>.
|
|
@@ -51,14 +52,6 @@ function buildHtmlWithInputPlaceholders(html, wordBank) {
|
|
|
51
52
|
`;
|
|
52
53
|
});
|
|
53
54
|
}
|
|
54
|
-
function normalizeStructuredAnswer(value, caseSensitive) {
|
|
55
|
-
const normalized = value
|
|
56
|
-
.trim()
|
|
57
|
-
.replace(/[\p{P}]/gu, '')
|
|
58
|
-
.replace(/\s+/g, ' ')
|
|
59
|
-
.trim();
|
|
60
|
-
return caseSensitive ? normalized : normalized.toLowerCase();
|
|
61
|
-
}
|
|
62
55
|
// Stable empty object to avoid new {} on every render (breaks React.memo)
|
|
63
56
|
const emptyObj = {};
|
|
64
57
|
/**
|
|
@@ -98,33 +91,33 @@ const StructuredFormQuestion = memo(function StructuredFormQuestion({ question,
|
|
|
98
91
|
const wrapper = container.querySelector(`[data-blank-wrapper="${key}"]`);
|
|
99
92
|
const hint = container.querySelector(`[data-blank-hint="${key}"]`);
|
|
100
93
|
const userAnswer = initialAnswersRef.current[key] || '';
|
|
101
|
-
const correctAnswer = question.answers[key]
|
|
94
|
+
const correctAnswer = question.answers[key];
|
|
95
|
+
const correctLabel = formatWfsfCorrectLabel(correctAnswer);
|
|
102
96
|
const hasUserAnswer = userAnswer.trim().length > 0;
|
|
103
97
|
const isCorrect = hasUserAnswer
|
|
104
|
-
&&
|
|
105
|
-
=== normalizeStructuredAnswer(correctAnswer, question.caseSensitive);
|
|
98
|
+
&& isWfsfBlankCorrect(userAnswer, correctAnswer, question.caseSensitive);
|
|
106
99
|
// Restore saved answer value on mount
|
|
107
|
-
const shouldReplaceInputWithReviewText = isReviewMode && !isCorrect &&
|
|
100
|
+
const shouldReplaceInputWithReviewText = isReviewMode && !isCorrect && correctLabel.length > 0;
|
|
108
101
|
input.value = shouldReplaceInputWithReviewText
|
|
109
|
-
? `${hasUserAnswer ? userAnswer : '(trống)'} → ${
|
|
102
|
+
? `${hasUserAnswer ? userAnswer : '(trống)'} → ${correctLabel}`
|
|
110
103
|
: userAnswer;
|
|
111
104
|
input.disabled = isReviewMode;
|
|
112
105
|
const isSelect = input.tagName === 'SELECT';
|
|
113
106
|
// Set classes from blankStyle (letter = square box)
|
|
114
107
|
const blankStyle = question.blankStyle || 'word';
|
|
115
108
|
const isLetter = blankStyle === 'letter';
|
|
116
|
-
const isPhrase = blankStyle === 'phrase';
|
|
109
|
+
const isPhrase = blankStyle === 'phrase' || blankStyle === 'translate';
|
|
117
110
|
if (isLetter && !isSelect && 'maxLength' in input) {
|
|
118
|
-
|
|
111
|
+
const maxAltLen = expandWfsfCorrectAlts(correctAnswer).reduce((max, alt) => Math.max(max, alt.trim().length), 0);
|
|
112
|
+
input.maxLength = Math.max(1, maxAltLen || 1);
|
|
119
113
|
}
|
|
120
114
|
input.className = cn('structured-blank-input inline-block bg-slate-50/40 text-sm font-semibold outline-none transition-all align-baseline hover:bg-slate-100/60 focus:bg-teal-50/60 focus:ring-0', isSelect
|
|
121
115
|
? 'min-w-[5.5rem] max-w-[10rem] rounded border px-2 py-1 text-center'
|
|
122
116
|
: isLetter
|
|
123
117
|
? 'h-7 w-7 rounded border-2 px-0 text-center uppercase'
|
|
124
118
|
: isPhrase
|
|
125
|
-
? 'min-w-[
|
|
126
|
-
:
|
|
127
|
-
'min-w-[4.5rem] max-w-[7.5rem] w-[8ch] rounded-t border-b-2 px-1 py-0.5 text-center', isReviewMode
|
|
119
|
+
? 'min-w-[16rem] max-w-[36rem] w-[28ch] rounded-t border-b-2 px-2 py-0.5 text-center'
|
|
120
|
+
: 'min-w-[4.5rem] max-w-[8rem] w-[10ch] rounded-t border-b-2 px-1 py-0.5 text-center', isReviewMode
|
|
128
121
|
? isCorrect
|
|
129
122
|
? 'border-emerald-500 bg-emerald-50 text-emerald-800 font-bold'
|
|
130
123
|
: hasUserAnswer
|
|
@@ -132,7 +125,7 @@ const StructuredFormQuestion = memo(function StructuredFormQuestion({ question,
|
|
|
132
125
|
: 'border-amber-500 bg-amber-50 text-amber-800 font-bold'
|
|
133
126
|
: 'border-slate-400 text-slate-800 focus:border-teal-600');
|
|
134
127
|
if (!isSelect && !isLetter && 'size' in input) {
|
|
135
|
-
input.size = isPhrase ?
|
|
128
|
+
input.size = isPhrase ? 28 : 10;
|
|
136
129
|
}
|
|
137
130
|
// letter inline box — host FE may not scan Tailwind h-7/w-7
|
|
138
131
|
if (isLetter) {
|
|
@@ -176,7 +169,7 @@ const StructuredFormQuestion = memo(function StructuredFormQuestion({ question,
|
|
|
176
169
|
}, [processedHtml, isReviewMode]);
|
|
177
170
|
return (_jsxs("div", { className: "space-y-3", children: [question.title && (_jsx("h3", { className: "text-base font-semibold text-gray-800", children: question.title })), question.imageUrl && (_jsx("div", { className: "flex justify-center", children: _jsx("div", { className: "overflow-hidden rounded-lg border border-gray-200", children: _jsx(ResolvedImage, { src: question.imageUrl, alt: question.title || 'Question image', className: "max-h-48 w-auto object-contain" }) }) })), _jsx("div", { ref: containerRef, className: cn('structured-form-content rounded-lg border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4 text-sm leading-relaxed text-gray-800', '[&_.structured-blank-wrapper]:inline-flex [&_.structured-blank-wrapper]:align-baseline [&_.structured-blank-wrapper]:items-baseline [&_.structured-blank-hint]:max-w-[180px]',
|
|
178
171
|
// Conversation: speaker column hug name (not equal 50/50 split)
|
|
179
|
-
'[&_table.dialogue]:w-full [&_table.dialogue]:border-collapse [&_table.dialogue]:table-fixed', '[&_table.dialogue_td.speaker]:w-[1%] [&_table.dialogue_td.speaker]:whitespace-nowrap', '[&_table.dialogue_td.speaker]:align-top [&_table.dialogue_td.speaker]:pr-3', '[&_table.dialogue_td.speaker]:font-semibold [&_table.dialogue_td.speaker]:text-slate-900', '[&_table.dialogue_td]:align-top [&_table.dialogue_td]:py-1.5') }), isReviewMode && (_jsxs("div", { className: "space-y-2 rounded-lg border border-slate-200 bg-slate-50 p-3", children: [_jsxs("div", { className: "flex flex-wrap gap-2 text-[11px] font-medium", children: [_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-1 text-green-700", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-green-500" }), "\u0110\u00FAng"] }), _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-red-100 px-2 py-1 text-red-700", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-red-500" }), "Sai"] }), _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-1 text-amber-700", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-amber-500" }), "Ch\u01B0a tr\u1EA3 l\u1EDDi"] })] }), _jsxs("div", { children: [_jsx("span", { className: "text-[10px] font-medium uppercase tracking-wider text-slate-500", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("div", { className: "mt-1 flex flex-wrap gap-1.5", children: Object.entries(question.answers).map(([key, val]) => (_jsxs("span", { className: "rounded border border-green-300 bg-green-100 px-2 py-0.5 text-xs font-semibold text-green-800", children: [key, ": ", val] }, key))) })] })] }))] }));
|
|
172
|
+
'[&_table.dialogue]:w-full [&_table.dialogue]:border-collapse [&_table.dialogue]:table-fixed', '[&_table.dialogue_td.speaker]:w-[1%] [&_table.dialogue_td.speaker]:whitespace-nowrap', '[&_table.dialogue_td.speaker]:align-top [&_table.dialogue_td.speaker]:pr-3', '[&_table.dialogue_td.speaker]:font-semibold [&_table.dialogue_td.speaker]:text-slate-900', '[&_table.dialogue_td]:align-top [&_table.dialogue_td]:py-1.5') }), isReviewMode && (_jsxs("div", { className: "space-y-2 rounded-lg border border-slate-200 bg-slate-50 p-3", children: [_jsxs("div", { className: "flex flex-wrap gap-2 text-[11px] font-medium", children: [_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-1 text-green-700", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-green-500" }), "\u0110\u00FAng"] }), _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-red-100 px-2 py-1 text-red-700", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-red-500" }), "Sai"] }), _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-1 text-amber-700", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-amber-500" }), "Ch\u01B0a tr\u1EA3 l\u1EDDi"] })] }), _jsxs("div", { children: [_jsx("span", { className: "text-[10px] font-medium uppercase tracking-wider text-slate-500", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("div", { className: "mt-1 flex flex-wrap gap-1.5", children: Object.entries(question.answers).map(([key, val]) => (_jsxs("span", { className: "rounded border border-green-300 bg-green-100 px-2 py-0.5 text-xs font-semibold text-green-800", children: [key, ": ", formatWfsfCorrectLabel(val)] }, key))) })] })] }))] }));
|
|
180
173
|
});
|
|
181
174
|
export function MoversWordFillStructuredFormRenderer({ partNumber, partName, questionCount, instruction, audioUrl, groupTitle, groupContent, groupImageUrl, questions, answers, onAnswerChange, isReviewMode = false, compactLayout = false, }) {
|
|
182
175
|
const answersRef = useRef(answers);
|
|
@@ -11,7 +11,7 @@ export interface TemplateSection {
|
|
|
11
11
|
viewMode?: string;
|
|
12
12
|
wordBankConfig?: string[];
|
|
13
13
|
}
|
|
14
|
-
export type WfsfBlankStyle = 'letter' | 'word' | 'phrase';
|
|
14
|
+
export type WfsfBlankStyle = 'letter' | 'word' | 'phrase' | 'translate';
|
|
15
15
|
export type WfsfWordBankEntry = {
|
|
16
16
|
options: string[];
|
|
17
17
|
} | {
|
|
@@ -21,7 +21,7 @@ export type WfsfWordBankEntry = {
|
|
|
21
21
|
export interface WordFillStructuredFormQuestion {
|
|
22
22
|
id: string;
|
|
23
23
|
content: string;
|
|
24
|
-
answers: Record<string, string>;
|
|
24
|
+
answers: Record<string, string | string[]>;
|
|
25
25
|
title?: string;
|
|
26
26
|
instruction?: string;
|
|
27
27
|
caseSensitive?: boolean;
|
|
@@ -118,6 +118,8 @@ export function transformWordFillStructuredForm(questions) {
|
|
|
118
118
|
return 'letter';
|
|
119
119
|
if (v === 'phrase' || v === 'sentence')
|
|
120
120
|
return 'phrase';
|
|
121
|
+
if (v === 'translate')
|
|
122
|
+
return 'translate';
|
|
121
123
|
return 'word';
|
|
122
124
|
};
|
|
123
125
|
const normalizeWordBank = (raw) => {
|
|
@@ -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) {
|
|
@@ -32,7 +32,6 @@ export interface ImageLabelData {
|
|
|
32
32
|
/** Nhãn / từ hiển thị dưới ảnh */
|
|
33
33
|
label: string;
|
|
34
34
|
}
|
|
35
|
-
export declare function hasImageLabels(items?: ImageLabelData[] | null): boolean;
|
|
36
35
|
/**
|
|
37
36
|
* Shared context fields for a group of ANSWER_THE_QUESTION,
|
|
38
37
|
* CHOOSE_THE_CORRECT_ANSWER, or FILL_IN_BLANK questions.
|
|
@@ -9,6 +9,4 @@
|
|
|
9
9
|
* - IMAGE_LABEL → FILL_IN_BLANK word bank (image + label items)
|
|
10
10
|
* - (future) → MATCH_BY_WRITING_ANSWER, LISTENING_*, etc.
|
|
11
11
|
*/
|
|
12
|
-
export
|
|
13
|
-
return Array.isArray(items) && items.some((item) => Boolean(item?.imageUrl?.trim() || item?.label?.trim()));
|
|
14
|
-
}
|
|
12
|
+
export {};
|
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { ResolvedImage } from '../../../components/common/ResolvedImage';
|
|
4
|
-
import { hasImageLabels } from '../_shared/types/question-group.
|
|
4
|
+
import { hasImageLabels } from '../_shared/types/question-group.utils';
|
|
5
5
|
/**
|
|
6
6
|
* Horizontal picture + label bank shown to students (and creator preview).
|
|
7
7
|
*/
|
package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js
CHANGED
|
@@ -449,7 +449,7 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
|
|
|
449
449
|
? 'border-violet-300 bg-violet-50 text-violet-700'
|
|
450
450
|
: 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'} ${Boolean(multipleAnswers) ? 'cursor-not-allowed opacity-50' : ''}`, children: [_jsx(ListChecks, { className: "h-4 w-4" }), "Nhi\u1EC1u \u0111\u00E1p \u00E1n"] }), _jsxs("button", { type: "button", style: { paddingBlock: 'calc(var(--spacing) * 1)' }, onClick: () => setIsTipTapMode((enabled) => !enabled), title: isTipTapMode ? 'Chuyển về ô nhập thường' : 'Bật định dạng chữ (in đậm, danh sách, bảng)', className: `inline-flex items-center gap-2 rounded-md border px-3.5 text-sm font-medium transition-colors select-none ${isTipTapMode
|
|
451
451
|
? 'border-indigo-300 bg-indigo-50 text-indigo-700'
|
|
452
|
-
: 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'}`, children: [_jsx(Pilcrow, { className: "h-4 w-4" }), "\u0110\u1ECBnh d\u1EA1ng"] })] })] }), viewMode === 'CONVERSATION' && (_jsxs("div", { className: "space-y-3", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Users, { className: "h-4 w-4 text-indigo-500" }), "H\u1ED9i tho\u1EA1i ", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn - d\u00F9ng cho c\u00E2u h\u1ECFi d\u1EA1ng h\u1ED9i tho\u1EA1i)" })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4 rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: "askerName", className: "flex items-center gap-1 text-sm font-medium text-gray-600", children: ["T\u00EAn ng\u01B0\u1EDDi h\u1ECFi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "askerName", ...form.register('askerName'), placeholder: "VD: John, Mary, Teacher...", className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('askerName') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` }), form.hasError('askerName') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('askerName') }))] }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: "answererName", className: "flex items-center gap-1 text-sm font-medium text-gray-600", children: ["T\u00EAn ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "answererName", ...form.register('answererName'), placeholder: "VD: Tom, Anna, Student...", className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('answererName') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` }), form.hasError('answererName') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('answererName') }))] })] })] })), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('question')) && (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "question", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-indigo-500" }), _jsx("span", { className: "flex items-center gap-1", children: "C\u00E2u h\u1ECFi" }), _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(ChooseAnswerTextInput, { value: watchedQuestion, onChange: (html) => form.setValue('question', html, { shouldValidate: true }), isTipTapMode: isTipTapMode, placeholder: "Nh\u1EADp c\u00E2u h\u1ECFi..." }), form.hasError('question') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('question') }))] })), ((!questionConfig || questionConfig.length === 0 || questionConfig.includes('imageUrl') || questionConfig.includes('imageUrl ')) && !isImageInputVisible
|
|
452
|
+
: 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'}`, children: [_jsx(Pilcrow, { className: "h-4 w-4" }), "\u0110\u1ECBnh d\u1EA1ng"] })] })] }), viewMode === 'CONVERSATION' && (_jsxs("div", { className: "space-y-3", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Users, { className: "h-4 w-4 text-indigo-500" }), "H\u1ED9i tho\u1EA1i ", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn - d\u00F9ng cho c\u00E2u h\u1ECFi d\u1EA1ng h\u1ED9i tho\u1EA1i)" })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4 rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: "askerName", className: "flex items-center gap-1 text-sm font-medium text-gray-600", children: ["T\u00EAn ng\u01B0\u1EDDi h\u1ECFi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "askerName", ...form.register('askerName'), placeholder: "VD: John, Mary, Teacher...", className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('askerName') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` }), form.hasError('askerName') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('askerName') }))] }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: "answererName", className: "flex items-center gap-1 text-sm font-medium text-gray-600", children: ["T\u00EAn ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "answererName", ...form.register('answererName'), placeholder: "VD: Tom, Anna, Student...", className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('answererName') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` }), form.hasError('answererName') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('answererName') }))] })] })] })), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('question')) && (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "question", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-indigo-500" }), _jsx("span", { className: "flex items-center gap-1", children: "C\u00E2u h\u1ECFi" }), _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(ChooseAnswerTextInput, { value: watchedQuestion ?? '', onChange: (html) => form.setValue('question', html, { shouldValidate: true }), isTipTapMode: isTipTapMode, placeholder: "Nh\u1EADp c\u00E2u h\u1ECFi..." }), form.hasError('question') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('question') }))] })), ((!questionConfig || questionConfig.length === 0 || questionConfig.includes('imageUrl') || questionConfig.includes('imageUrl ')) && !isImageInputVisible
|
|
453
453
|
|| !isExplanationVisible) && (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [(!questionConfig || questionConfig.length === 0 || questionConfig.includes('imageUrl') || questionConfig.includes('imageUrl ')) && !isImageInputVisible && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsImageInputVisible(true), className: "gap-2 border-indigo-200 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(ImageIcon, { className: "h-4 w-4" }), watchedImageUrl ? 'Hiện hình ảnh' : 'Thêm hình ảnh'] })), !isExplanationVisible && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsExplanationVisible(true), className: "gap-2 border-amber-200 text-amber-700 hover:bg-amber-50 hover:text-amber-800", children: [_jsx(BookOpen, { className: "h-4 w-4" }), watchedExplanation ? 'Hiện giải thích' : 'Thêm giải thích'] }))] })), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('imageUrl') || questionConfig.includes('imageUrl ')) && isImageInputVisible && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-indigo-500" }), "H\u00ECnh \u1EA3nh"] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsImageInputVisible(false), className: "h-8 text-gray-500 hover:text-gray-700", children: "\u1EA8n" })] }), _jsx(FileUpload, { id: "question-image", label: "", accept: "image/*", value: watchedImageUrl || '', onChange: (url) => {
|
|
454
454
|
form.setValue('imageUrl', url, { shouldValidate: true });
|
|
455
455
|
}, onPresignedUrlChange: setImagePreviewUrl, maxSize: 5, placeholder: "Upload \u1EA3nh ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }), form.hasError('imageUrl') && (_jsx("p", { className: "mt-1 text-sm text-red-600", children: form.getFieldError('imageUrl') })), displayPreviewUrl && (_jsx(ImagePreview, { src: displayPreviewUrl, alt: "Preview", className: "mt-2 h-48 w-full max-w-md rounded-lg border border-gray-200" }))] })), isExplanationVisible && (_jsxs("div", { className: "relative", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsExplanationVisible(false), className: "absolute right-3 top-3 z-10 h-8 text-gray-500 hover:text-gray-700", children: "\u1EA8n" }), _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, { ...form.register('explanation'), 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" }) })] })] })), 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 flex-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, index) => (_jsx("li", { children: error }, index))) })] })] })), form.formState.errors.options?.message && (_jsx("div", { className: "rounded-xl border border-red-200 bg-red-50 p-4", children: _jsx("p", { className: "text-sm text-red-600", children: form.formState.errors.options.message }) }))] })] }), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('options')) && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-indigo-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-indigo-100", children: _jsx(Lightbulb, { className: "h-3.5 w-3.5 text-indigo-600" }) }), _jsxs(CardTitle, { className: "text-base font-semibold text-gray-800", children: ["C\u00E1c l\u1EF1a ch\u1ECDn ", _jsxs("span", { className: "text-xs font-normal text-gray-500", children: ["(", watchedOptions.length, " \u0111\u00E1p \u00E1n)"] })] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-1.5", children: [_jsx(Type, { className: `h-3.5 w-3.5 ${watchedOptionType === 'text' ? 'text-indigo-600' : 'text-gray-400'}` }), _jsx("span", { className: `text-xs font-medium ${watchedOptionType === 'text' ? 'text-indigo-600' : 'text-gray-400'}`, children: "Text" }), _jsx(Switch, { checked: watchedOptionType === 'image', disabled: true, className: watchedOptionType === 'image' ? '!bg-indigo-600 !opacity-100' : '!bg-indigo-600 !opacity-100', onCheckedChange: (checked) => {
|
|
@@ -4,7 +4,7 @@ import { useState, useEffect, useMemo } from 'react';
|
|
|
4
4
|
import { Input } from '../../../../components/ui/input';
|
|
5
5
|
import { Check, X, PenLine, AlertTriangle, ClipboardList, Lightbulb, BookOpen, Hash, Volume2, } from 'lucide-react';
|
|
6
6
|
import { ImageLabelWordBank } from '../../groups/ImageLabelWordBank';
|
|
7
|
-
import { hasImageLabels } from '../../_shared/types/question-group.
|
|
7
|
+
import { hasImageLabels } from '../../_shared/types/question-group.utils';
|
|
8
8
|
import { extractPlaceholderIndices, formatBlankAnswersDisplay, getPrimaryBlankAnswer, isBlankInputCorrect, normalizeBlankAnswers, normalizeFillInBlankAnswers, } from './answer-utils';
|
|
9
9
|
import { parseFillBlankSegments } from '../../../../shared/lib/utils/fill-in-blank';
|
|
10
10
|
import { cn } from '../../../../shared/lib/utils';
|
|
@@ -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]
|
|
65
|
-
onAnswerChange(key,
|
|
63
|
+
const removed = entry.options[optionIndex]?.trim();
|
|
64
|
+
if (key && removed && (answers[key] || []).includes(removed)) {
|
|
65
|
+
onAnswerChange(key, (answers[key] || []).filter((alt) => alt !== removed));
|
|
66
|
+
}
|
|
66
67
|
};
|
|
67
68
|
return (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-indigo-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-indigo-400 to-purple-400" }), _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-indigo-100", children: _jsx(Library, { className: "h-3.5 w-3.5 text-indigo-600" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "Word Bank / g\u1EE3i \u00FD \u0111\u00E1p \u00E1n" }), _jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: "Ch\u1ECDn ch\u1EBF \u0111\u1ED9 hi\u1EC3n th\u1ECB cho h\u1ECDc sinh khi l\u00E0m b\u00E0i (WFSF plan 0024/0025)" })] })] }) }), _jsx(CardContent, { className: "space-y-3 px-4 pb-4", children: _jsx("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-3", children: MODE_OPTIONS.map((opt) => {
|
|
68
69
|
const active = mode === opt.id;
|
|
@@ -76,10 +77,19 @@ export function WfsfWordBankEditor({ mode, onModeChange, sharedBank, onSharedBan
|
|
|
76
77
|
: 'bg-gradient-to-br from-purple-500 to-pink-600'}`, children: index + 1 }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Checkbox, { id: `wfsf-ex-${index}`, checked: !!item.isExample, onCheckedChange: (c) => toggleSharedExample(index, c === true) }), _jsx(Label, { htmlFor: `wfsf-ex-${index}`, className: "cursor-pointer text-xs text-gray-600", children: "Example" })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeSharedWord(index), className: "h-7 w-7 p-0 text-gray-400 hover:bg-red-50 hover:text-red-500", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] })] }), _jsx(Input, { value: item.word, onChange: (e) => updateSharedWord(index, e.target.value), placeholder: "Nh\u1EADp t\u1EEB...", className: item.isExample ? 'bg-amber-50' : 'border-gray-200' })] }, index))) }))] })] })), mode === 'WITH_OPTIONS' && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-teal-400 to-cyan-400" }), _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-teal-100", children: _jsx(ListChecks, { className: "h-3.5 w-3.5 text-teal-600" }) }), _jsxs(CardTitle, { className: "text-base font-semibold text-gray-800", children: ["L\u1EF1a ch\u1ECDn theo ch\u1ED7 tr\u1ED1ng (", blankKeys.length, ")"] })] }) }), _jsx(CardContent, { className: "space-y-4 px-4 pb-4", children: blankKeys.length === 0 ? (_jsxs("p", { className: "rounded-lg border-2 border-dashed border-gray-200 p-6 text-center text-sm text-gray-500", children: ["Ch\u01B0a c\u00F3 ch\u1ED7 tr\u1ED1ng. Th\u00EAm ", '{0}', ", ", '{1}', "\u2026 trong n\u1ED9i dung tr\u01B0\u1EDBc."] })) : (blankKeys.map((key, blankIndex) => {
|
|
77
78
|
const entry = optionsBank[blankIndex] ?? null;
|
|
78
79
|
const isFree = entry == null;
|
|
79
|
-
const selected = answers[key] ||
|
|
80
|
+
const selected = (answers[key] || []).filter(Boolean);
|
|
80
81
|
return (_jsxs("div", { className: `rounded-xl border-2 p-4 ${isFree
|
|
81
82
|
? 'border-dashed border-gray-200 bg-gray-50/40'
|
|
82
|
-
: 'border-gray-200 bg-white'}`, children: [_jsxs("div", { className: "mb-3 flex flex-wrap items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-teal-500 to-cyan-600 text-sm font-bold text-white", children: key }), _jsxs("span", { className: "text-sm font-semibold text-gray-700", children: ["Ch\u1ED7 tr\u1ED1ng ", _jsx("code", { className: "text-teal-700", children: `{${key}}` })] }), selected && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700", children: [_jsx(CheckCircle2, { className: "h-3 w-3" }), selected] }))] }), _jsxs("label", { className: "flex items-center gap-2 text-xs text-gray-600", children: [_jsx(Checkbox, { checked: isFree, onCheckedChange: (c) => setBlankFreeType(blankIndex, c === true) }), "Free-type (kh\u00F4ng options)"] })] }), !isFree && entry && (_jsxs("div", { className: "space-y-2", children: [entry.options.map((option, optionIndex) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", title: "Ch\u1ECDn
|
|
83
|
+
: 'border-gray-200 bg-white'}`, children: [_jsxs("div", { className: "mb-3 flex flex-wrap items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-teal-500 to-cyan-600 text-sm font-bold text-white", children: key }), _jsxs("span", { className: "text-sm font-semibold text-gray-700", children: ["Ch\u1ED7 tr\u1ED1ng ", _jsx("code", { className: "text-teal-700", children: `{${key}}` })] }), selected.length > 0 && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700", children: [_jsx(CheckCircle2, { className: "h-3 w-3" }), selected.join(' / ')] }))] }), _jsxs("label", { className: "flex items-center gap-2 text-xs text-gray-600", children: [_jsx(Checkbox, { checked: isFree, onCheckedChange: (c) => setBlankFreeType(blankIndex, c === true) }), "Free-type (kh\u00F4ng options)"] })] }), !isFree && entry && (_jsxs("div", { className: "space-y-2", children: [entry.options.map((option, optionIndex) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", title: "Ch\u1ECDn / b\u1ECF \u0111\u00E1p \u00E1n \u0111\u00FAng", onClick: () => {
|
|
84
|
+
const opt = option.trim();
|
|
85
|
+
if (!opt)
|
|
86
|
+
return;
|
|
87
|
+
const current = answers[key] || [];
|
|
88
|
+
const next = current.includes(opt)
|
|
89
|
+
? current.filter((alt) => alt !== opt)
|
|
90
|
+
: [...current, opt];
|
|
91
|
+
onAnswerChange(key, next);
|
|
92
|
+
}, className: `flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-xs font-bold ${selected.includes(option.trim()) && option.trim()
|
|
83
93
|
? 'bg-green-500 text-white'
|
|
84
94
|
: 'bg-gray-100 text-gray-500 hover:bg-green-100 hover:text-green-600'}`, children: String.fromCharCode(65 + optionIndex) }), _jsx(Input, { value: option, onChange: (e) => updateOption(blankIndex, optionIndex, e.target.value), placeholder: `Lựa chọn ${String.fromCharCode(65 + optionIndex)}`, className: "flex-1 border-gray-200 text-sm" }), entry.options.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeOption(blankIndex, optionIndex), className: "h-8 w-8 p-0 text-red-400 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, optionIndex))), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => addOption(blankIndex), className: "mt-1 gap-1 text-xs text-teal-600 hover:bg-teal-50", children: [_jsx(Plus, { className: "h-3 w-3" }), "Th\u00EAm l\u1EF1a ch\u1ECDn"] })] }))] }, key));
|
|
85
95
|
})) })] }))] }));
|
package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js
CHANGED
|
@@ -5,12 +5,15 @@ import { Check, X, FileText, Library } from 'lucide-react';
|
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
6
|
import { cn } from '../../../../shared/lib/utils';
|
|
7
7
|
import { detectWfsfBankMode, isOptionsWordEntry, normalizeSharedWordBank, } from './wfsf-word-bank.utils';
|
|
8
|
+
import { expandWfsfCorrectAlts, formatWfsfCorrectLabel, isWfsfBlankCorrect, primaryWfsfAnswers, } from '../../../../shared/lib/utils/wfsf-blank-match';
|
|
8
9
|
function normalizeWfsfBlankStyle(raw) {
|
|
9
10
|
const v = String(raw || '').toLowerCase();
|
|
10
11
|
if (v === 'letter' || v === 'box')
|
|
11
12
|
return 'letter';
|
|
12
13
|
if (v === 'phrase' || v === 'sentence')
|
|
13
14
|
return 'phrase';
|
|
15
|
+
if (v === 'translate')
|
|
16
|
+
return 'translate';
|
|
14
17
|
return 'word';
|
|
15
18
|
}
|
|
16
19
|
function parseStyleAttribute(styleValue) {
|
|
@@ -59,7 +62,10 @@ function ClientAudioItem({ url }) {
|
|
|
59
62
|
}
|
|
60
63
|
const DEFAULT_WFSF_HEADER = 'Điền từ vào biểu mẫu có cấu trúc';
|
|
61
64
|
export function WordFillStructuredFormClient({ questionData, isReviewMode = false, userAnswer, onAnswerChange, autoFillCorrectAnswers = false, instruction, questionNumber, explanation, }) {
|
|
62
|
-
const { title = '', content = '', answers = {}, caseSensitive = false, viewMode, wordBank = [], instruction: dataInstruction, } = questionData;
|
|
65
|
+
const { title = '', content = '', answers = {}, caseSensitive = false, blankStyle: rawBlankStyle, viewMode, wordBank = [], instruction: dataInstruction, } = questionData;
|
|
66
|
+
const blankStyle = normalizeWfsfBlankStyle(rawBlankStyle);
|
|
67
|
+
const isLetterBlank = blankStyle === 'letter';
|
|
68
|
+
const isPhraseBlank = blankStyle === 'phrase' || blankStyle === 'translate';
|
|
63
69
|
const allImages = useMemo(() => {
|
|
64
70
|
if (Array.isArray(questionData.imageUrls) && questionData.imageUrls.length > 0) {
|
|
65
71
|
return questionData.imageUrls.filter(Boolean);
|
|
@@ -80,7 +86,7 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
80
86
|
if (userAnswer)
|
|
81
87
|
return { ...userAnswer };
|
|
82
88
|
if (autoFillCorrectAnswers)
|
|
83
|
-
return
|
|
89
|
+
return primaryWfsfAnswers(answers);
|
|
84
90
|
return {};
|
|
85
91
|
});
|
|
86
92
|
const [expandedBlank, setExpandedBlank] = useState(null);
|
|
@@ -127,11 +133,7 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
127
133
|
});
|
|
128
134
|
}, [content]);
|
|
129
135
|
const allFilled = expectedBlankKeys.every(k => filledAnswers[k]?.trim());
|
|
130
|
-
const isAllCorrect = isReviewMode && expectedBlankKeys.every(k =>
|
|
131
|
-
const userVal = (filledAnswers[k] || '').trim();
|
|
132
|
-
const correctVal = (answers[k] || '').trim();
|
|
133
|
-
return caseSensitive ? userVal === correctVal : userVal.toLowerCase() === correctVal.toLowerCase();
|
|
134
|
-
});
|
|
136
|
+
const isAllCorrect = isReviewMode && expectedBlankKeys.every((k) => isWfsfBlankCorrect(filledAnswers[k] || '', answers[k], caseSensitive));
|
|
135
137
|
// Header = page rubric. Pearson preview maps draft.instruction → explanation.
|
|
136
138
|
const rawExplanation = String(explanation || questionData.explanation || '').trim();
|
|
137
139
|
const headerInstruction = String(instruction || dataInstruction || rawExplanation || '').trim();
|
|
@@ -153,11 +155,8 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
153
155
|
}, [isReviewMode, onAnswerChange]);
|
|
154
156
|
const renderBlank = useCallback((key, reactKey) => {
|
|
155
157
|
const filled = filledAnswers[key] || '';
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const normalizedCorrect = caseSensitive ? correctAnswer.trim() : correctAnswer.trim().toLowerCase();
|
|
159
|
-
const isCorrectFill = isReviewMode && normalizedFilled === normalizedCorrect;
|
|
160
|
-
const isWrongFill = isReviewMode && !!filled && normalizedFilled !== normalizedCorrect;
|
|
158
|
+
const isCorrectFill = isReviewMode && isWfsfBlankCorrect(filled, answers[key], caseSensitive);
|
|
159
|
+
const isWrongFill = isReviewMode && !!filled && !isCorrectFill;
|
|
161
160
|
const blankOptions = optionsByKey.get(key);
|
|
162
161
|
if (bankMode === 'WITH_OPTIONS' && blankOptions && blankOptions.length >= 2) {
|
|
163
162
|
const isExpanded = expandedBlank === key;
|
|
@@ -173,13 +172,36 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
173
172
|
? 'border-teal-500 bg-teal-100 text-teal-800'
|
|
174
173
|
: 'border-gray-200 bg-white text-gray-700 hover:border-teal-300 hover:bg-teal-50'), children: opt }, opt))) }))] }, reactKey));
|
|
175
174
|
}
|
|
176
|
-
return (_jsxs("span", { className: "relative mx-1 inline-flex items-baseline align-baseline", children: [_jsx("span", { className: "absolute -top-4 left-1 text-[10px] font-semibold text-gray-400", children: key }), _jsx("input", { type: "text", value: filled, onChange: (e) => handleInputChange(key, e.target.value), disabled: isReviewMode, placeholder: "",
|
|
175
|
+
return (_jsxs("span", { className: "relative mx-1 inline-flex items-baseline align-baseline", children: [_jsx("span", { className: "absolute -top-4 left-1 text-[10px] font-semibold text-gray-400", children: key }), _jsx("input", { type: "text", value: filled, onChange: (e) => handleInputChange(key, e.target.value), disabled: isReviewMode, placeholder: "", maxLength: isLetterBlank
|
|
176
|
+
? Math.max(1, expandWfsfCorrectAlts(answers[key]).reduce((max, alt) => Math.max(max, alt.trim().length), 0) || 1)
|
|
177
|
+
: undefined, size: isLetterBlank ? undefined : isPhraseBlank ? 28 : 10, className: cn('inline-block bg-slate-50/40 text-center text-sm font-semibold outline-none transition-all align-baseline hover:bg-slate-100/60 focus:bg-teal-50/60 focus:ring-0', isLetterBlank
|
|
178
|
+
? 'h-7 w-7 rounded border-2 px-0 uppercase'
|
|
179
|
+
: isPhraseBlank
|
|
180
|
+
? 'min-w-[16rem] max-w-[36rem] w-[28ch] rounded-t border-b-2 px-2 py-0.5'
|
|
181
|
+
: 'min-w-[4.5rem] max-w-[8rem] w-[10ch] rounded-t border-b-2 px-1 py-0.5', isReviewMode
|
|
177
182
|
? isCorrectFill
|
|
178
183
|
? 'border-emerald-500 bg-emerald-50 text-emerald-800 font-bold'
|
|
179
184
|
: isWrongFill
|
|
180
185
|
? 'border-rose-500 bg-rose-50 text-rose-800 font-bold'
|
|
181
186
|
: 'border-amber-500 bg-amber-50 text-amber-800'
|
|
182
|
-
: 'border-slate-400 text-slate-800 focus:border-teal-600')
|
|
187
|
+
: 'border-slate-400 text-slate-800 focus:border-teal-600'), style: isLetterBlank
|
|
188
|
+
? {
|
|
189
|
+
display: 'inline-block',
|
|
190
|
+
width: '1.75rem',
|
|
191
|
+
height: '1.75rem',
|
|
192
|
+
minWidth: '1.75rem',
|
|
193
|
+
maxWidth: '1.75rem',
|
|
194
|
+
padding: 0,
|
|
195
|
+
margin: '0 2px',
|
|
196
|
+
borderWidth: 2,
|
|
197
|
+
borderStyle: 'solid',
|
|
198
|
+
borderRadius: 4,
|
|
199
|
+
textAlign: 'center',
|
|
200
|
+
fontWeight: 700,
|
|
201
|
+
textTransform: 'uppercase',
|
|
202
|
+
boxSizing: 'border-box',
|
|
203
|
+
}
|
|
204
|
+
: undefined }), isReviewMode && filled && (isCorrectFill
|
|
183
205
|
? _jsx(Check, { className: "ml-0.5 h-3.5 w-3.5 text-green-600" })
|
|
184
206
|
: _jsx(X, { className: "ml-0.5 h-3.5 w-3.5 text-red-600" }))] }, reactKey));
|
|
185
207
|
}, [
|
|
@@ -190,6 +212,8 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
190
212
|
filledAnswers,
|
|
191
213
|
handleInputChange,
|
|
192
214
|
handleSelectOption,
|
|
215
|
+
isLetterBlank,
|
|
216
|
+
isPhraseBlank,
|
|
193
217
|
isReviewMode,
|
|
194
218
|
optionsByKey,
|
|
195
219
|
]);
|
|
@@ -260,5 +284,5 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
260
284
|
: used
|
|
261
285
|
? 'border-purple-200 bg-white/60 text-purple-400 line-through'
|
|
262
286
|
: 'border-purple-300 bg-white text-purple-800'), children: [item.word, item.isExample ? ' (ex)' : ''] }, `${item.word}-${index}`));
|
|
263
|
-
}) })] })), _jsx("div", { className: "structured-form-content space-y-2 rounded-lg border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-5 text-base leading-loose text-gray-800", children: renderedContent }), isReviewMode && !isAllCorrect && (_jsxs("div", { className: "rounded-lg border border-green-200 bg-green-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-green-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: expectedBlankKeys.map((key) => (_jsxs("span", { className: "rounded-lg border border-green-300 bg-green-100 px-3 py-1.5 text-sm font-semibold text-green-800", children: [`{${key}}`, ": ", answers[key]] }, key))) })] })), isReviewMode && displayExplanation && (_jsxs("div", { className: "rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-blue-600", children: "Gi\u1EA3i th\u00EDch" }), _jsx("p", { className: "mt-1 text-sm text-blue-800", children: displayExplanation })] }))] })] }) }));
|
|
287
|
+
}) })] })), _jsx("div", { className: "structured-form-content space-y-2 rounded-lg border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-5 text-base leading-loose text-gray-800", children: renderedContent }), isReviewMode && !isAllCorrect && (_jsxs("div", { className: "rounded-lg border border-green-200 bg-green-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-green-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: expectedBlankKeys.map((key) => (_jsxs("span", { className: "rounded-lg border border-green-300 bg-green-100 px-3 py-1.5 text-sm font-semibold text-green-800", children: [`{${key}}`, ": ", formatWfsfCorrectLabel(answers[key])] }, key))) })] })), isReviewMode && displayExplanation && (_jsxs("div", { className: "rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-blue-600", children: "Gi\u1EA3i th\u00EDch" }), _jsx("p", { className: "mt-1 text-sm text-blue-800", children: displayExplanation })] }))] })] }) }));
|
|
264
288
|
}
|
package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js
CHANGED
|
@@ -20,6 +20,7 @@ import { ImagePreview } from '../../../../components/ui/image-preview';
|
|
|
20
20
|
import { WordFillStructuredFormClient } from './WordFillStructuredFormClient';
|
|
21
21
|
import { WfsfWordBankEditor } from './WfsfWordBankEditor';
|
|
22
22
|
import { useDebouncedCallback, usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
23
|
+
import { expandWfsfCorrectAlts, hasWfsfBlankAnswer, normalizeWfsfAnswersMap, primaryWfsfAnswers, } from '../../../../shared/lib/utils/wfsf-blank-match';
|
|
23
24
|
import { BasicQuestionGroupCard } from '../../groups/BasicQuestionGroupCard';
|
|
24
25
|
import { DocumentGroupCard } from '../../groups/DocumentGroupCard';
|
|
25
26
|
import { useBasicQuestionGroup, getGroupNumberForIndex } from '../../_shared/hooks/useBasicQuestionGroup';
|
|
@@ -173,7 +174,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
173
174
|
// ── TipTap ─────────────────────────────────────────────────────────────────
|
|
174
175
|
const initialContent = initialData?.content || '<p></p>';
|
|
175
176
|
const [content, setContent] = useState(initialContent);
|
|
176
|
-
const [answers, setAnswers] = useState(initialData?.answers || {});
|
|
177
|
+
const [answers, setAnswers] = useState(() => normalizeWfsfAnswersMap(initialData?.answers || {}));
|
|
177
178
|
const previousPayloadRef = useRef('');
|
|
178
179
|
const previousInitialDataRef = useRef(initialData);
|
|
179
180
|
const isInitializingRef = useRef(false);
|
|
@@ -248,7 +249,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
248
249
|
? nextExplanation
|
|
249
250
|
: '');
|
|
250
251
|
}
|
|
251
|
-
setAnswers(initialData?.answers || {});
|
|
252
|
+
setAnswers(normalizeWfsfAnswersMap(initialData?.answers || {}));
|
|
252
253
|
setBlankStyle(initialData?.blankStyle);
|
|
253
254
|
const nextMode = detectWfsfBankMode(initialData?.viewMode ?? viewModeProp, initialData?.wordBank);
|
|
254
255
|
setBankMode(nextMode);
|
|
@@ -299,12 +300,31 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
299
300
|
changed = true;
|
|
300
301
|
} });
|
|
301
302
|
expectedBlankKeys.forEach(k => { if (!(k in next)) {
|
|
302
|
-
next[k] = '';
|
|
303
|
+
next[k] = [''];
|
|
303
304
|
changed = true;
|
|
304
305
|
} });
|
|
305
306
|
return changed ? next : prev;
|
|
306
307
|
});
|
|
307
308
|
}, [expectedBlankKeys]);
|
|
309
|
+
const updateAnswerAlt = useCallback((key, index, value) => {
|
|
310
|
+
setAnswers((prev) => {
|
|
311
|
+
const current = [...(prev[key] || [''])];
|
|
312
|
+
current[index] = value;
|
|
313
|
+
return { ...prev, [key]: current };
|
|
314
|
+
});
|
|
315
|
+
}, []);
|
|
316
|
+
const addAnswerAlt = useCallback((key) => {
|
|
317
|
+
setAnswers((prev) => ({ ...prev, [key]: [...(prev[key] || ['']), ''] }));
|
|
318
|
+
}, []);
|
|
319
|
+
const removeAnswerAlt = useCallback((key, index) => {
|
|
320
|
+
setAnswers((prev) => {
|
|
321
|
+
const current = [...(prev[key] || [''])];
|
|
322
|
+
if (current.length <= 1)
|
|
323
|
+
return { ...prev, [key]: [''] };
|
|
324
|
+
current.splice(index, 1);
|
|
325
|
+
return { ...prev, [key]: current };
|
|
326
|
+
});
|
|
327
|
+
}, []);
|
|
308
328
|
// Keep WITH_OPTIONS bank length aligned to blank keys
|
|
309
329
|
useEffect(() => {
|
|
310
330
|
if (bankMode !== 'WITH_OPTIONS')
|
|
@@ -413,7 +433,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
413
433
|
if (expectedBlankKeys.length === 0)
|
|
414
434
|
return false;
|
|
415
435
|
for (const key of expectedBlankKeys) {
|
|
416
|
-
if (!answers[key]
|
|
436
|
+
if (!hasWfsfBlankAnswer(answers[key]))
|
|
417
437
|
return false;
|
|
418
438
|
}
|
|
419
439
|
if (bankMode === 'WITH_WORD_BANK') {
|
|
@@ -432,8 +452,10 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
432
452
|
if (opts.length < 2)
|
|
433
453
|
return false;
|
|
434
454
|
const key = expectedBlankKeys[i];
|
|
435
|
-
const
|
|
436
|
-
if (
|
|
455
|
+
const alts = expandWfsfCorrectAlts(answers[key]);
|
|
456
|
+
if (alts.length === 0)
|
|
457
|
+
return false;
|
|
458
|
+
if (alts.some((alt) => !opts.includes(alt)))
|
|
437
459
|
return false;
|
|
438
460
|
}
|
|
439
461
|
}
|
|
@@ -453,10 +475,10 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
453
475
|
]);
|
|
454
476
|
// ── Preview mode ───────────────────────────────────────────────────────────
|
|
455
477
|
if (isPreviewMode) {
|
|
456
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsx(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-teal-500 to-cyan-600 shadow-sm", children: _jsx(Eye, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n t\u1EEB \u2013 Xem tr\u01B0\u1EDBc" }), _jsx("p", { className: "text-xs text-gray-500", children: "Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc v\u1EDBi \u0111\u00E1p \u00E1n \u0111\u00FAng \u0111\u00E3 \u0111\u01B0\u1EE3c \u0111i\u1EC1n s\u1EB5n" })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsPreviewMode(false), className: "gap-2 border-teal-200 text-teal-700 hover:bg-teal-50", children: [_jsx(Pencil, { className: "h-4 w-4" }), "Quay l\u1EA1i Edit"] })] }) }) }), _jsx(WordFillStructuredFormClient, { questionData: buildFormData(), isReviewMode: true, userAnswer: answers, instruction: instruction })] }));
|
|
478
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-teal-500 to-cyan-600 shadow-sm", children: _jsx(Eye, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n t\u1EEB \u2013 Xem tr\u01B0\u1EDBc" }), _jsx("p", { className: "text-xs text-gray-500", children: "Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc v\u1EDBi \u0111\u00E1p \u00E1n \u0111\u00FAng \u0111\u00E3 \u0111\u01B0\u1EE3c \u0111i\u1EC1n s\u1EB5n" })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsPreviewMode(false), className: "gap-2 border-teal-200 text-teal-700 hover:bg-teal-50", children: [_jsx(Pencil, { className: "h-4 w-4" }), "Quay l\u1EA1i Edit"] })] }) }) }), _jsx(WordFillStructuredFormClient, { questionData: buildFormData(), isReviewMode: true, userAnswer: primaryWfsfAnswers(answers), instruction: instruction })] }));
|
|
457
479
|
}
|
|
458
480
|
// ── Edit mode ──────────────────────────────────────────────────────────────
|
|
459
|
-
return (_jsxs("div", { className: "space-y-4", children: [isGrouped && effectiveGroupType === 'BASIC' && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `word-fill-structured-${partId ?? 'group'}` })), isGrouped && effectiveGroupType === 'DOCUMENT' && (_jsx(DocumentGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `word-fill-structured-${partId ?? 'document'}` })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-teal-500 via-cyan-500 to-emerald-500" }), _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100", children: _jsx(PenLine, { className: "h-4 w-4 text-teal-600" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n t\u1EEB v\u00E0o bi\u1EC3u m\u1EABu c\u00F3 c\u1EA5u tr\u00FAc" }), _jsxs("p", { className: "text-xs text-gray-500", children: ["Nh\u1EADp n\u1ED9i dung, \u0111\u1EB7t ch\u1ED7 tr\u1ED1ng b\u1EB1ng ", `{0}`, " ", `{1}`, "\u2026 r\u1ED3i \u0111i\u1EC1n \u0111\u00E1p \u00E1n b\u00EAn d\u01B0\u1EDBi"] })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsPreviewMode(true), disabled: expectedBlankKeys.length === 0 || expectedBlankKeys.some(k => !answers[k]
|
|
481
|
+
return (_jsxs("div", { className: "space-y-4", children: [isGrouped && effectiveGroupType === 'BASIC' && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `word-fill-structured-${partId ?? 'group'}` })), isGrouped && effectiveGroupType === 'DOCUMENT' && (_jsx(DocumentGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `word-fill-structured-${partId ?? 'document'}` })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-teal-500 via-cyan-500 to-emerald-500" }), _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100", children: _jsx(PenLine, { className: "h-4 w-4 text-teal-600" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n t\u1EEB v\u00E0o bi\u1EC3u m\u1EABu c\u00F3 c\u1EA5u tr\u00FAc" }), _jsxs("p", { className: "text-xs text-gray-500", children: ["Nh\u1EADp n\u1ED9i dung, \u0111\u1EB7t ch\u1ED7 tr\u1ED1ng b\u1EB1ng ", `{0}`, " ", `{1}`, "\u2026 r\u1ED3i \u0111i\u1EC1n \u0111\u00E1p \u00E1n b\u00EAn d\u01B0\u1EDBi"] })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsPreviewMode(true), disabled: expectedBlankKeys.length === 0 || expectedBlankKeys.some(k => !hasWfsfBlankAnswer(answers[k])), className: "gap-2 border-teal-200 text-teal-600 hover:bg-teal-50 hover:text-teal-700", children: [_jsx(Eye, { className: "h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] })] }) }), _jsxs(CardContent, { className: "space-y-4 px-4 pb-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "wfsf-instruction", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Info, { className: "h-4 w-4 text-teal-500" }), "Instruction", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(h\u01B0\u1EDBng d\u1EABn b\u00E0i t\u1EADp)" })] }), _jsx(Textarea, { id: "wfsf-instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: "VD: Complete the words in the sentences. Use the words in the box.", rows: 2, className: "min-h-[64px] resize-y border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx(PointsInput, { value: points, onChange: (e) => setPoints(Number(e.target.value) || 1) }), _jsxs("div", { role: "button", tabIndex: 0, onClick: () => setCaseSensitive(!caseSensitive), onKeyDown: (e) => { if (e.key === ' ' || e.key === 'Enter')
|
|
460
482
|
setCaseSensitive(!caseSensitive); }, style: { paddingBlock: 'calc(var(--spacing) * 1)' }, className: `inline-flex cursor-pointer items-center gap-2 rounded-md border px-3.5 text-sm font-medium transition-colors select-none ${caseSensitive
|
|
461
483
|
? 'border-teal-300 bg-teal-50 text-teal-700'
|
|
462
484
|
: 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'}`, children: [_jsx(Switch, { checked: caseSensitive, onCheckedChange: setCaseSensitive, className: caseSensitive ? 'data-[state=checked]:bg-teal-500 pointer-events-none' : 'pointer-events-none', tabIndex: -1 }), "Ph\u00E2n bi\u1EC7t Hoa / th\u01B0\u1EDDng"] })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "wfsf-title", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-teal-500" }), "Ti\u00EAu \u0111\u1EC1", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(Input, { id: "wfsf-title", value: effectiveTitle, onChange: (e) => setEffectiveTitle(e.target.value), placeholder: "Nh\u1EADp ti\u00EAu \u0111\u1EC1 c\u00E2u h\u1ECFi...", className: "border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100" })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Volume2, { className: "h-4 w-4 text-teal-500" }), "File \u00E2m thanh (Audio)", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setAudioUrls((prev) => [...prev, '']), className: "gap-1.5 border-teal-200 text-teal-600 hover:bg-teal-50 hover:text-teal-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm audio"] })] }), audioUrls.length === 0 ? (_jsx("div", { className: "flex items-center justify-between rounded-lg border border-dashed border-gray-200 bg-gray-50/50 p-3 text-xs text-gray-500", children: _jsx("span", { children: "Ch\u01B0a c\u00F3 file audio. Nh\u1EA5n \"Th\u00EAm audio\" \u0111\u1EC3 upload file nghe cho c\u00E2u h\u1ECFi." }) })) : (_jsx("div", { className: "space-y-2.5", children: audioUrls.map((audioUrl, index) => (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50/30 p-2.5", children: [_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-teal-100 text-xs font-semibold text-teal-700", children: index + 1 }), _jsx("div", { className: "flex-1", children: _jsx(FileUpload, { id: `wfsf-audio-${index}`, label: "", accept: "audio/*", value: audioUrl, onChange: (url) => {
|
|
@@ -475,10 +497,11 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
475
497
|
});
|
|
476
498
|
}, onRemove: () => {
|
|
477
499
|
setImageUrls((prev) => prev.filter((_, idx) => idx !== index));
|
|
478
|
-
} }, index))) }))] }), externalErrors && externalErrors.length > 0 && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4", children: [_jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-red-100", children: _jsx("span", { className: "text-lg", children: "\u274C" }) }), _jsxs("div", { className: "text-sm", children: [_jsx("p", { className: "font-semibold text-red-800", children: "L\u1ED7i:" }), _jsx("ul", { className: "mt-1 list-inside list-disc text-red-700", children: externalErrors.map((error, i) => _jsx("li", { children: error }, i)) })] })] }))] })] }), showContent && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-teal-100", children: _jsx(ListOrdered, { className: "h-3.5 w-3.5 text-teal-600" }) }), _jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "N\u1ED9i dung c\u1EA5u tr\u00FAc" })] }), expectedBlankKeys.length > 0 && (_jsxs("span", { className: "inline-flex items-center rounded-full border border-teal-200 bg-teal-50 px-2.5 py-0.5 text-xs font-semibold text-teal-700", children: [expectedBlankKeys.length, " ch\u1ED7 tr\u1ED1ng"] }))] }) }), _jsxs(CardContent, { className: "space-y-3 px-4 pb-4", children: [_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 transition-all focus-within:border-teal-300 focus-within:ring-2 focus-within:ring-teal-100", children: [_jsx(EditorToolbar, { editor: editor }), _jsx(EditorContent, { editor: editor })] }), _jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-teal-100 bg-teal-50/60 px-3 py-2.5", children: [_jsx(Info, { className: "mt-0.5 h-4 w-4 shrink-0 text-teal-500" }), _jsxs("p", { className: "text-xs text-teal-700", children: ["Nh\u1EADp ", _jsx("strong", { children: `{0}` }), ", ", _jsx("strong", { children: `{1}` }), ", ", _jsx("strong", { children: `{2}` }), "\u2026 v\u00E0o v\u0103n b\u1EA3n \u0111\u1EC3 t\u1EA1o \u00F4 tr\u1ED1ng. \u0110\u00E1p \u00E1n s\u1EBD t\u1EF1 \u0111\u1ED9ng xu\u1EA5t hi\u1EC7n b\u00EAn d\u01B0\u1EDBi \u0111\u1EC3 b\u1EA1n nh\u1EADp."] })] })] })] })), _jsx(WfsfWordBankEditor, { mode: bankMode, onModeChange: handleBankModeChange, sharedBank: sharedBank, onSharedBankChange: setSharedBank, optionsBank: optionsBank, onOptionsBankChange: setOptionsBank, blankKeys: expectedBlankKeys, answers: answers, onAnswerChange: (key, value) => setAnswers((prev) => ({ ...prev, [key]: value })) }), expectedBlankKeys.length > 0 && bankMode !== 'WITH_OPTIONS' && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-teal-100", children: _jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-teal-600" }) }), _jsxs(CardTitle, { className: "text-base font-semibold text-gray-800", children: ["\u0110\u00E1p \u00E1n ", _jsxs("span", { className: "text-xs font-normal text-gray-500", children: ["(", expectedBlankKeys.length, " ch\u1ED7 tr\u1ED1ng)"] })] })] }) }), _jsxs(CardContent, { className: "px-4 pb-4", children: [_jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4", children: expectedBlankKeys.map((key) => {
|
|
479
|
-
const
|
|
500
|
+
} }, index))) }))] }), externalErrors && externalErrors.length > 0 && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4", children: [_jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-red-100", children: _jsx("span", { className: "text-lg", children: "\u274C" }) }), _jsxs("div", { className: "text-sm", children: [_jsx("p", { className: "font-semibold text-red-800", children: "L\u1ED7i:" }), _jsx("ul", { className: "mt-1 list-inside list-disc text-red-700", children: externalErrors.map((error, i) => _jsx("li", { children: error }, i)) })] })] }))] })] }), showContent && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-teal-100", children: _jsx(ListOrdered, { className: "h-3.5 w-3.5 text-teal-600" }) }), _jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "N\u1ED9i dung c\u1EA5u tr\u00FAc" })] }), expectedBlankKeys.length > 0 && (_jsxs("span", { className: "inline-flex items-center rounded-full border border-teal-200 bg-teal-50 px-2.5 py-0.5 text-xs font-semibold text-teal-700", children: [expectedBlankKeys.length, " ch\u1ED7 tr\u1ED1ng"] }))] }) }), _jsxs(CardContent, { className: "space-y-3 px-4 pb-4", children: [_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 transition-all focus-within:border-teal-300 focus-within:ring-2 focus-within:ring-teal-100", children: [_jsx(EditorToolbar, { editor: editor }), _jsx(EditorContent, { editor: editor })] }), _jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-teal-100 bg-teal-50/60 px-3 py-2.5", children: [_jsx(Info, { className: "mt-0.5 h-4 w-4 shrink-0 text-teal-500" }), _jsxs("p", { className: "text-xs text-teal-700", children: ["Nh\u1EADp ", _jsx("strong", { children: `{0}` }), ", ", _jsx("strong", { children: `{1}` }), ", ", _jsx("strong", { children: `{2}` }), "\u2026 v\u00E0o v\u0103n b\u1EA3n \u0111\u1EC3 t\u1EA1o \u00F4 tr\u1ED1ng. \u0110\u00E1p \u00E1n s\u1EBD t\u1EF1 \u0111\u1ED9ng xu\u1EA5t hi\u1EC7n b\u00EAn d\u01B0\u1EDBi \u0111\u1EC3 b\u1EA1n nh\u1EADp."] })] })] })] })), _jsx(WfsfWordBankEditor, { mode: bankMode, onModeChange: handleBankModeChange, sharedBank: sharedBank, onSharedBankChange: setSharedBank, optionsBank: optionsBank, onOptionsBankChange: setOptionsBank, blankKeys: expectedBlankKeys, answers: answers, onAnswerChange: (key, value) => setAnswers((prev) => ({ ...prev, [key]: value })) }), expectedBlankKeys.length > 0 && bankMode !== 'WITH_OPTIONS' && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-teal-100", children: _jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-teal-600" }) }), _jsxs(CardTitle, { className: "text-base font-semibold text-gray-800", children: ["\u0110\u00E1p \u00E1n ", _jsxs("span", { className: "text-xs font-normal text-gray-500", children: ["(", expectedBlankKeys.length, " ch\u1ED7 tr\u1ED1ng)"] })] })] }) }), _jsxs(CardContent, { className: "px-4 pb-4", children: [_jsx("p", { className: "mb-3 text-xs text-gray-500", children: "M\u1ED7i ch\u1ED7 tr\u1ED1ng c\u00F3 th\u1EC3 c\u00F3 nhi\u1EC1u \u0111\u00E1p \u00E1n \u0111\u00FAng. H\u1ECDc vi\u00EAn \u0111\u00FAng n\u1EBFu kh\u1EDBp m\u1ED9t trong c\u00E1c \u0111\u00E1p \u00E1n." }), _jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4", children: expectedBlankKeys.map((key) => {
|
|
501
|
+
const alts = answers[key]?.length ? answers[key] : [''];
|
|
502
|
+
const isEmpty = !hasWfsfBlankAnswer(alts);
|
|
480
503
|
return (_jsxs("div", { className: `rounded-xl border-2 p-3 transition-all duration-200 ${isEmpty
|
|
481
504
|
? 'border-gray-200 bg-white'
|
|
482
|
-
: 'border-teal-200 bg-gradient-to-br from-teal-50 to-cyan-50 shadow-sm shadow-teal-100'}`, children: [_jsxs(Label, { className: "mb-1.5 flex items-center justify-between text-sm font-semibold text-gray-700", children: [_jsxs("span", { children: ["Ch\u1ED7 tr\u1ED1ng ", _jsx("code", { className: "rounded bg-gray-100 px-1 text-teal-700", children: `{${key}}` })] }), isEmpty && (_jsx("span", { className: "text-[10px] font-normal italic text-red-500", children: "*B\u1EAFt bu\u1ED9c" }))] }), _jsx(
|
|
483
|
-
}) }), expectedBlankKeys.some(k => !answers[k]
|
|
505
|
+
: 'border-teal-200 bg-gradient-to-br from-teal-50 to-cyan-50 shadow-sm shadow-teal-100'}`, children: [_jsxs(Label, { className: "mb-1.5 flex items-center justify-between text-sm font-semibold text-gray-700", children: [_jsxs("span", { children: ["Ch\u1ED7 tr\u1ED1ng ", _jsx("code", { className: "rounded bg-gray-100 px-1 text-teal-700", children: `{${key}}` })] }), isEmpty && (_jsx("span", { className: "text-[10px] font-normal italic text-red-500", children: "*B\u1EAFt bu\u1ED9c" }))] }), _jsx("div", { className: "space-y-1.5", children: alts.map((alt, altIndex) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Input, { value: alt, onChange: (e) => updateAnswerAlt(key, altIndex, e.target.value), placeholder: altIndex === 0 ? 'Đáp án đúng...' : `Đáp án thay thế ${altIndex + 1}`, className: `border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100 ${!isEmpty ? 'border-teal-200' : ''}` }), alts.length > 1 && (_jsx("button", { type: "button", title: "X\u00F3a \u0111\u00E1p \u00E1n n\u00E0y", onClick: () => removeAnswerAlt(key, altIndex), className: "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-gray-400 hover:bg-rose-50 hover:text-rose-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, `${key}-${altIndex}`))) }), _jsxs("button", { type: "button", onClick: () => addAnswerAlt(key), className: "mt-2 inline-flex items-center gap-1 text-[11px] font-medium text-teal-700 hover:text-teal-800", children: [_jsx(Plus, { className: "h-3 w-3" }), "Th\u00EAm \u0111\u00E1p \u00E1n \u0111\u00FAng"] })] }, key));
|
|
506
|
+
}) }), expectedBlankKeys.some(k => !hasWfsfBlankAnswer(answers[k])) && (_jsxs("div", { className: "mt-4 flex items-start gap-3 rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-4", children: [_jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-amber-100", children: _jsx("span", { className: "text-lg", children: "\u26A0\uFE0F" }) }), _jsxs("div", { className: "text-sm text-amber-800", children: [_jsx("p", { className: "font-semibold", children: "C\u00F2n ch\u1ED7 tr\u1ED1ng ch\u01B0a c\u00F3 \u0111\u00E1p \u00E1n" }), _jsxs("p", { className: "mt-0.5 text-amber-700", children: ["Vui l\u00F2ng \u0111i\u1EC1n \u0111\u00E1p \u00E1n \u0111\u00FAng cho t\u1EA5t c\u1EA3 c\u00E1c \u00F4 \u0111\u01B0\u1EE3c \u0111\u00E1nh d\u1EA5u ", _jsx("span", { className: "text-red-500 font-medium", children: "*B\u1EAFt bu\u1ED9c" }), "."] })] })] }))] })] })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 shadow-sm", children: _jsx(BookOpen, { className: "h-3.5 w-3.5 text-white" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" }), _jsx("span", { className: "inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-600", children: "Kh\u00F4ng b\u1EAFt bu\u1ED9c" })] }), _jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: "Gi\u1EA3i th\u00EDch cho \u0111\u00E1p \u00E1n \u0111\u00FAng \u2014 hi\u1EC3n th\u1ECB sau khi h\u1ECDc sinh ho\u00E0n th\u00E0nh c\u00E2u h\u1ECFi" })] })] }) }), _jsx(CardContent, { className: "px-4 pb-4", children: _jsx(Textarea, { value: explanation, onChange: (e) => setExplanation(e.target.value), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", rows: 3, className: "min-h-[80px] resize-none border-gray-200 bg-white transition-all focus:border-amber-300 focus:ring-amber-200" }) })] })] }));
|
|
484
507
|
}
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
+
import { normalizeWfsfAnswersMap } from '../../../../shared/lib/utils/wfsf-blank-match';
|
|
1
2
|
import { detectWfsfBankMode, normalizeOptionsWordBank, normalizeSharedWordBank, } from './wfsf-word-bank.utils';
|
|
2
3
|
function asRecord(value) {
|
|
3
4
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
4
5
|
? value
|
|
5
6
|
: {};
|
|
6
7
|
}
|
|
7
|
-
function asStringAnswers(value) {
|
|
8
|
-
const record = asRecord(value);
|
|
9
|
-
const answers = {};
|
|
10
|
-
for (const [key, entry] of Object.entries(record)) {
|
|
11
|
-
if (typeof entry === 'string')
|
|
12
|
-
answers[key] = entry;
|
|
13
|
-
}
|
|
14
|
-
return answers;
|
|
15
|
-
}
|
|
16
8
|
function asBlankStyle(value) {
|
|
17
9
|
if (value === 'letter' ||
|
|
18
10
|
value === 'word' ||
|
|
@@ -104,7 +96,7 @@ export function mapQuestionToWfsfData(question) {
|
|
|
104
96
|
audioUrl: audioUrls[0] || String(apiContent.audioUrl || answer.audioUrl || source.audio_url || ''),
|
|
105
97
|
audioUrls,
|
|
106
98
|
content,
|
|
107
|
-
answers:
|
|
99
|
+
answers: normalizeWfsfAnswersMap(correctAnswer.answers || answer.answers),
|
|
108
100
|
caseSensitive: Boolean(correctAnswer.caseSensitive ?? answer.caseSensitive ?? false),
|
|
109
101
|
blankStyle: asBlankStyle(apiContent.blankStyle ?? answer.blankStyle),
|
|
110
102
|
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
|
*
|
|
@@ -61,7 +62,7 @@ export const transformWordFillStructuredForm = (question) => {
|
|
|
61
62
|
apiContent.wordBank = answerData.wordBank;
|
|
62
63
|
}
|
|
63
64
|
const apiCorrectAnswer = {
|
|
64
|
-
answers: answerData.answers,
|
|
65
|
+
answers: compactWfsfAnswersMap((answerData.answers || {})),
|
|
65
66
|
caseSensitive: answerData.caseSensitive ?? false,
|
|
66
67
|
};
|
|
67
68
|
const basicGroupData = hasGroupData
|
|
@@ -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';
|
|
@@ -517,72 +516,6 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
517
516
|
onSubmitAnswer?.(answers);
|
|
518
517
|
} }));
|
|
519
518
|
};
|
|
520
|
-
const renderWordFillStructuredForm = () => {
|
|
521
|
-
const content = (question.content && typeof question.content === 'object'
|
|
522
|
-
? question.content
|
|
523
|
-
: {});
|
|
524
|
-
// Pearson preview maps correctAnswer → answer; bank may use either shape.
|
|
525
|
-
const answer = (question.answer ??
|
|
526
|
-
question.correctAnswer ??
|
|
527
|
-
{});
|
|
528
|
-
const answers = answer?.answers && typeof answer.answers === 'object' && !Array.isArray(answer.answers)
|
|
529
|
-
? answer.answers
|
|
530
|
-
: {};
|
|
531
|
-
const html = String(content.content || '');
|
|
532
|
-
const blankRaw = String(content.blankStyle || '').toLowerCase();
|
|
533
|
-
const blankStyle = blankRaw === 'letter' || blankRaw === 'box'
|
|
534
|
-
? 'letter'
|
|
535
|
-
: blankRaw === 'phrase' || blankRaw === 'sentence'
|
|
536
|
-
? 'phrase'
|
|
537
|
-
: 'word';
|
|
538
|
-
const userMap = userAnswer && typeof userAnswer === 'object' && !Array.isArray(userAnswer)
|
|
539
|
-
? userAnswer
|
|
540
|
-
: selectedAnswer && typeof selectedAnswer === 'object' && !Array.isArray(selectedAnswer)
|
|
541
|
-
? selectedAnswer
|
|
542
|
-
: isReviewMode
|
|
543
|
-
? answers
|
|
544
|
-
: {};
|
|
545
|
-
const qNum = typeof question.questionNumber === 'number' && question.questionNumber > 0
|
|
546
|
-
? question.questionNumber
|
|
547
|
-
: undefined;
|
|
548
|
-
const rawWordBank = Array.isArray(content.wordBank) ? content.wordBank : undefined;
|
|
549
|
-
const wordBank = rawWordBank?.map((entry) => {
|
|
550
|
-
if (entry == null || typeof entry !== 'object')
|
|
551
|
-
return null;
|
|
552
|
-
const rec = entry;
|
|
553
|
-
if (Array.isArray(rec.options)) {
|
|
554
|
-
const options = rec.options.map((o) => String(o ?? '').trim()).filter(Boolean);
|
|
555
|
-
return options.length >= 2 ? { options } : null;
|
|
556
|
-
}
|
|
557
|
-
const word = String(rec.word ?? '').trim();
|
|
558
|
-
if (word)
|
|
559
|
-
return rec.isExample ? { word, isExample: true } : { word };
|
|
560
|
-
return null;
|
|
561
|
-
});
|
|
562
|
-
const hasWordOptions = Boolean(wordBank?.some((e) => e != null &&
|
|
563
|
-
'options' in e &&
|
|
564
|
-
Array.isArray(e.options) &&
|
|
565
|
-
(e.options?.length ?? 0) >= 2));
|
|
566
|
-
const hasSharedWords = Boolean(wordBank?.some((e) => e != null && 'word' in e && e.word));
|
|
567
|
-
const viewMode = String(content.viewMode || '').trim() ||
|
|
568
|
-
(hasWordOptions ? 'WITH_OPTIONS' : hasSharedWords ? 'WITH_WORD_BANK' : undefined);
|
|
569
|
-
return (_jsx(WordFillStructuredFormClient, { questionData: {
|
|
570
|
-
title: String(content.title || ''),
|
|
571
|
-
imageUrl: content.imageUrl ? String(content.imageUrl) : undefined,
|
|
572
|
-
content: html,
|
|
573
|
-
blankStyle,
|
|
574
|
-
answers,
|
|
575
|
-
caseSensitive: Boolean(answer?.caseSensitive),
|
|
576
|
-
explanation: question.explanation,
|
|
577
|
-
...(viewMode ? { viewMode } : {}),
|
|
578
|
-
...((hasWordOptions || hasSharedWords) && wordBank ? { wordBank } : {}),
|
|
579
|
-
}, userAnswer: userMap, isReviewMode: isReviewMode, autoFillCorrectAnswers: isReviewMode && Object.keys(userMap).length === 0, explanation: question.explanation, questionNumber: qNum, onAnswerChange: (key, value) => {
|
|
580
|
-
const prev = selectedAnswer && typeof selectedAnswer === 'object' && !Array.isArray(selectedAnswer)
|
|
581
|
-
? selectedAnswer
|
|
582
|
-
: {};
|
|
583
|
-
setSelectedAnswer({ ...prev, [key]: value });
|
|
584
|
-
} }));
|
|
585
|
-
};
|
|
586
519
|
const renderWordFillParagraph = () => {
|
|
587
520
|
const content = (question.content && typeof question.content === 'object'
|
|
588
521
|
? question.content
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FillBlankQuestion } from '../../../components/exams/take/types';
|
|
2
|
-
import {
|
|
2
|
+
import type { ImageLabelData } from '../../questions/_shared/types/question-group.type';
|
|
3
3
|
interface ReviewListenAndWriteRendererProps {
|
|
4
4
|
partNumber: number;
|
|
5
5
|
questionCount: number;
|
|
@@ -7,7 +7,7 @@ import CambridgeYleInstructionBanner from '../../../components/themes/cambridge-
|
|
|
7
7
|
import CambridgeYleAudioPlayer from '../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
|
|
8
8
|
import { countBlanks, formatAcceptedAnswers, hasAnyBlankValue, isBlankCorrect, toBlankValues, } from '../../../shared/lib/utils/fill-in-blank';
|
|
9
9
|
import { ImageLabelWordBank } from '../../questions/groups/ImageLabelWordBank';
|
|
10
|
-
import { hasImageLabels } from '../../questions/_shared/types/question-group.
|
|
10
|
+
import { hasImageLabels } from '../../questions/_shared/types/question-group.utils';
|
|
11
11
|
export function ReviewListenAndWriteRenderer({ partNumber, questionCount, instruction, partName, audioUrl, title, exampleText, questions, answers, isCorrectMap, imageLabels, }) {
|
|
12
12
|
/**
|
|
13
13
|
* Get answer status from API's isCorrect field
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Info, Megaphone } from 'lucide-react';
|
|
4
|
+
/** CMS instruction may be plain text or HTML (e.g. `<p>…</p>`). */
|
|
5
|
+
function isHtmlContent(value) {
|
|
6
|
+
return /<[a-z][\s\S]*>/i.test(value);
|
|
7
|
+
}
|
|
8
|
+
function InstructionHtml({ html, className }) {
|
|
9
|
+
return (_jsx("div", { className: className,
|
|
10
|
+
// Exam CMS content — not end-user input.
|
|
11
|
+
dangerouslySetInnerHTML: { __html: html } }));
|
|
12
|
+
}
|
|
4
13
|
export default function CambridgeYleInstructionBanner({ instruction, instructions, hasExample = false, children, isReviewMode = false, compact = false, }) {
|
|
5
14
|
if (isReviewMode)
|
|
6
15
|
return null;
|
|
@@ -11,9 +20,9 @@ export default function CambridgeYleInstructionBanner({ instruction, instruction
|
|
|
11
20
|
? 'flex w-full items-center gap-2 rounded-[14px] border-2 border-dashed border-blue-300 bg-[#F8FBFF] px-3 py-2 transition-all hover:bg-blue-50/50 lg:gap-3 lg:px-4 lg:py-2.5 xl:gap-5 xl:rounded-[24px] xl:px-5 xl:py-4'
|
|
12
21
|
: 'flex w-full items-center gap-4 rounded-[24px] border-2 border-dashed border-blue-300 bg-[#F8FBFF] px-5 py-4 transition-all hover:bg-blue-50/50 sm:gap-5', children: [_jsx("div", { className: compact
|
|
13
22
|
? 'flex h-8 w-8 shrink-0 -rotate-6 items-center justify-center rounded-[10px] bg-[#0070FF] shadow-[3px_3px_0px_rgba(191,219,254,1)] transition-transform hover:rotate-0 lg:h-9 lg:w-9 xl:h-14 xl:w-14 xl:rounded-[16px] xl:shadow-[4px_4px_0px_rgba(191,219,254,1)]'
|
|
14
|
-
: 'flex h-12 w-12 shrink-0 -rotate-6 items-center justify-center rounded-[16px] bg-[#0070FF] shadow-[4px_4px_0px_rgba(191,219,254,1)] transition-transform hover:rotate-0 sm:h-14 sm:w-14', children: _jsx(Megaphone, { className: compact ? 'h-4 w-4 text-white lg:h-5 lg:w-5 xl:h-7 xl:w-7' : 'h-6 w-6 text-white sm:h-7 sm:w-7', strokeWidth: 2.5 }) }), _jsx("div", { className: "flex flex-1 flex-col justify-center", children: _jsxs("
|
|
23
|
+
: 'flex h-12 w-12 shrink-0 -rotate-6 items-center justify-center rounded-[16px] bg-[#0070FF] shadow-[4px_4px_0px_rgba(191,219,254,1)] transition-transform hover:rotate-0 sm:h-14 sm:w-14', children: _jsx(Megaphone, { className: compact ? 'h-4 w-4 text-white lg:h-5 lg:w-5 xl:h-7 xl:w-7' : 'h-6 w-6 text-white sm:h-7 sm:w-7', strokeWidth: 2.5 }) }), _jsx("div", { className: "flex flex-1 flex-col justify-center", children: _jsxs("div", { className: compact
|
|
15
24
|
? 'text-[12px] font-black uppercase leading-tight tracking-wide text-[#002868] lg:text-[13px] xl:text-[18px] xl:leading-snug'
|
|
16
|
-
: 'text-[15px] font-black uppercase leading-snug tracking-wide text-[#002868] sm:text-[18px]', style: { fontFamily: '"SVN-Volte Rounded", "Quicksand", sans-serif' }, children: [instruction, hasExample && (_jsx("span", { className: compact
|
|
25
|
+
: 'text-[15px] font-black uppercase leading-snug tracking-wide text-[#002868] sm:text-[18px]', style: { fontFamily: '"SVN-Volte Rounded", "Quicksand", sans-serif' }, children: [isHtmlContent(instruction) ? (_jsx(InstructionHtml, { html: instruction, className: "contents [&_p]:m-0 [&_p]:inline" })) : (instruction), hasExample && (_jsx("span", { className: compact
|
|
17
26
|
? 'ml-1 inline-block rounded-full bg-blue-100 px-1.5 py-0.5 align-middle text-[10px] font-bold tracking-normal text-[#0070FF] xl:-mt-1 xl:ml-2 xl:px-3 xl:text-[16px]'
|
|
18
|
-
: 'ml-2 -mt-1 inline-block rounded-full bg-blue-100 px-3 py-0.5 align-middle text-[14px] font-bold tracking-normal text-[#0070FF] sm:text-[16px]', children: "THERE IS ONE EXAMPLE" }))] }) })] })), children, hasInstructionsList && (_jsx("div", { className: "rounded-lg border border-blue-200 bg-white p-4 shadow-sm", children: _jsxs("div", { className: "space-y-3", children: [_jsxs("h4", { className: "flex items-center gap-2 font-semibold text-blue-900", children: [_jsx(Info, { className: "h-4 w-4" }), "Instructions"] }), _jsx("ol", { className: "list-inside list-decimal space-y-1.5 text-sm text-gray-700", children: instructions.map((item, index) => (_jsx("li", { className: "leading-relaxed", children: item }, index))) })] }) }))] }));
|
|
27
|
+
: 'ml-2 -mt-1 inline-block rounded-full bg-blue-100 px-3 py-0.5 align-middle text-[14px] font-bold tracking-normal text-[#0070FF] sm:text-[16px]', children: "THERE IS ONE EXAMPLE" }))] }) })] })), children, hasInstructionsList && (_jsx("div", { className: "rounded-lg border border-blue-200 bg-white p-4 shadow-sm", children: _jsxs("div", { className: "space-y-3", children: [_jsxs("h4", { className: "flex items-center gap-2 font-semibold text-blue-900", children: [_jsx(Info, { className: "h-4 w-4" }), "Instructions"] }), _jsx("ol", { className: "list-inside list-decimal space-y-1.5 text-sm text-gray-700", children: instructions.map((item, index) => (_jsx("li", { className: "leading-relaxed", children: isHtmlContent(item) ? (_jsx(InstructionHtml, { html: item, className: "[&_p]:m-0" })) : (item) }, index))) })] }) }))] }));
|
|
19
28
|
}
|
|
@@ -5,6 +5,7 @@ import { ResolvedImage } from '../../../components/common/ResolvedImage';
|
|
|
5
5
|
import { cn } from '../../../shared/lib/utils';
|
|
6
6
|
import { transformWordFillStructuredForm } from '../../../components/exams/take/exam-taking.utils';
|
|
7
7
|
import { englishCertQuestionDomId } from './english-cert-scroll';
|
|
8
|
+
import { expandWfsfCorrectAlts, formatWfsfCorrectLabel, isWfsfBlankCorrect, } from '../../../shared/lib/utils/wfsf-blank-match';
|
|
8
9
|
/**
|
|
9
10
|
* Replace `{key}` placeholders in raw HTML with inline `<input>` elements so the
|
|
10
11
|
* full HTML structure (tables, forms, …) is preserved and inputs render inside
|
|
@@ -25,14 +26,6 @@ function buildHtmlWithInputPlaceholders(html) {
|
|
|
25
26
|
`;
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
|
-
function normalizeStructuredAnswer(value, caseSensitive) {
|
|
29
|
-
const normalized = value
|
|
30
|
-
.trim()
|
|
31
|
-
.replace(/[\p{P}]/gu, '')
|
|
32
|
-
.replace(/\s+/g, ' ')
|
|
33
|
-
.trim();
|
|
34
|
-
return caseSensitive ? normalized : normalized.toLowerCase();
|
|
35
|
-
}
|
|
36
29
|
/**
|
|
37
30
|
* Build the card label from the blank keys (which are the IELTS question numbers).
|
|
38
31
|
* Returns "Questions x–y" for a numeric range, a single "Question n", or falls
|
|
@@ -82,28 +75,28 @@ const StructuredFormBody = memo(function StructuredFormBody({ question, initialA
|
|
|
82
75
|
const key = input.getAttribute('data-blank-key') || '';
|
|
83
76
|
const wrapper = container.querySelector(`[data-blank-wrapper="${key}"]`);
|
|
84
77
|
const userAnswer = initialAnswersRef.current[key] || '';
|
|
85
|
-
const correctAnswer = question.answers[key]
|
|
78
|
+
const correctAnswer = question.answers[key];
|
|
79
|
+
const correctLabel = formatWfsfCorrectLabel(correctAnswer);
|
|
86
80
|
const hasUserAnswer = userAnswer.trim().length > 0;
|
|
87
81
|
const isCorrect = hasUserAnswer &&
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const shouldShowReviewText = isReviewMode && !isCorrect && correctAnswer.trim().length > 0;
|
|
82
|
+
isWfsfBlankCorrect(userAnswer, correctAnswer, question.caseSensitive);
|
|
83
|
+
const shouldShowReviewText = isReviewMode && !isCorrect && correctLabel.length > 0;
|
|
91
84
|
input.value = shouldShowReviewText
|
|
92
|
-
? `${hasUserAnswer ? userAnswer : '(empty)'} → ${
|
|
85
|
+
? `${hasUserAnswer ? userAnswer : '(empty)'} → ${correctLabel}`
|
|
93
86
|
: userAnswer;
|
|
94
87
|
input.disabled = isReviewMode;
|
|
95
88
|
const blankStyle = question.blankStyle || 'word';
|
|
96
89
|
const isLetter = blankStyle === 'letter';
|
|
97
|
-
const isPhrase = blankStyle === 'phrase';
|
|
90
|
+
const isPhrase = blankStyle === 'phrase' || blankStyle === 'translate';
|
|
98
91
|
if (isLetter) {
|
|
99
|
-
|
|
92
|
+
const maxAltLen = expandWfsfCorrectAlts(correctAnswer).reduce((max, alt) => Math.max(max, alt.trim().length), 0);
|
|
93
|
+
input.maxLength = Math.max(1, maxAltLen || 1);
|
|
100
94
|
}
|
|
101
95
|
input.className = cn('ec-blank-input inline-block bg-blue-50/30 text-base font-semibold leading-tight text-[#0f1730] align-baseline outline-none transition-all hover:bg-blue-50/60 focus:bg-blue-50/80', isLetter
|
|
102
96
|
? 'h-8 w-8 rounded border-2 border-[#1167e8] px-0 text-center uppercase'
|
|
103
97
|
: isPhrase
|
|
104
|
-
? 'min-w-[
|
|
105
|
-
:
|
|
106
|
-
'min-w-[4.5rem] max-w-[7.5rem] w-[8ch] rounded-t border-0 border-b-2 border-dashed border-[#1167e8] px-1 py-0.5 text-center focus:border-solid focus:border-[#0b57d0]', isReviewMode
|
|
98
|
+
? 'min-w-[16rem] max-w-[36rem] w-[28ch] rounded-t border-0 border-b-2 border-dashed border-[#1167e8] px-2 py-0.5 text-center focus:border-solid focus:border-[#0b57d0]'
|
|
99
|
+
: 'min-w-[4.5rem] max-w-[8rem] w-[10ch] rounded-t border-0 border-b-2 border-dashed border-[#1167e8] px-1 py-0.5 text-center focus:border-solid focus:border-[#0b57d0]', isReviewMode
|
|
107
100
|
? isCorrect
|
|
108
101
|
? 'border-solid border-[#22c55e] bg-emerald-50 text-[#15803d]'
|
|
109
102
|
: hasUserAnswer
|
|
@@ -111,7 +104,7 @@ const StructuredFormBody = memo(function StructuredFormBody({ question, initialA
|
|
|
111
104
|
: 'border-solid border-[#f97316] bg-amber-50 text-[#c2410c]'
|
|
112
105
|
: '');
|
|
113
106
|
if (!isLetter) {
|
|
114
|
-
input.size = isPhrase ?
|
|
107
|
+
input.size = isPhrase ? 28 : 10;
|
|
115
108
|
}
|
|
116
109
|
// letter inline box — host FE may not scan Tailwind
|
|
117
110
|
if (isLetter) {
|
|
@@ -160,7 +153,7 @@ const StructuredFormBody = memo(function StructuredFormBody({ question, initialA
|
|
|
160
153
|
};
|
|
161
154
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
162
155
|
}, [processedHtml, isReviewMode]);
|
|
163
|
-
return (_jsxs("div", { className: "space-y-3", children: [question.title && (_jsx("h3", { className: "text-base font-semibold text-[#0f1730]", children: question.title })), question.imageUrl && (_jsx("div", { className: "flex justify-center", children: _jsx("div", { className: "overflow-hidden rounded-lg border border-[#d9e4f2]", children: _jsx(ResolvedImage, { src: question.imageUrl, alt: question.title || 'Question image', className: "max-h-56 w-auto object-contain" }) }) })), _jsx("div", { ref: containerRef, className: "ec-structured-content w-full overflow-x-auto rounded-xl border border-[#d9e4f2] bg-[#f4f9ff] p-5 text-base leading-relaxed text-[#0f1730] [&_table]:w-full [&_table]:border-collapse [&_td]:border [&_td]:border-[#d9e4f2] [&_td]:px-4 [&_td]:py-3 [&_td]:align-top [&_th]:border [&_th]:border-[#d9e4f2] [&_th]:bg-[#e9f3ff] [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-semibold" }), isReviewMode && Object.keys(question.answers).length > 0 && (_jsxs("div", { className: "rounded-xl border border-[#d9e4f2] bg-white/70 p-3", children: [_jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wider text-[#64748b]", children: "Correct answers" }), _jsx("div", { className: "mt-1 flex flex-wrap gap-1.5", children: Object.entries(question.answers).map(([key, val]) => (_jsxs("span", { className: "rounded border border-[#b9edc8] bg-[#ddf8e7] px-2 py-0.5 text-xs font-semibold text-[#15803d]", children: [key, ": ", val] }, key))) })] }))] }));
|
|
156
|
+
return (_jsxs("div", { className: "space-y-3", children: [question.title && (_jsx("h3", { className: "text-base font-semibold text-[#0f1730]", children: question.title })), question.imageUrl && (_jsx("div", { className: "flex justify-center", children: _jsx("div", { className: "overflow-hidden rounded-lg border border-[#d9e4f2]", children: _jsx(ResolvedImage, { src: question.imageUrl, alt: question.title || 'Question image', className: "max-h-56 w-auto object-contain" }) }) })), _jsx("div", { ref: containerRef, className: "ec-structured-content w-full overflow-x-auto rounded-xl border border-[#d9e4f2] bg-[#f4f9ff] p-5 text-base leading-relaxed text-[#0f1730] [&_table]:w-full [&_table]:border-collapse [&_td]:border [&_td]:border-[#d9e4f2] [&_td]:px-4 [&_td]:py-3 [&_td]:align-top [&_th]:border [&_th]:border-[#d9e4f2] [&_th]:bg-[#e9f3ff] [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-semibold" }), isReviewMode && Object.keys(question.answers).length > 0 && (_jsxs("div", { className: "rounded-xl border border-[#d9e4f2] bg-white/70 p-3", children: [_jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wider text-[#64748b]", children: "Correct answers" }), _jsx("div", { className: "mt-1 flex flex-wrap gap-1.5", children: Object.entries(question.answers).map(([key, val]) => (_jsxs("span", { className: "rounded border border-[#b9edc8] bg-[#ddf8e7] px-2 py-0.5 text-xs font-semibold text-[#15803d]", children: [key, ": ", formatWfsfCorrectLabel(val)] }, key))) })] }))] }));
|
|
164
157
|
});
|
|
165
158
|
/**
|
|
166
159
|
* Branded English Certification renderer for `WORD_FILL_STRUCTURED_FORM`.
|
|
@@ -21,8 +21,8 @@ export declare const ROUTES: {
|
|
|
21
21
|
readonly USERS: "/manage/users";
|
|
22
22
|
readonly USERS_CREATE: "/manage/users/create";
|
|
23
23
|
readonly USERS_EDIT: (id: string) => string;
|
|
24
|
-
readonly QUESTION_BANK: "/manage/question-
|
|
25
|
-
readonly QUESTION_CREATE: "/manage/question-
|
|
24
|
+
readonly QUESTION_BANK: "/manage/question-banks";
|
|
25
|
+
readonly QUESTION_CREATE: "/manage/question-banks/create";
|
|
26
26
|
readonly QUESTION_EDIT: (id: string) => string;
|
|
27
27
|
readonly EXAMS: "/manage/exams";
|
|
28
28
|
readonly EXAMS_CREATE: "/manage/exams/create";
|
|
@@ -26,9 +26,9 @@ export const ROUTES = {
|
|
|
26
26
|
USERS: '/manage/users',
|
|
27
27
|
USERS_CREATE: '/manage/users/create',
|
|
28
28
|
USERS_EDIT: (id) => `/manage/users/${id}/edit`,
|
|
29
|
-
QUESTION_BANK: '/manage/question-
|
|
30
|
-
QUESTION_CREATE: '/manage/question-
|
|
31
|
-
QUESTION_EDIT: (id) => `/manage/question-
|
|
29
|
+
QUESTION_BANK: '/manage/question-banks',
|
|
30
|
+
QUESTION_CREATE: '/manage/question-banks/create',
|
|
31
|
+
QUESTION_EDIT: (id) => `/manage/question-banks/${id}/edit`,
|
|
32
32
|
EXAMS: '/manage/exams',
|
|
33
33
|
EXAMS_CREATE: '/manage/exams/create',
|
|
34
34
|
EXAMS_EDIT: (id) => `/manage/exams/${id}/edit`,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type WfsfCorrectValue = string | string[];
|
|
2
|
+
export declare function cleanWfsfWord(str: unknown, caseSensitive?: boolean): string;
|
|
3
|
+
export declare function expandWfsfCorrectAlts(correctVal: unknown): string[];
|
|
4
|
+
export declare function formatWfsfCorrectLabel(correctVal: unknown): string;
|
|
5
|
+
export declare function isWfsfBlankCorrect(userWord: string, correctVal: unknown, caseSensitive?: boolean): boolean;
|
|
6
|
+
export declare function normalizeWfsfAnswersMap(raw: unknown): Record<string, string[]>;
|
|
7
|
+
export declare function compactWfsfAnswersMap(answers: Record<string, string | string[]>): Record<string, string[]>;
|
|
8
|
+
export declare function primaryWfsfAnswers(answers: Record<string, string | string[]>): Record<string, string>;
|
|
9
|
+
export declare function hasWfsfBlankAnswer(value: unknown): boolean;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function cleanWfsfWord(str, caseSensitive = false) {
|
|
2
|
+
const text = str == null ? '' : String(str);
|
|
3
|
+
const cleaned = text
|
|
4
|
+
.trim()
|
|
5
|
+
.replace(/[\p{P}]/gu, '')
|
|
6
|
+
.replace(/\s+/g, ' ')
|
|
7
|
+
.trim();
|
|
8
|
+
return caseSensitive ? cleaned : cleaned.toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
export function expandWfsfCorrectAlts(correctVal) {
|
|
11
|
+
if (Array.isArray(correctVal)) {
|
|
12
|
+
return correctVal.map((v) => String(v ?? '').trim()).filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
if (correctVal == null)
|
|
15
|
+
return [];
|
|
16
|
+
const valStr = String(correctVal);
|
|
17
|
+
if (valStr.includes('/')) {
|
|
18
|
+
return valStr.split('/').map((s) => s.trim()).filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
const trimmed = valStr.trim();
|
|
21
|
+
return trimmed ? [trimmed] : [];
|
|
22
|
+
}
|
|
23
|
+
export function formatWfsfCorrectLabel(correctVal) {
|
|
24
|
+
return expandWfsfCorrectAlts(correctVal).join(' / ');
|
|
25
|
+
}
|
|
26
|
+
export function isWfsfBlankCorrect(userWord, correctVal, caseSensitive = false) {
|
|
27
|
+
const userClean = cleanWfsfWord(userWord, caseSensitive);
|
|
28
|
+
if (!userClean)
|
|
29
|
+
return false;
|
|
30
|
+
return expandWfsfCorrectAlts(correctVal).some((alt) => cleanWfsfWord(alt, caseSensitive) === userClean);
|
|
31
|
+
}
|
|
32
|
+
export function normalizeWfsfAnswersMap(raw) {
|
|
33
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
34
|
+
return {};
|
|
35
|
+
const result = {};
|
|
36
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
37
|
+
const alts = expandWfsfCorrectAlts(value);
|
|
38
|
+
result[key] = alts.length > 0 ? alts : [''];
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
export function compactWfsfAnswersMap(answers) {
|
|
43
|
+
const result = {};
|
|
44
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
45
|
+
const alts = expandWfsfCorrectAlts(value);
|
|
46
|
+
if (alts.length > 0)
|
|
47
|
+
result[key] = alts;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
export function primaryWfsfAnswers(answers) {
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
54
|
+
result[key] = expandWfsfCorrectAlts(value)[0] || '';
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
export function hasWfsfBlankAnswer(value) {
|
|
59
|
+
return expandWfsfCorrectAlts(value).length > 0;
|
|
60
|
+
}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|