@tinyweb_dev/oe-exam-sdk 1.0.2 → 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.
Files changed (43) hide show
  1. package/dist/api/exam-questions.d.ts +1 -1
  2. package/dist/api/exam-questions.js +39 -1
  3. package/dist/api/exam-questions.types.d.ts +28 -0
  4. package/dist/components/exams/ExamCreator.d.ts +7 -1
  5. package/dist/components/exams/ExamCreator.js +57 -21
  6. package/dist/components/exams/ExamQuestionsPageContainer.d.ts +5 -0
  7. package/dist/components/exams/ExamQuestionsPageContainer.js +2 -2
  8. package/dist/components/exams/api.d.ts +1 -1
  9. package/dist/components/exams/index.d.ts +1 -1
  10. package/dist/components/exams/take/components/QuestionRenderer.d.ts +6 -1
  11. package/dist/components/exams/take/components/QuestionRenderer.js +2 -2
  12. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +1 -1
  13. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +1 -1
  14. package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.d.ts +7 -1
  15. package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +18 -9
  16. package/dist/components/exams/take/index.d.ts +6 -0
  17. package/dist/components/exams/take/index.js +5 -0
  18. package/dist/components/exams/take/utils/question-transformers.js +25 -2
  19. package/dist/components/questions/_shared/types/question-group.type.d.ts +0 -1
  20. package/dist/components/questions/_shared/types/question-group.type.js +1 -3
  21. package/dist/components/questions/_shared/types/question-group.utils.d.ts +2 -0
  22. package/dist/components/questions/_shared/types/question-group.utils.js +3 -0
  23. package/dist/components/questions/groups/ImageLabelWordBank.d.ts +1 -1
  24. package/dist/components/questions/groups/ImageLabelWordBank.js +1 -1
  25. package/dist/components/questions/question-bank/QuestionBankPickerDialog.d.ts +45 -0
  26. package/dist/components/questions/question-bank/QuestionBankPickerDialog.js +209 -0
  27. package/dist/components/questions/question-bank/QuestionBankPickerRow.d.ts +9 -0
  28. package/dist/components/questions/question-bank/QuestionBankPickerRow.js +20 -0
  29. package/dist/components/questions/question-bank/index.d.ts +1 -0
  30. package/dist/components/questions/question-bank/index.js +1 -0
  31. package/dist/components/questions/question-bank/question-bank-picker.utils.d.ts +55 -0
  32. package/dist/components/questions/question-bank/question-bank-picker.utils.js +408 -0
  33. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -1
  34. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -1
  35. package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +8 -1
  36. package/dist/components/questions/types/word-fill-structured-form/transform.js +4 -0
  37. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +1 -1
  38. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +1 -1
  39. package/dist/index.d.ts +4 -3
  40. package/dist/index.js +2 -1
  41. package/dist/shared/constants/ApiConstant.d.ts +1 -0
  42. package/dist/shared/constants/ApiConstant.js +1 -0
  43. 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(/&nbsp;/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
+ }
@@ -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.type';
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';
@@ -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
- const instruction = String(apiContent.instruction || answer.instruction || source.instruction || source.explanation || '');
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 { type ImageLabelData } from '../../questions/_shared/types/question-group.type';
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.type';
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';
@@ -86,6 +86,7 @@ export declare const API_ENDPOINTS: {
86
86
  readonly QUESTION_BANKS: "/api/v1/manage/question-banks";
87
87
  readonly QUESTION_BANK_DETAIL: (id: string) => string;
88
88
  readonly QUESTION_BANKS_STATISTICS: "/api/v1/manage/question-banks/statistics";
89
+ readonly QUESTION_BANKS_FILTERS: "/api/v1/manage/question-banks/filters";
89
90
  readonly SCHEDULES_SUMMARY: "/api/v1/manage/schedules/summary";
90
91
  readonly SCHEDULES_CALENDAR: "/api/v1/manage/schedules/calendar";
91
92
  readonly SCHEDULES: "/api/v1/manage/schedules";
@@ -95,6 +95,7 @@ export const API_ENDPOINTS = {
95
95
  QUESTION_BANKS: `/api/${API_VERSION}/manage/question-banks`,
96
96
  QUESTION_BANK_DETAIL: (id) => `/api/${API_VERSION}/manage/question-banks/${id}`,
97
97
  QUESTION_BANKS_STATISTICS: `/api/${API_VERSION}/manage/question-banks/statistics`,
98
+ QUESTION_BANKS_FILTERS: `/api/${API_VERSION}/manage/question-banks/filters`,
98
99
  // Schedules endpoints
99
100
  SCHEDULES_SUMMARY: `/api/${API_VERSION}/manage/schedules/summary`,
100
101
  SCHEDULES_CALENDAR: `/api/${API_VERSION}/manage/schedules/calendar`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyweb_dev/oe-exam-sdk",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Reusable OceanEdu question components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",