@tinyweb_dev/oe-exam-sdk 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/exam-questions.d.ts +1 -1
- package/dist/api/exam-questions.js +39 -1
- package/dist/api/exam-questions.types.d.ts +28 -0
- package/dist/components/exams/ExamCreator.d.ts +7 -1
- package/dist/components/exams/ExamCreator.js +57 -21
- package/dist/components/exams/ExamQuestionsPageContainer.d.ts +5 -0
- package/dist/components/exams/ExamQuestionsPageContainer.js +2 -2
- package/dist/components/exams/api.d.ts +1 -1
- package/dist/components/exams/index.d.ts +1 -1
- package/dist/components/exams/take/components/QuestionRenderer.d.ts +6 -1
- package/dist/components/exams/take/components/QuestionRenderer.js +2 -2
- 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.d.ts +7 -1
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +18 -9
- package/dist/components/exams/take/index.d.ts +6 -0
- package/dist/components/exams/take/index.js +5 -0
- package/dist/components/exams/take/utils/question-transformers.js +25 -2
- 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/groups/ImageLabelWordBank.d.ts +1 -1
- package/dist/components/questions/groups/ImageLabelWordBank.js +1 -1
- package/dist/components/questions/question-bank/QuestionBankPickerDialog.d.ts +45 -0
- package/dist/components/questions/question-bank/QuestionBankPickerDialog.js +209 -0
- package/dist/components/questions/question-bank/QuestionBankPickerRow.d.ts +9 -0
- package/dist/components/questions/question-bank/QuestionBankPickerRow.js +20 -0
- package/dist/components/questions/question-bank/index.d.ts +1 -0
- package/dist/components/questions/question-bank/index.js +1 -0
- package/dist/components/questions/question-bank/question-bank-picker.utils.d.ts +55 -0
- package/dist/components/questions/question-bank/question-bank-picker.utils.js +408 -0
- 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/WordFillStructuredFormCreator.js +16 -3
- package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +8 -1
- package/dist/components/questions/types/word-fill-structured-form/transform.js +4 -0
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +1 -1
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/shared/constants/ApiConstant.d.ts +1 -0
- package/dist/shared/constants/ApiConstant.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { BookOpen, FileText, Grid3X3, Image as ImageIcon, Volume2, } from 'lucide-react';
|
|
5
|
+
import { Label } from '../../../components/ui/label';
|
|
6
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../../components/ui/select';
|
|
7
|
+
export const QUESTION_BANK_FILTER_ALL_VALUE = '__all__';
|
|
8
|
+
/** Keep selected id visible even before cascade options finish loading. */
|
|
9
|
+
export function mergeFilterOptions(options, selectedId, selectedLabel) {
|
|
10
|
+
const id = selectedId.trim();
|
|
11
|
+
if (!id)
|
|
12
|
+
return options;
|
|
13
|
+
if (options.some((opt) => opt.id === id))
|
|
14
|
+
return options;
|
|
15
|
+
return [{ id, name: selectedLabel?.trim() || id }, ...options];
|
|
16
|
+
}
|
|
17
|
+
export function FilterSelect({ label, value, options, onChange, disabled, selectedLabel, }) {
|
|
18
|
+
const mergedOptions = useMemo(() => mergeFilterOptions(options, value, selectedLabel), [options, value, selectedLabel]);
|
|
19
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-[11px] font-semibold text-gray-500", children: label }), _jsxs(Select, { value: value || QUESTION_BANK_FILTER_ALL_VALUE, onValueChange: (v) => onChange(v === QUESTION_BANK_FILTER_ALL_VALUE ? '' : v), disabled: disabled, children: [_jsx(SelectTrigger, { className: "h-9 w-full bg-white", children: _jsx(SelectValue, { placeholder: `Tất cả ${label.toLowerCase()}` }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: QUESTION_BANK_FILTER_ALL_VALUE, children: "T\u1EA5t c\u1EA3" }), mergedOptions.map((opt) => (_jsx(SelectItem, { value: opt.id, children: opt.name }, opt.id)))] })] })] }));
|
|
20
|
+
}
|
|
21
|
+
/** Map plain string list (e.g. questionTypes) → FilterSelect options. */
|
|
22
|
+
export function toStringOptions(values) {
|
|
23
|
+
return values.map((value) => ({ id: value, name: questionTypeLabel(value) }));
|
|
24
|
+
}
|
|
25
|
+
/** Vietnamese labels for the most common question types in the bank. */
|
|
26
|
+
const QUESTION_TYPE_LABELS = {
|
|
27
|
+
CHOOSE_THE_CORRECT_ANSWER: 'Chọn đáp án đúng',
|
|
28
|
+
CHOOSE_THE_CORRECT_ANSWER_GROUP: 'Chọn đáp án đúng (nhóm)',
|
|
29
|
+
FILL_IN_BLANK: 'Điền vào chỗ trống',
|
|
30
|
+
FILL_IN_BLANK_GROUP: 'Điền chỗ trống (nhóm)',
|
|
31
|
+
WORD_FILL_STRUCTURED_FORM: 'Điền từ / cấu trúc',
|
|
32
|
+
READ_PASSAGE_AND_ANSWER_QUESTIONS: 'Đọc hiểu',
|
|
33
|
+
READ_DISPLAYED_CONTENT: 'Đọc nội dung hiển thị',
|
|
34
|
+
ANSWER_THE_QUESTION: 'Trả lời câu hỏi',
|
|
35
|
+
MATCH_BY_WRITING_ANSWER: 'Nối bằng cách viết đáp án',
|
|
36
|
+
SPEAKING_DESCRIBE_IMAGE: 'Nói: mô tả tranh',
|
|
37
|
+
SPEAKING_CONVERSATION: 'Nói: hội thoại',
|
|
38
|
+
GN_SPEAKING_INTERVIEW: 'Nói: phỏng vấn',
|
|
39
|
+
LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER: 'Nhìn tranh điền chỗ trống',
|
|
40
|
+
LOOK_PICTURE_CHOOSE_CORRECT_ANSWER: 'Nhìn tranh chọn đáp án',
|
|
41
|
+
LOOK_PICTURE_FILL_WORD_HINT: 'Nhìn tranh điền từ gợi ý',
|
|
42
|
+
LABEL_THE_PICTURE: 'Gắn nhãn hình ảnh',
|
|
43
|
+
MATCH_WORD_TO_PICTURE: 'Nối từ với tranh',
|
|
44
|
+
MATCH_WORD_TO_PICTURE_GROUP: 'Nối từ với tranh (nhóm)',
|
|
45
|
+
MATCHING_WITH_LINES_GROUP: 'Nối bằng đường kẻ (nhóm)',
|
|
46
|
+
MATCH_COLUMNS_TO_MAKE_SENTENCES: 'Ghép cột thành câu',
|
|
47
|
+
WORD_ORDERING_GROUP: 'Sắp xếp từ (nhóm)',
|
|
48
|
+
WORD_ORDER_AND_MATCH_GROUP: 'Sắp xếp từ & nối (nhóm)',
|
|
49
|
+
SORT_WORDS_INTO_CATEGORIES: 'Sắp xếp từ vào nhóm',
|
|
50
|
+
CHOOSE_THEN_ANSWER_GROUP: 'Chọn rồi trả lời (nhóm)',
|
|
51
|
+
ANSWER_THE_QUESTION_GROUP: 'Trả lời câu hỏi (nhóm)',
|
|
52
|
+
TRUE_FALSE_GROUP: 'Đúng / Sai (nhóm)',
|
|
53
|
+
TRUE_FALSE_CORRECT_GROUP: 'Đúng / Sai & sửa (nhóm)',
|
|
54
|
+
CROSS_OUT_WORD_GROUP: 'Gạch từ thừa (nhóm)',
|
|
55
|
+
FILL_MISSING_WORDS_IN_GRID: 'Điền chữ trong bảng',
|
|
56
|
+
CROSSWORD_PUZZLE: 'Ô chữ',
|
|
57
|
+
FIND_WORDS_IN_MATRIX: 'Tìm từ trong bảng',
|
|
58
|
+
WRITE_A_SHORT_LETTER: 'Viết thư ngắn',
|
|
59
|
+
WRITE_SHORT_PARAGRAPH: 'Viết đoạn văn ngắn',
|
|
60
|
+
WRITE_CORRECT_VERB_FORM: 'Viết dạng động từ',
|
|
61
|
+
SIMPLE_ANSWER: 'Trả lời ngắn',
|
|
62
|
+
SPONTANEOUS_QA_GROUP: 'Hỏi đáp tự do (nhóm)',
|
|
63
|
+
};
|
|
64
|
+
export function questionTypeLabel(type) {
|
|
65
|
+
return QUESTION_TYPE_LABELS[type] ?? type.replace(/_/g, ' ').toLowerCase();
|
|
66
|
+
}
|
|
67
|
+
var QuestionTypeVisualKind;
|
|
68
|
+
(function (QuestionTypeVisualKind) {
|
|
69
|
+
QuestionTypeVisualKind["IMAGE"] = "image";
|
|
70
|
+
QuestionTypeVisualKind["PASSAGE"] = "passage";
|
|
71
|
+
QuestionTypeVisualKind["GRID"] = "grid";
|
|
72
|
+
QuestionTypeVisualKind["TEXT"] = "text";
|
|
73
|
+
})(QuestionTypeVisualKind || (QuestionTypeVisualKind = {}));
|
|
74
|
+
const TYPE_VISUAL_KIND = {
|
|
75
|
+
LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER: QuestionTypeVisualKind.IMAGE,
|
|
76
|
+
LOOK_PICTURE_CHOOSE_CORRECT_ANSWER: QuestionTypeVisualKind.IMAGE,
|
|
77
|
+
LOOK_PICTURE_FILL_WORD_HINT: QuestionTypeVisualKind.IMAGE,
|
|
78
|
+
LABEL_THE_PICTURE: QuestionTypeVisualKind.IMAGE,
|
|
79
|
+
MATCH_WORD_TO_PICTURE: QuestionTypeVisualKind.IMAGE,
|
|
80
|
+
MATCH_WORD_TO_PICTURE_GROUP: QuestionTypeVisualKind.IMAGE,
|
|
81
|
+
MATCHING_WITH_LINES_GROUP: QuestionTypeVisualKind.IMAGE,
|
|
82
|
+
READ_PASSAGE_AND_ANSWER_QUESTIONS: QuestionTypeVisualKind.PASSAGE,
|
|
83
|
+
READ_PASSAGE_AND_COMPLETE_STORY: QuestionTypeVisualKind.PASSAGE,
|
|
84
|
+
FILL_MISSING_WORDS_IN_GRID: QuestionTypeVisualKind.GRID,
|
|
85
|
+
CROSSWORD_PUZZLE: QuestionTypeVisualKind.GRID,
|
|
86
|
+
FIND_WORDS_IN_MATRIX: QuestionTypeVisualKind.GRID,
|
|
87
|
+
};
|
|
88
|
+
/** Icon + tint per question type family — quick visual scanning in lists. */
|
|
89
|
+
export function QuestionTypeIcon({ type }) {
|
|
90
|
+
const kind = TYPE_VISUAL_KIND[type] ?? QuestionTypeVisualKind.TEXT;
|
|
91
|
+
switch (kind) {
|
|
92
|
+
case QuestionTypeVisualKind.IMAGE:
|
|
93
|
+
return _jsx(ImageIcon, { className: "h-4 w-4 text-blue-500" });
|
|
94
|
+
case QuestionTypeVisualKind.PASSAGE:
|
|
95
|
+
return _jsx(BookOpen, { className: "h-4 w-4 text-green-600" });
|
|
96
|
+
case QuestionTypeVisualKind.GRID:
|
|
97
|
+
return _jsx(Grid3X3, { className: "h-4 w-4 text-purple-500" });
|
|
98
|
+
default:
|
|
99
|
+
return _jsx(FileText, { className: "h-4 w-4 text-gray-400" });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export var QuestionDifficulty;
|
|
103
|
+
(function (QuestionDifficulty) {
|
|
104
|
+
QuestionDifficulty["EASY"] = "EASY";
|
|
105
|
+
QuestionDifficulty["MEDIUM"] = "MEDIUM";
|
|
106
|
+
QuestionDifficulty["HARD"] = "HARD";
|
|
107
|
+
})(QuestionDifficulty || (QuestionDifficulty = {}));
|
|
108
|
+
const DIFFICULTY_BADGE_CLASS = {
|
|
109
|
+
[QuestionDifficulty.EASY]: 'border-green-200 bg-green-50 text-green-700',
|
|
110
|
+
[QuestionDifficulty.MEDIUM]: 'border-amber-200 bg-amber-50 text-amber-700',
|
|
111
|
+
[QuestionDifficulty.HARD]: 'border-red-200 bg-red-50 text-red-700',
|
|
112
|
+
};
|
|
113
|
+
const DIFFICULTY_LABEL = {
|
|
114
|
+
[QuestionDifficulty.EASY]: 'Dễ',
|
|
115
|
+
[QuestionDifficulty.MEDIUM]: 'TB',
|
|
116
|
+
[QuestionDifficulty.HARD]: 'Khó',
|
|
117
|
+
};
|
|
118
|
+
function parseDifficulty(raw) {
|
|
119
|
+
const upper = raw?.trim().toUpperCase();
|
|
120
|
+
if (upper === QuestionDifficulty.EASY)
|
|
121
|
+
return QuestionDifficulty.EASY;
|
|
122
|
+
if (upper === QuestionDifficulty.MEDIUM)
|
|
123
|
+
return QuestionDifficulty.MEDIUM;
|
|
124
|
+
if (upper === QuestionDifficulty.HARD)
|
|
125
|
+
return QuestionDifficulty.HARD;
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
export function difficultyBadge(raw) {
|
|
129
|
+
const difficulty = parseDifficulty(raw);
|
|
130
|
+
if (!difficulty)
|
|
131
|
+
return null;
|
|
132
|
+
return {
|
|
133
|
+
label: DIFFICULTY_LABEL[difficulty],
|
|
134
|
+
className: DIFFICULTY_BADGE_CLASS[difficulty],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function firstStringOf(record, keys) {
|
|
138
|
+
for (const key of keys) {
|
|
139
|
+
const value = record[key];
|
|
140
|
+
if (typeof value === 'string' && value.trim())
|
|
141
|
+
return value.trim();
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
function asRecord(value) {
|
|
146
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
/** Strip HTML tags / entities so list rows show readable text, not markup. */
|
|
152
|
+
export function stripHtmlToText(raw) {
|
|
153
|
+
let text = raw
|
|
154
|
+
.replace(/<\s*br\s*\/?>/gi, ' ')
|
|
155
|
+
.replace(/<\/\s*(p|div|tr|li|h[1-6]|table)\s*>/gi, ' ')
|
|
156
|
+
.replace(/<[^>]+>/g, ' ')
|
|
157
|
+
.replace(/\{(\d+)\}/g, '___')
|
|
158
|
+
.replace(/ /gi, ' ')
|
|
159
|
+
.replace(/&/gi, '&')
|
|
160
|
+
.replace(/</gi, '<')
|
|
161
|
+
.replace(/>/gi, '>')
|
|
162
|
+
.replace(/"/gi, '"')
|
|
163
|
+
.replace(/'/g, "'")
|
|
164
|
+
.replace(/'/gi, "'");
|
|
165
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
166
|
+
return text;
|
|
167
|
+
}
|
|
168
|
+
const PREVIEW_TEXT_KEYS = [
|
|
169
|
+
'question',
|
|
170
|
+
'questionText',
|
|
171
|
+
'sentence',
|
|
172
|
+
'text',
|
|
173
|
+
'passage',
|
|
174
|
+
'prompt',
|
|
175
|
+
'instruction',
|
|
176
|
+
'mainTitle',
|
|
177
|
+
'title',
|
|
178
|
+
// Pearson WORD_FILL_STRUCTURED_FORM nests HTML under `content`.
|
|
179
|
+
'content',
|
|
180
|
+
];
|
|
181
|
+
const PREVIEW_MAX_LEN = 220;
|
|
182
|
+
function clampPreview(text, max = PREVIEW_MAX_LEN) {
|
|
183
|
+
if (text.length <= max)
|
|
184
|
+
return text;
|
|
185
|
+
return `${text.slice(0, max - 1).trimEnd()}…`;
|
|
186
|
+
}
|
|
187
|
+
function optionPreviewLabel(option) {
|
|
188
|
+
if (typeof option === 'string' && option.trim())
|
|
189
|
+
return option.trim();
|
|
190
|
+
const record = asRecord(option);
|
|
191
|
+
if (!record)
|
|
192
|
+
return null;
|
|
193
|
+
return firstStringOf(record, ['text', 'label', 'value']);
|
|
194
|
+
}
|
|
195
|
+
/** Build "A / B / C" snippet from options array when question stem is empty. */
|
|
196
|
+
function optionsPreview(options) {
|
|
197
|
+
if (!Array.isArray(options) || options.length === 0)
|
|
198
|
+
return null;
|
|
199
|
+
const labels = options
|
|
200
|
+
.map(optionPreviewLabel)
|
|
201
|
+
.filter((v) => Boolean(v))
|
|
202
|
+
.slice(0, 4);
|
|
203
|
+
if (labels.length === 0)
|
|
204
|
+
return null;
|
|
205
|
+
return labels.join(' · ');
|
|
206
|
+
}
|
|
207
|
+
function extractFromRecord(record) {
|
|
208
|
+
// Prefer human-readable fields; strip HTML when present.
|
|
209
|
+
for (const key of PREVIEW_TEXT_KEYS) {
|
|
210
|
+
const value = record[key];
|
|
211
|
+
if (typeof value !== 'string' || !value.trim())
|
|
212
|
+
continue;
|
|
213
|
+
const cleaned = stripHtmlToText(value);
|
|
214
|
+
if (cleaned)
|
|
215
|
+
return cleaned;
|
|
216
|
+
}
|
|
217
|
+
// Options-only MCQ (empty question stem).
|
|
218
|
+
const fromOptions = optionsPreview(record.options);
|
|
219
|
+
if (fromOptions)
|
|
220
|
+
return fromOptions;
|
|
221
|
+
// Word bank / labels.
|
|
222
|
+
if (Array.isArray(record.wordBank)) {
|
|
223
|
+
const words = record.wordBank
|
|
224
|
+
.map((w) => {
|
|
225
|
+
if (typeof w === 'string')
|
|
226
|
+
return w;
|
|
227
|
+
const r = asRecord(w);
|
|
228
|
+
return r ? firstStringOf(r, ['text', 'label', 'word']) : null;
|
|
229
|
+
})
|
|
230
|
+
.filter((v) => Boolean(v))
|
|
231
|
+
.slice(0, 5);
|
|
232
|
+
if (words.length > 0)
|
|
233
|
+
return `Từ: ${words.join(', ')}`;
|
|
234
|
+
}
|
|
235
|
+
// Crossword clues.
|
|
236
|
+
const clues = asRecord(record.clues);
|
|
237
|
+
if (clues) {
|
|
238
|
+
const across = Array.isArray(clues.across) ? clues.across : [];
|
|
239
|
+
const firstClue = asRecord(across[0]);
|
|
240
|
+
const clueText = firstClue ? firstStringOf(firstClue, ['clue']) : null;
|
|
241
|
+
if (clueText)
|
|
242
|
+
return clueText;
|
|
243
|
+
}
|
|
244
|
+
// Nested group items / subQuestions.
|
|
245
|
+
const nested = record.items ?? record.subQuestions;
|
|
246
|
+
if (Array.isArray(nested) && nested.length > 0) {
|
|
247
|
+
for (const raw of nested) {
|
|
248
|
+
const item = asRecord(raw);
|
|
249
|
+
if (!item)
|
|
250
|
+
continue;
|
|
251
|
+
const nestedContent = asRecord(item.content) ?? item;
|
|
252
|
+
const sub = extractFromRecord(nestedContent);
|
|
253
|
+
if (sub)
|
|
254
|
+
return sub;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// meta.instruction (group wrappers).
|
|
258
|
+
const meta = asRecord(record.meta);
|
|
259
|
+
if (meta) {
|
|
260
|
+
const instruction = firstStringOf(meta, ['instruction', 'title']);
|
|
261
|
+
if (instruction)
|
|
262
|
+
return stripHtmlToText(instruction);
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Human-readable preview for bank list rows.
|
|
268
|
+
* Handles Pearson HTML (`content.content`), group items, options-only MCQ.
|
|
269
|
+
*/
|
|
270
|
+
export function previewText(item) {
|
|
271
|
+
const content = item.content;
|
|
272
|
+
if (typeof content === 'string' && content.trim()) {
|
|
273
|
+
const cleaned = stripHtmlToText(content);
|
|
274
|
+
if (cleaned)
|
|
275
|
+
return clampPreview(cleaned);
|
|
276
|
+
}
|
|
277
|
+
const record = asRecord(content);
|
|
278
|
+
if (record) {
|
|
279
|
+
const extracted = extractFromRecord(record);
|
|
280
|
+
if (extracted)
|
|
281
|
+
return clampPreview(extracted);
|
|
282
|
+
}
|
|
283
|
+
// Title often embeds exercise name; better than a bare dash.
|
|
284
|
+
const title = item.title?.trim();
|
|
285
|
+
if (title)
|
|
286
|
+
return clampPreview(stripHtmlToText(title), 120);
|
|
287
|
+
return item.explanation?.trim() || '—';
|
|
288
|
+
}
|
|
289
|
+
/** Prefer content preview over XML-ish titles for the primary row heading. */
|
|
290
|
+
export function primaryDisplayText(item) {
|
|
291
|
+
const preview = previewText(item);
|
|
292
|
+
const title = item.title?.trim() ?? '';
|
|
293
|
+
// Title looks like a source file path → show content instead.
|
|
294
|
+
if (/\.xml\b/i.test(title) || /exercise-\d+/i.test(title)) {
|
|
295
|
+
return preview !== '—' ? preview : title;
|
|
296
|
+
}
|
|
297
|
+
if (title)
|
|
298
|
+
return title;
|
|
299
|
+
return preview;
|
|
300
|
+
}
|
|
301
|
+
function readMetaString(meta, keys) {
|
|
302
|
+
if (!meta)
|
|
303
|
+
return null;
|
|
304
|
+
for (const key of keys) {
|
|
305
|
+
const value = meta[key];
|
|
306
|
+
if (typeof value === 'string' && value.trim())
|
|
307
|
+
return value.trim();
|
|
308
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
309
|
+
return String(value);
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
/** Pretty exercise label: "01-exercise-1.xml" → "Ex 1". */
|
|
314
|
+
function formatExerciseLabel(assetName) {
|
|
315
|
+
if (!assetName)
|
|
316
|
+
return null;
|
|
317
|
+
const match = assetName.match(/exercise[-_]?(\d+)/i) ??
|
|
318
|
+
assetName.match(/^0*(\d+)[-_]/);
|
|
319
|
+
if (match?.[1])
|
|
320
|
+
return `Ex ${Number(match[1])}`;
|
|
321
|
+
// Strip .xml extension as fallback.
|
|
322
|
+
return assetName.replace(/\.xml$/i, '');
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Compact provenance for row footer (right side):
|
|
326
|
+
* "Unit 0 · 0.3 Favourites · Ex 1"
|
|
327
|
+
*/
|
|
328
|
+
export function provenanceFooterText(item) {
|
|
329
|
+
const meta = asRecord(item.meta);
|
|
330
|
+
const unit = readMetaString(meta, ['unitName', 'unitTitle']) ??
|
|
331
|
+
readMetaString(asRecord(meta?.unit), ['name', 'title']);
|
|
332
|
+
const lesson = readMetaString(meta, ['lessonName', 'lessonTitle']) ??
|
|
333
|
+
readMetaString(asRecord(meta?.lesson), ['name', 'title']);
|
|
334
|
+
const asset = readMetaString(meta, ['assetName', 'assetTitle']) ??
|
|
335
|
+
readMetaString(asRecord(meta?.asset), ['name', 'title']);
|
|
336
|
+
const exercise = formatExerciseLabel(asset);
|
|
337
|
+
const parts = [];
|
|
338
|
+
if (unit) {
|
|
339
|
+
// "0" / bare number → "Unit 0"; already labeled names stay as-is.
|
|
340
|
+
parts.push(/^\d+([.]\d+)?$/.test(unit) ? `Unit ${unit}` : unit);
|
|
341
|
+
}
|
|
342
|
+
if (lesson)
|
|
343
|
+
parts.push(lesson);
|
|
344
|
+
if (exercise)
|
|
345
|
+
parts.push(exercise);
|
|
346
|
+
if (parts.length === 0)
|
|
347
|
+
return null;
|
|
348
|
+
return parts.join(' · ');
|
|
349
|
+
}
|
|
350
|
+
/** Sub-item count for group types — shown as “n câu con”. */
|
|
351
|
+
export function subItemCount(item) {
|
|
352
|
+
const record = asRecord(item.content);
|
|
353
|
+
if (!record)
|
|
354
|
+
return null;
|
|
355
|
+
const items = record.items ?? record.subQuestions;
|
|
356
|
+
if (Array.isArray(items) && items.length > 1)
|
|
357
|
+
return items.length;
|
|
358
|
+
// WORD_FILL blanks counted via {n} placeholders in HTML content.
|
|
359
|
+
const html = typeof record.content === 'string' ? record.content : '';
|
|
360
|
+
if (html) {
|
|
361
|
+
const matches = html.match(/\{\d+\}/g);
|
|
362
|
+
if (matches && matches.length > 1)
|
|
363
|
+
return matches.length;
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
/** Detect media attachments to surface image/audio hints on list rows. */
|
|
368
|
+
export function contentMediaFlags(item) {
|
|
369
|
+
const record = asRecord(item.content);
|
|
370
|
+
if (!record)
|
|
371
|
+
return { hasImage: false, hasAudio: false };
|
|
372
|
+
const topImage = Boolean(firstStringOf(record, ['imageUrl', 'backgroundImageUrl']));
|
|
373
|
+
const topAudio = Boolean(firstStringOf(record, ['audioUrl']));
|
|
374
|
+
const meta = asRecord(record.meta);
|
|
375
|
+
const metaImage = Boolean(meta && firstStringOf(meta, ['imageUrl', 'backgroundImageUrl']));
|
|
376
|
+
const metaAudio = Boolean(meta && firstStringOf(meta, ['audioUrl']));
|
|
377
|
+
// Options may be image-based.
|
|
378
|
+
let optionImage = false;
|
|
379
|
+
if (Array.isArray(record.options)) {
|
|
380
|
+
optionImage = record.options.some((opt) => {
|
|
381
|
+
const r = asRecord(opt);
|
|
382
|
+
return Boolean(r && firstStringOf(r, ['imageUrl']));
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
// Group first item media.
|
|
386
|
+
let nestedImage = false;
|
|
387
|
+
let nestedAudio = false;
|
|
388
|
+
const nested = record.items ?? record.subQuestions;
|
|
389
|
+
if (Array.isArray(nested) && nested.length > 0) {
|
|
390
|
+
const first = asRecord(nested[0]);
|
|
391
|
+
const nestedContent = first ? (asRecord(first.content) ?? first) : null;
|
|
392
|
+
if (nestedContent) {
|
|
393
|
+
nestedImage = Boolean(firstStringOf(nestedContent, ['imageUrl', 'backgroundImageUrl']));
|
|
394
|
+
nestedAudio = Boolean(firstStringOf(nestedContent, ['audioUrl']));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
hasImage: topImage || metaImage || optionImage || nestedImage,
|
|
399
|
+
hasAudio: topAudio || metaAudio || nestedAudio,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/** Small inline icons for image/audio presence. */
|
|
403
|
+
export function MediaHints({ item }) {
|
|
404
|
+
const { hasImage, hasAudio } = contentMediaFlags(item);
|
|
405
|
+
if (!hasImage && !hasAudio)
|
|
406
|
+
return null;
|
|
407
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1 text-gray-400", children: [hasImage ? _jsx(ImageIcon, { className: "h-3.5 w-3.5" }) : null, hasAudio ? _jsx(Volume2, { className: "h-3.5 w-3.5" }) : null] }));
|
|
408
|
+
}
|
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';
|
package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js
CHANGED
|
@@ -99,6 +99,19 @@ const ColorToolbarBtn = ({ editor }) => {
|
|
|
99
99
|
: 'text-gray-600 hover:bg-gray-200 hover:text-gray-900'}`, children: [_jsx(Palette, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "h-3.5 w-3.5 rounded-full border border-gray-300 shadow-sm", style: { backgroundColor: currentColor || '#0f172a' } })] }), showMenu && (_jsxs("div", { className: "absolute left-0 top-full z-50 mt-1 flex flex-col gap-2 rounded-lg border border-gray-200 bg-white p-2.5 shadow-xl min-w-[170px]", children: [_jsx("div", { className: "text-[11px] font-medium text-gray-500 px-0.5", children: "M\u00E0u ch\u1EEF nhanh" }), _jsx("div", { className: "grid grid-cols-4 gap-1.5", children: COLOR_PRESETS.map((p) => (_jsx("button", { type: "button", title: p.label, onClick: () => handleSetColor(p.color), className: "h-6 w-6 rounded-full border border-gray-200 shadow-xs transition-transform hover:scale-110 focus:outline-none", style: { backgroundColor: p.color } }, p.color))) }), _jsxs("div", { className: "flex items-center justify-between gap-2 pt-1.5 border-t border-gray-100", children: [_jsxs("label", { className: "flex items-center gap-1.5 text-[11px] text-gray-600 cursor-pointer", children: [_jsx("span", { children: "T\u00F9y ch\u1ECDn:" }), _jsx("input", { type: "color", value: currentColor || '#000000', onChange: (e) => handleSetColor(e.target.value), className: "h-5 w-5 cursor-pointer rounded border-0 p-0" })] }), _jsx("button", { type: "button", onClick: () => handleSetColor(''), className: "text-[11px] font-medium text-red-500 hover:underline", children: "M\u1EB7c \u0111\u1ECBnh" })] })] }))] }));
|
|
100
100
|
};
|
|
101
101
|
const ToolbarSep = () => _jsx("div", { className: "mx-1 h-4 w-px bg-gray-200" });
|
|
102
|
+
const BLANK_STYLE_OPTIONS = [
|
|
103
|
+
{ value: 'letter', label: 'Letter', hint: 'Ô 1 ký tự' },
|
|
104
|
+
{ value: 'word', label: 'Word', hint: 'Từ ngắn' },
|
|
105
|
+
{ value: 'phrase', label: 'Phrase', hint: 'Cụm / câu' },
|
|
106
|
+
];
|
|
107
|
+
function BlankStyleSwitch({ value, onChange, }) {
|
|
108
|
+
return (_jsxs("div", { role: "radiogroup", "aria-label": "Ki\u1EC3u \u00F4 tr\u1ED1ng", className: "inline-flex flex-wrap items-center gap-1 rounded-md border border-gray-200 bg-white p-1", children: [BLANK_STYLE_OPTIONS.map((option) => {
|
|
109
|
+
const selected = value === option.value;
|
|
110
|
+
return (_jsxs("button", { type: "button", role: "radio", "aria-checked": selected, title: option.hint, onClick: () => onChange(option.value), className: `inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-sm font-medium transition-colors ${selected
|
|
111
|
+
? 'border border-teal-300 bg-teal-50 text-teal-700'
|
|
112
|
+
: 'border border-transparent text-gray-400 hover:bg-gray-50 hover:text-gray-600'}`, children: [option.label, _jsx("span", { className: `text-[10px] font-normal ${selected ? 'text-teal-600' : 'text-gray-400'}`, children: option.hint })] }, option.value));
|
|
113
|
+
}), value === 'translate' && (_jsxs("span", { role: "radio", "aria-checked": true, title: "Ch\u1EA5m LLM \u2014 gi\u1EEF nguy\u00EAn \u0111\u1EBFn khi ch\u1ECDn style kh\u00E1c", className: "inline-flex items-center gap-1.5 rounded border border-teal-300 bg-teal-50 px-2.5 py-1 text-sm font-medium text-teal-700", children: ["Translate", _jsx("span", { className: "text-[10px] font-normal text-teal-600", children: "D\u1ECBch (LLM)" })] }))] }));
|
|
114
|
+
}
|
|
102
115
|
const EditorToolbar = ({ editor }) => {
|
|
103
116
|
if (!editor)
|
|
104
117
|
return null;
|
|
@@ -126,7 +139,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
126
139
|
// Page rubric (Pearson draft.instruction). Prefer instruction; legacy load may use explanation.
|
|
127
140
|
const [instruction, setInstruction] = useState(initialData?.instruction || initialData?.explanation || '');
|
|
128
141
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
129
|
-
const [blankStyle, setBlankStyle] = useState(initialData?.blankStyle);
|
|
142
|
+
const [blankStyle, setBlankStyle] = useState(initialData?.blankStyle ?? 'word');
|
|
130
143
|
// ── Multiple Images & Audios ────────────────────────────────────────────────
|
|
131
144
|
const [imageUrls, setImageUrls] = useState(() => {
|
|
132
145
|
if (Array.isArray(initialData?.imageUrls) && initialData.imageUrls.length > 0) {
|
|
@@ -250,7 +263,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
250
263
|
: '');
|
|
251
264
|
}
|
|
252
265
|
setAnswers(normalizeWfsfAnswersMap(initialData?.answers || {}));
|
|
253
|
-
setBlankStyle(initialData?.blankStyle);
|
|
266
|
+
setBlankStyle(initialData?.blankStyle ?? 'word');
|
|
254
267
|
const nextMode = detectWfsfBankMode(initialData?.viewMode ?? viewModeProp, initialData?.wordBank);
|
|
255
268
|
setBankMode(nextMode);
|
|
256
269
|
if (nextMode === 'WITH_WORD_BANK') {
|
|
@@ -481,7 +494,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
481
494
|
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')
|
|
482
495
|
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
|
|
483
496
|
? 'border-teal-300 bg-teal-50 text-teal-700'
|
|
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) => {
|
|
497
|
+
: '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"] }), _jsx(BlankStyleSwitch, { value: blankStyle, onChange: setBlankStyle })] }), _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) => {
|
|
485
498
|
setAudioUrls((prev) => {
|
|
486
499
|
const next = [...prev];
|
|
487
500
|
next[index] = url;
|
|
@@ -50,6 +50,7 @@ export function mapQuestionToWfsfData(question) {
|
|
|
50
50
|
const source = asRecord(question);
|
|
51
51
|
const apiContent = asRecord(source.content);
|
|
52
52
|
const answer = asRecord(source.answer);
|
|
53
|
+
const meta = asRecord(source.meta);
|
|
53
54
|
const correctAnswer = asRecord(source.correctAnswer ??
|
|
54
55
|
source.correct_answer ??
|
|
55
56
|
answer.correctAnswer ??
|
|
@@ -58,7 +59,13 @@ export function mapQuestionToWfsfData(question) {
|
|
|
58
59
|
? parseFloat(String(source.total_points))
|
|
59
60
|
: answer.points ?? source.points ?? 1;
|
|
60
61
|
const points = Number.isFinite(Number(pointsRaw)) ? Number(pointsRaw) : 1;
|
|
61
|
-
|
|
62
|
+
// Pearson bank stores rubric on meta.instruction; school/API use content.instruction.
|
|
63
|
+
const instruction = String(apiContent.instruction ||
|
|
64
|
+
answer.instruction ||
|
|
65
|
+
source.instruction ||
|
|
66
|
+
meta.instruction ||
|
|
67
|
+
source.explanation ||
|
|
68
|
+
'');
|
|
62
69
|
const explanation = String(answer.explanation || '');
|
|
63
70
|
const content = resolveHtmlContent(apiContent, answer);
|
|
64
71
|
const rawWordBank = apiContent.wordBank ?? answer.wordBank;
|
|
@@ -35,6 +35,10 @@ export const transformWordFillStructuredForm = (question) => {
|
|
|
35
35
|
audioUrls,
|
|
36
36
|
content: answerData.content,
|
|
37
37
|
};
|
|
38
|
+
const instructionText = String(answerData.instruction ?? '').trim();
|
|
39
|
+
if (instructionText) {
|
|
40
|
+
apiContent.instruction = instructionText;
|
|
41
|
+
}
|
|
38
42
|
if (answerData.blankStyle) {
|
|
39
43
|
apiContent.blankStyle = answerData.blankStyle;
|
|
40
44
|
}
|
|
@@ -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
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export * as Results from './components/results';
|
|
|
6
6
|
export * as Themes from './components/themes';
|
|
7
7
|
export * as Shared from './shared';
|
|
8
8
|
export { buildExamTemplatePayload, createContestRoomsApi, createExamCreateApi, createExamEntryApi, createExamQuestionsApi, createExamQuestionsFetchApi, createExamTakingApi, createMockContestRoomsApi, createMockCreateExamApi, createMockExamEntryApi, createMockExamQuestionsApi, createMockExamTakingApi, createMockResultReviewApi, createResultReviewApi } from './api';
|
|
9
|
-
export type { AnswerGradingStatusItem, AnswersGradingStatusResult, ContestRoomListItem, ContestRoomRegistrationStatus, ContestRoomVisibility, ContestRoomsContestDetail, ContestRoomsFetchApiOptions, ContestRoomsSdkApi, CreateExamFetchApiOptions, CreateExamPayload, CreateExamSdkApi, ExamFormData, ExamLevelOption, ExamQuestionsSdkApi, ExamResponse, ExamTakingAnswerValue, ExamTakingAttempt, ExamTakingFetchApiOptions, ExamTakingQuestion, ExamTakingQuestionsResult, ExamTakingRoom, ExamTakingSdkApi, ExamTakingTemplatePart, ExamTemplatePayload, ExamWithTemplate, ExistingQuestionBank, FetchApiOptions, GradingSummary, QuestionSearchItem, SaveExamInput, SaveExamTakingAnswersInput, SaveQuestionInput, SaveQuestionResult, SearchQuestionBankInput, SearchQuestionBankResult, SubmitExamTakingAttemptResult, UpdateExamTemplateInput, UpdatePartAudioInput, UploadAudioAnswerResult } from './api';
|
|
9
|
+
export type { AnswerGradingStatusItem, AnswersGradingStatusResult, ContestRoomListItem, ContestRoomRegistrationStatus, ContestRoomVisibility, ContestRoomsContestDetail, ContestRoomsFetchApiOptions, ContestRoomsSdkApi, CreateExamFetchApiOptions, CreateExamPayload, CreateExamSdkApi, ExamFormData, ExamLevelOption, ExamQuestionsSdkApi, ExamResponse, ExamTakingAnswerValue, ExamTakingAttempt, ExamTakingFetchApiOptions, ExamTakingQuestion, ExamTakingQuestionsResult, ExamTakingRoom, ExamTakingSdkApi, ExamTakingTemplatePart, ExamTemplatePayload, ExamWithTemplate, ExistingQuestionBank, FetchApiOptions, GradingSummary, QuestionBankFiltersInput, QuestionBankFiltersResult, QuestionSearchItem, SaveExamInput, SaveExamTakingAnswersInput, SaveQuestionInput, SaveQuestionResult, SearchQuestionBankInput, SearchQuestionBankResult, SubmitExamTakingAttemptResult, UpdateExamTemplateInput, UpdatePartAudioInput, UploadAudioAnswerResult } from './api';
|
|
10
10
|
export { CreateExamPageContainer, ExamPreviewDialog, ExamPreviewMode, ExamQuestionsPageContainer, ExamTakingPageContainer, ExamViewDialog, StudentContestRoomsPageContainer, StudentExamEntryPageContainer } from './components/exams';
|
|
11
11
|
export type { CreateExamPageContainerProps, CreateExamTexts, ExamPreviewDialogProps, ExamQuestionsPageContainerProps, ExamTakingPageContainerProps, ExamViewDialogExam, ExamViewDialogProps, StudentContestRoomsPageContainerProps, StudentExamEntryPageContainerProps } from './components/exams';
|
|
12
12
|
export { ResultReviewPageContainer } from './components/results';
|
|
@@ -15,8 +15,8 @@ export { QuestionCreator } from './components/questions/creator/QuestionCreator'
|
|
|
15
15
|
export type { QuestionCreatorProps } from './components/questions/creator/QuestionCreator';
|
|
16
16
|
export { QuestionGroupCreator } from './components/questions/creator/QuestionGroupCreator';
|
|
17
17
|
export type { QuestionGroupCreatorProps } from './components/questions/creator/QuestionGroupCreator';
|
|
18
|
-
export { DifficultyLevel, EditQuestionBankDialog, QuestionBankEditorType, QuestionEditorRenderer, } from './components/questions/question-bank';
|
|
19
|
-
export type { EditQuestionBankDialogProps, QuestionBankEditItem, QuestionEditorChangeData, QuestionEditorRendererProps, SkillType as QuestionBankSkillType, UpdateQuestionBankDto, } from './components/questions/question-bank';
|
|
18
|
+
export { DifficultyLevel, EditQuestionBankDialog, QuestionBankEditorType, QuestionBankPickerDialog, QuestionEditorRenderer, } from './components/questions/question-bank';
|
|
19
|
+
export type { EditQuestionBankDialogProps, QuestionBankEditItem, QuestionBankLessonSuggestion, QuestionBankPickerDefaultFilters, QuestionBankPickerDialogProps, QuestionEditorChangeData, QuestionEditorRendererProps, SkillType as QuestionBankSkillType, UpdateQuestionBankDto, } from './components/questions/question-bank';
|
|
20
20
|
export { renderDedicatedComponent } from './components/questions/creator/dedicated-component-router';
|
|
21
21
|
export type { DedicatedComponentRouterProps, ValidationRef, } from './components/questions/creator/dedicated-component-router';
|
|
22
22
|
export { questionTypeRegistry } from './components/questions/creator/question-type-registry';
|
|
@@ -24,6 +24,7 @@ export type { QuestionTypeRegistration, RouterContext, } from './components/ques
|
|
|
24
24
|
export { QUESTION_TYPE_CONFIG, QUESTION_TYPES_WITH_DEDICATED_COMPONENTS } from './components/questions/_shared/config/question-types.config';
|
|
25
25
|
export { validateQuestion } from './components/questions/_shared/utils/question-validation';
|
|
26
26
|
export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
|
|
27
|
+
export { transformApiQuestionToFrontend, transformApiQuestionsToFrontend, } from './shared/lib/utils/question-reverse-transform';
|
|
27
28
|
export type { QuestionFormState } from './components/questions/_shared/hooks/useQuestionFormState';
|
|
28
29
|
export * from './components/questions/_shared/types';
|
|
29
30
|
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, SortWordsIntoCategoriesViewer, ChooseThenAnswerGroupViewer, } from './components/questions/viewer';
|
package/dist/index.js
CHANGED
|
@@ -14,11 +14,12 @@ export { CreateExamPageContainer, ExamPreviewDialog, ExamPreviewMode, ExamQuesti
|
|
|
14
14
|
export { ResultReviewPageContainer } from './components/results';
|
|
15
15
|
export { QuestionCreator } from './components/questions/creator/QuestionCreator';
|
|
16
16
|
export { QuestionGroupCreator } from './components/questions/creator/QuestionGroupCreator';
|
|
17
|
-
export { DifficultyLevel, EditQuestionBankDialog, QuestionBankEditorType, QuestionEditorRenderer, } from './components/questions/question-bank';
|
|
17
|
+
export { DifficultyLevel, EditQuestionBankDialog, QuestionBankEditorType, QuestionBankPickerDialog, QuestionEditorRenderer, } from './components/questions/question-bank';
|
|
18
18
|
export { renderDedicatedComponent } from './components/questions/creator/dedicated-component-router';
|
|
19
19
|
export { questionTypeRegistry } from './components/questions/creator/question-type-registry';
|
|
20
20
|
export { QUESTION_TYPE_CONFIG, QUESTION_TYPES_WITH_DEDICATED_COMPONENTS } from './components/questions/_shared/config/question-types.config';
|
|
21
21
|
export { validateQuestion } from './components/questions/_shared/utils/question-validation';
|
|
22
22
|
export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
|
|
23
|
+
export { transformApiQuestionToFrontend, transformApiQuestionsToFrontend, } from './shared/lib/utils/question-reverse-transform';
|
|
23
24
|
export * from './components/questions/_shared/types';
|
|
24
25
|
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, SortWordsIntoCategoriesViewer, ChooseThenAnswerGroupViewer, } from './components/questions/viewer';
|