@tinyweb_dev/oe-exam-sdk 1.0.0 → 1.0.1
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/api/exam-taking.types.d.ts +1 -0
- package/dist/components/exams/ExamCreator.d.ts +7 -1
- package/dist/components/exams/ExamCreator.js +57 -21
- package/dist/components/exams/ExamPreviewDialog.js +4 -2
- 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 +4 -4
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +3 -1
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +5 -2
- 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/exam-taking.utils.d.ts +1 -0
- package/dist/components/exams/take/exam-taking.utils.js +1 -0
- 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.d.ts +9 -1
- package/dist/components/exams/take/utils/question-transformers.js +68 -4
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.d.ts +6 -2
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +26 -5
- package/dist/components/questions/_shared/types/fill-in-blank.type.d.ts +9 -0
- package/dist/components/questions/_shared/types/question-group.type.d.ts +11 -0
- package/dist/components/questions/_shared/types/question-group.type.js +4 -1
- package/dist/components/questions/_shared/types/spontaneous-qa.type.d.ts +7 -0
- package/dist/components/questions/groups/ImageLabelGroupCard.d.ts +15 -0
- package/dist/components/questions/groups/ImageLabelGroupCard.js +42 -0
- package/dist/components/questions/groups/ImageLabelWordBank.d.ts +10 -0
- package/dist/components/questions/groups/ImageLabelWordBank.js +17 -0
- 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 +33 -9
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +6 -2
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +28 -5
- package/dist/components/questions/types/fill-in-blank/register.js +5 -0
- package/dist/components/questions/types/fill-in-blank/transform.js +4 -1
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.d.ts +11 -0
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +27 -0
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQAClient.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQAClient.js +15 -0
- package/dist/components/questions/types/spontaneous-qa/map-spontaneous-qa-data.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa/map-spontaneous-qa-data.js +33 -0
- 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/questions/types/word-ordering/WordOrderingClient.js +51 -33
- package/dist/components/questions/viewer/QuestionViewer.js +12 -1
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +3 -1
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +4 -2
- package/dist/components/results/renderers/review-question-body-dedicated.js +2 -2
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +8 -0
- 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/dist/shared/lib/rich-text.d.ts +4 -0
- package/dist/shared/lib/rich-text.js +25 -0
- package/dist/shared/lib/stores/examQuestionStore.d.ts +4 -0
- package/dist/shared/lib/utils/question-transform-types.d.ts +4 -0
- package/dist/shared/lib/utils/question-transform.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { Library, Loader2, Search } from 'lucide-react';
|
|
5
|
+
import { Button } from '../../../components/ui/button';
|
|
6
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../../../components/ui/dialog';
|
|
7
|
+
import { Input } from '../../../components/ui/input';
|
|
8
|
+
import { FilterSelect, mergeFilterOptions, toStringOptions, } from './question-bank-picker.utils';
|
|
9
|
+
import { QuestionBankPickerRow } from './QuestionBankPickerRow';
|
|
10
|
+
const PAGE_SIZE = 20;
|
|
11
|
+
const EMPTY_FILTERS = {
|
|
12
|
+
questionTypes: [],
|
|
13
|
+
books: [],
|
|
14
|
+
units: [],
|
|
15
|
+
lessons: [],
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Multi-select bank picker. Cascade filters use stable meta ids:
|
|
19
|
+
* bookId → unitId → lessonId (labels from meta.*Name).
|
|
20
|
+
* Default lesson scope = session lessonIds when dropdown is "Tất cả".
|
|
21
|
+
*/
|
|
22
|
+
export function QuestionBankPickerDialog({ open, onOpenChange, searchQuestions, getQuestionFilters, defaultFilters, lessonSuggestions = [], onConfirm, title = 'Chọn từ ngân hàng câu hỏi', description = 'Lọc theo sách / unit / lesson rồi chọn một hoặc nhiều câu.', confirmLabel = 'Thêm vào đề', }) {
|
|
23
|
+
const [bookId, setBookId] = useState('');
|
|
24
|
+
const [unitId, setUnitId] = useState('');
|
|
25
|
+
const [lessonId, setLessonId] = useState('');
|
|
26
|
+
const [questionType, setQuestionType] = useState('');
|
|
27
|
+
const [keyword, setKeyword] = useState('');
|
|
28
|
+
const [debouncedKeyword, setDebouncedKeyword] = useState('');
|
|
29
|
+
const [page, setPage] = useState(1);
|
|
30
|
+
const [items, setItems] = useState([]);
|
|
31
|
+
const [totalItems, setTotalItems] = useState(0);
|
|
32
|
+
const [loading, setLoading] = useState(false);
|
|
33
|
+
const [filtersLoading, setFiltersLoading] = useState(false);
|
|
34
|
+
const [filterOptions, setFilterOptions] = useState(EMPTY_FILTERS);
|
|
35
|
+
const [selected, setSelected] = useState(new Map());
|
|
36
|
+
const [error, setError] = useState(null);
|
|
37
|
+
const sessionLessonIds = useMemo(() => {
|
|
38
|
+
const fromDefault = (defaultFilters?.lessonIds ?? [])
|
|
39
|
+
.map((id) => id.trim())
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
if (fromDefault.length > 0)
|
|
42
|
+
return [...new Set(fromDefault)];
|
|
43
|
+
const fromSuggestions = lessonSuggestions
|
|
44
|
+
.map((s) => s.lessonId?.trim() || s.lesson?.trim() || '')
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
return [...new Set(fromSuggestions)];
|
|
47
|
+
}, [defaultFilters?.lessonIds, lessonSuggestions]);
|
|
48
|
+
const sessionLessonOptions = useMemo(() => {
|
|
49
|
+
const out = [];
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
for (const s of lessonSuggestions) {
|
|
52
|
+
const id = s.lessonId?.trim() || s.lesson?.trim() || '';
|
|
53
|
+
if (!id || seen.has(id))
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(id);
|
|
56
|
+
out.push({ id, name: s.label?.trim() || id });
|
|
57
|
+
}
|
|
58
|
+
for (const id of sessionLessonIds) {
|
|
59
|
+
if (seen.has(id))
|
|
60
|
+
continue;
|
|
61
|
+
seen.add(id);
|
|
62
|
+
out.push({ id, name: id });
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}, [lessonSuggestions, sessionLessonIds]);
|
|
66
|
+
// Prefill when dialog opens. Lesson/type empty = "Tất cả" (session lessons for search).
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!open)
|
|
69
|
+
return;
|
|
70
|
+
setBookId(defaultFilters?.bookId?.trim() ?? '');
|
|
71
|
+
setUnitId(defaultFilters?.unitId?.trim() ?? '');
|
|
72
|
+
setLessonId(defaultFilters?.lessonId?.trim() ?? '');
|
|
73
|
+
setQuestionType(defaultFilters?.questionType?.trim() ?? '');
|
|
74
|
+
setKeyword('');
|
|
75
|
+
setDebouncedKeyword('');
|
|
76
|
+
setPage(1);
|
|
77
|
+
setSelected(new Map());
|
|
78
|
+
setError(null);
|
|
79
|
+
}, [
|
|
80
|
+
open,
|
|
81
|
+
defaultFilters?.bookId,
|
|
82
|
+
defaultFilters?.unitId,
|
|
83
|
+
defaultFilters?.lessonId,
|
|
84
|
+
defaultFilters?.questionType,
|
|
85
|
+
]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const timer = window.setTimeout(() => setDebouncedKeyword(keyword.trim()), 300);
|
|
88
|
+
return () => window.clearTimeout(timer);
|
|
89
|
+
}, [keyword]);
|
|
90
|
+
// Cascade options: bookId → units; bookId+unitId → lessons.
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!open || !getQuestionFilters)
|
|
93
|
+
return;
|
|
94
|
+
let ignore = false;
|
|
95
|
+
setFiltersLoading(true);
|
|
96
|
+
void getQuestionFilters({
|
|
97
|
+
bookId: bookId || undefined,
|
|
98
|
+
unitId: unitId || undefined,
|
|
99
|
+
})
|
|
100
|
+
.then((result) => {
|
|
101
|
+
if (ignore)
|
|
102
|
+
return;
|
|
103
|
+
setFilterOptions(result);
|
|
104
|
+
})
|
|
105
|
+
.catch(() => {
|
|
106
|
+
if (!ignore)
|
|
107
|
+
setFilterOptions(EMPTY_FILTERS);
|
|
108
|
+
})
|
|
109
|
+
.finally(() => {
|
|
110
|
+
if (!ignore)
|
|
111
|
+
setFiltersLoading(false);
|
|
112
|
+
});
|
|
113
|
+
return () => {
|
|
114
|
+
ignore = true;
|
|
115
|
+
};
|
|
116
|
+
}, [open, getQuestionFilters, bookId, unitId]);
|
|
117
|
+
const lessonSelectOptions = useMemo(() => {
|
|
118
|
+
// BTVN session: only lessons taught that day — do not list the whole unit.
|
|
119
|
+
const base = sessionLessonOptions.length > 0
|
|
120
|
+
? sessionLessonOptions
|
|
121
|
+
: filterOptions.lessons;
|
|
122
|
+
return mergeFilterOptions(base, lessonId);
|
|
123
|
+
}, [sessionLessonOptions, filterOptions.lessons, lessonId]);
|
|
124
|
+
// Search by meta ids. When lesson = Tất cả and session has lessons → IN lessonIds.
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!open)
|
|
127
|
+
return;
|
|
128
|
+
let ignore = false;
|
|
129
|
+
setLoading(true);
|
|
130
|
+
setError(null);
|
|
131
|
+
const singleLesson = lessonId.trim();
|
|
132
|
+
const multiLessons = !singleLesson && sessionLessonIds.length > 0 ? sessionLessonIds : undefined;
|
|
133
|
+
void searchQuestions({
|
|
134
|
+
questionType: (questionType || undefined),
|
|
135
|
+
keyword: debouncedKeyword || undefined,
|
|
136
|
+
bookId: bookId.trim() || undefined,
|
|
137
|
+
unitId: unitId.trim() || undefined,
|
|
138
|
+
lessonId: singleLesson || undefined,
|
|
139
|
+
lessonIds: multiLessons,
|
|
140
|
+
page,
|
|
141
|
+
size: PAGE_SIZE,
|
|
142
|
+
})
|
|
143
|
+
.then((result) => {
|
|
144
|
+
if (ignore)
|
|
145
|
+
return;
|
|
146
|
+
setItems(result.data ?? []);
|
|
147
|
+
setTotalItems(result.meta?.totalItems ?? result.data?.length ?? 0);
|
|
148
|
+
})
|
|
149
|
+
.catch((err) => {
|
|
150
|
+
if (ignore)
|
|
151
|
+
return;
|
|
152
|
+
setItems([]);
|
|
153
|
+
setTotalItems(0);
|
|
154
|
+
setError(err instanceof Error ? err.message : 'Không tải được ngân hàng câu hỏi');
|
|
155
|
+
})
|
|
156
|
+
.finally(() => {
|
|
157
|
+
if (!ignore)
|
|
158
|
+
setLoading(false);
|
|
159
|
+
});
|
|
160
|
+
return () => {
|
|
161
|
+
ignore = true;
|
|
162
|
+
};
|
|
163
|
+
}, [
|
|
164
|
+
open,
|
|
165
|
+
searchQuestions,
|
|
166
|
+
questionType,
|
|
167
|
+
debouncedKeyword,
|
|
168
|
+
bookId,
|
|
169
|
+
unitId,
|
|
170
|
+
lessonId,
|
|
171
|
+
sessionLessonIds,
|
|
172
|
+
page,
|
|
173
|
+
]);
|
|
174
|
+
const totalPages = Math.max(1, Math.ceil(totalItems / PAGE_SIZE));
|
|
175
|
+
const questionTypeOptions = useMemo(() => toStringOptions(filterOptions.questionTypes), [filterOptions.questionTypes]);
|
|
176
|
+
const toggleItem = useCallback((item) => {
|
|
177
|
+
setSelected((prev) => {
|
|
178
|
+
const next = new Map(prev);
|
|
179
|
+
if (next.has(item.id))
|
|
180
|
+
next.delete(item.id);
|
|
181
|
+
else
|
|
182
|
+
next.set(item.id, item);
|
|
183
|
+
return next;
|
|
184
|
+
});
|
|
185
|
+
}, []);
|
|
186
|
+
const handleConfirm = () => {
|
|
187
|
+
onConfirm(Array.from(selected.values()));
|
|
188
|
+
onOpenChange(false);
|
|
189
|
+
};
|
|
190
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "flex h-[min(92vh,880px)] w-[min(96vw,920px)] max-w-4xl flex-col gap-0 overflow-hidden p-0", children: [_jsxs(DialogHeader, { className: "shrink-0 border-b border-gray-100 px-5 py-4", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2 text-base", children: [_jsx(Library, { className: "h-5 w-5 text-blue-600" }), title] }), _jsx(DialogDescription, { className: "text-xs text-gray-500", children: description })] }), _jsxs("div", { className: "shrink-0 space-y-3 border-b border-gray-100 bg-slate-50/70 px-5 py-3", children: [_jsxs("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-4", children: [_jsx(FilterSelect, { label: "S\u00E1ch", value: bookId, options: filterOptions.books, selectedLabel: defaultFilters?.bookLabel, onChange: (v) => {
|
|
191
|
+
setBookId(v);
|
|
192
|
+
setUnitId('');
|
|
193
|
+
setLessonId('');
|
|
194
|
+
setPage(1);
|
|
195
|
+
}, disabled: filtersLoading }), _jsx(FilterSelect, { label: "Unit", value: unitId, options: filterOptions.units, selectedLabel: defaultFilters?.unitLabel, onChange: (v) => {
|
|
196
|
+
setUnitId(v);
|
|
197
|
+
setLessonId('');
|
|
198
|
+
setPage(1);
|
|
199
|
+
}, disabled: filtersLoading || !bookId }), _jsx(FilterSelect, { label: "Lesson", value: lessonId, options: lessonSelectOptions, onChange: (v) => {
|
|
200
|
+
setLessonId(v);
|
|
201
|
+
setPage(1);
|
|
202
|
+
}, disabled: filtersLoading }), _jsx(FilterSelect, { label: "Lo\u1EA1i c\u00E2u", value: questionType, options: questionTypeOptions, onChange: (v) => {
|
|
203
|
+
setQuestionType(v);
|
|
204
|
+
setPage(1);
|
|
205
|
+
}, disabled: filtersLoading })] }), sessionLessonIds.length > 0 && !lessonId ? (_jsxs("p", { className: "text-[11px] text-slate-600", children: ["\u0110ang l\u1ECDc ", sessionLessonIds.length, " lesson trong bu\u1ED5i (T\u1EA5t c\u1EA3)."] })) : null, _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2 text-gray-400" }), _jsx(Input, { value: keyword, onChange: (e) => {
|
|
206
|
+
setKeyword(e.target.value);
|
|
207
|
+
setPage(1);
|
|
208
|
+
}, placeholder: "T\u00ECm theo m\u00E3 / ti\u00EAu \u0111\u1EC1\u2026", className: "h-9 pl-8" })] })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 py-2", children: loading ? (_jsxs("div", { className: "flex items-center justify-center gap-2 py-16 text-sm text-gray-500", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), "\u0110ang t\u1EA3i\u2026"] })) : error ? (_jsx("p", { className: "px-2 py-12 text-center text-sm text-red-600", children: error })) : items.length === 0 ? (_jsx("p", { className: "px-2 py-12 text-center text-sm text-gray-500", children: "Kh\u00F4ng c\u00F3 c\u00E2u h\u1ECFi kh\u1EDBp b\u1ED9 l\u1ECDc" })) : (_jsx("ul", { className: "space-y-1.5 pb-2", children: items.map((item) => (_jsx(QuestionBankPickerRow, { item: item, selected: selected.has(item.id), onToggle: toggleItem }, item.id))) })) }), _jsxs("div", { className: "flex shrink-0 items-center justify-between gap-2 border-t border-gray-100 px-5 py-2 text-xs text-gray-500", children: [_jsxs("span", { children: [totalItems, " c\u00E2u \u00B7 \u0111\u00E3 ch\u1ECDn ", selected.size] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: page <= 1 || loading, onClick: () => setPage((p) => Math.max(1, p - 1)), children: "Tr\u01B0\u1EDBc" }), _jsxs("span", { className: "px-2", children: [page, "/", totalPages] }), _jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: page >= totalPages || loading, onClick: () => setPage((p) => p + 1), children: "Sau" })] })] }), _jsxs(DialogFooter, { className: "shrink-0 border-t border-gray-100 bg-gray-50/80 px-5 py-3", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: "H\u1EE7y" }), _jsxs(Button, { type: "button", disabled: selected.size === 0, onClick: handleConfirm, children: [confirmLabel, selected.size > 0 ? ` (${selected.size})` : ''] })] })] }) }));
|
|
209
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { QuestionSearchItem } from '../../../api/exam-questions.types';
|
|
2
|
+
type QuestionBankPickerRowProps = {
|
|
3
|
+
item: QuestionSearchItem;
|
|
4
|
+
selected: boolean;
|
|
5
|
+
onToggle: (item: QuestionSearchItem) => void;
|
|
6
|
+
};
|
|
7
|
+
/** One multi-select row in the bank picker list. */
|
|
8
|
+
export declare function QuestionBankPickerRow({ item, selected, onToggle, }: QuestionBankPickerRowProps): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check } from 'lucide-react';
|
|
4
|
+
import { Badge } from '../../../components/ui/badge';
|
|
5
|
+
import { Checkbox } from '../../../components/ui/checkbox';
|
|
6
|
+
import { cn } from '../../../shared/lib/utils';
|
|
7
|
+
import { difficultyBadge, MediaHints, previewText, primaryDisplayText, provenanceFooterText, questionTypeLabel, QuestionTypeIcon, subItemCount, } from './question-bank-picker.utils';
|
|
8
|
+
/** One multi-select row in the bank picker list. */
|
|
9
|
+
export function QuestionBankPickerRow({ item, selected, onToggle, }) {
|
|
10
|
+
const difficulty = difficultyBadge(item.difficulty);
|
|
11
|
+
const subCount = subItemCount(item);
|
|
12
|
+
const typeName = String(item.questionType);
|
|
13
|
+
const heading = primaryDisplayText(item);
|
|
14
|
+
const body = previewText(item);
|
|
15
|
+
const showBody = body !== '—' && body !== heading;
|
|
16
|
+
const provenance = provenanceFooterText(item);
|
|
17
|
+
return (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onToggle(item), className: cn('flex w-full items-start gap-3 rounded-xl border px-3 py-2.5 text-left transition-colors', selected
|
|
18
|
+
? 'border-blue-300 bg-blue-50/80 ring-1 ring-blue-200'
|
|
19
|
+
: 'border-gray-200 bg-white hover:border-blue-200 hover:bg-slate-50'), children: [_jsx(Checkbox, { checked: selected, onCheckedChange: () => onToggle(item), className: "mt-0.5", onClick: (e) => e.stopPropagation() }), _jsx("div", { className: cn('mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg', selected ? 'bg-blue-100' : 'bg-slate-100'), children: _jsx(QuestionTypeIcon, { type: typeName }) }), _jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("p", { className: "line-clamp-2 min-w-0 flex-1 text-sm font-medium leading-snug text-gray-900", children: heading }), _jsx(MediaHints, { item: item })] }), showBody ? (_jsx("p", { className: "line-clamp-3 text-xs leading-relaxed text-gray-600", children: body })) : null, _jsxs("div", { className: "flex flex-wrap items-center gap-1.5 pt-0.5", children: [_jsx(Badge, { variant: "secondary", className: "text-[10px] font-normal text-gray-600", children: questionTypeLabel(typeName) }), difficulty ? (_jsx(Badge, { variant: "outline", className: cn('text-[10px] font-normal', difficulty.className), children: difficulty.label })) : null, item.skill ? (_jsx(Badge, { variant: "outline", className: "text-[10px] font-normal capitalize", children: item.skill.toLowerCase() })) : null, subCount ? (_jsxs(Badge, { variant: "outline", className: "text-[10px] font-normal", children: [subCount, " c\u00E2u con"] })) : null, provenance ? (_jsx("span", { className: "ml-auto max-w-[55%] truncate text-right text-[10px] font-medium text-gray-600", title: provenance, children: provenance })) : null] })] }), selected ? _jsx(Check, { className: "mt-0.5 h-4 w-4 shrink-0 text-blue-600" }) : null] }) }));
|
|
20
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { EditQuestionBankDialog } from './EditQuestionBankDialog';
|
|
2
2
|
export { QuestionEditorRenderer, QuestionBankEditorType } from './QuestionEditorRenderer';
|
|
3
|
+
export { QuestionBankPickerDialog, type QuestionBankLessonSuggestion, type QuestionBankPickerDefaultFilters, type QuestionBankPickerDialogProps, } from './QuestionBankPickerDialog';
|
|
3
4
|
export { DifficultyLevel, DIFFICULTY_LEVELS, parseDifficultyLevel } from './difficulty';
|
|
4
5
|
export type { EditQuestionBankDialogProps, QuestionBankEditItem, QuestionEditorChangeData, QuestionEditorRendererProps, SkillType, UpdateQuestionBankDto, } from './types';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { EditQuestionBankDialog } from './EditQuestionBankDialog';
|
|
2
2
|
export { QuestionEditorRenderer, QuestionBankEditorType } from './QuestionEditorRenderer';
|
|
3
|
+
export { QuestionBankPickerDialog, } from './QuestionBankPickerDialog';
|
|
3
4
|
export { DifficultyLevel, DIFFICULTY_LEVELS, parseDifficultyLevel } from './difficulty';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { QuestionBankFilterOption, QuestionSearchItem } from '../../../api/exam-questions.types';
|
|
2
|
+
export declare const QUESTION_BANK_FILTER_ALL_VALUE = "__all__";
|
|
3
|
+
/** Keep selected id visible even before cascade options finish loading. */
|
|
4
|
+
export declare function mergeFilterOptions(options: QuestionBankFilterOption[], selectedId: string, selectedLabel?: string): QuestionBankFilterOption[];
|
|
5
|
+
export declare function FilterSelect({ label, value, options, onChange, disabled, selectedLabel, }: {
|
|
6
|
+
label: string;
|
|
7
|
+
/** Selected option id (or questionType string). */
|
|
8
|
+
value: string;
|
|
9
|
+
options: QuestionBankFilterOption[];
|
|
10
|
+
onChange: (value: string) => void;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
/** Display label when value is not yet in options (prefill). */
|
|
13
|
+
selectedLabel?: string;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
15
|
+
/** Map plain string list (e.g. questionTypes) → FilterSelect options. */
|
|
16
|
+
export declare function toStringOptions(values: string[]): QuestionBankFilterOption[];
|
|
17
|
+
export declare function questionTypeLabel(type: string): string;
|
|
18
|
+
/** Icon + tint per question type family — quick visual scanning in lists. */
|
|
19
|
+
export declare function QuestionTypeIcon({ type }: {
|
|
20
|
+
type: string;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
22
|
+
export declare enum QuestionDifficulty {
|
|
23
|
+
EASY = "EASY",
|
|
24
|
+
MEDIUM = "MEDIUM",
|
|
25
|
+
HARD = "HARD"
|
|
26
|
+
}
|
|
27
|
+
export declare function difficultyBadge(raw?: string): {
|
|
28
|
+
label: string;
|
|
29
|
+
className: string;
|
|
30
|
+
} | null;
|
|
31
|
+
/** Strip HTML tags / entities so list rows show readable text, not markup. */
|
|
32
|
+
export declare function stripHtmlToText(raw: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Human-readable preview for bank list rows.
|
|
35
|
+
* Handles Pearson HTML (`content.content`), group items, options-only MCQ.
|
|
36
|
+
*/
|
|
37
|
+
export declare function previewText(item: QuestionSearchItem): string;
|
|
38
|
+
/** Prefer content preview over XML-ish titles for the primary row heading. */
|
|
39
|
+
export declare function primaryDisplayText(item: QuestionSearchItem): string;
|
|
40
|
+
/**
|
|
41
|
+
* Compact provenance for row footer (right side):
|
|
42
|
+
* "Unit 0 · 0.3 Favourites · Ex 1"
|
|
43
|
+
*/
|
|
44
|
+
export declare function provenanceFooterText(item: QuestionSearchItem): string | null;
|
|
45
|
+
/** Sub-item count for group types — shown as “n câu con”. */
|
|
46
|
+
export declare function subItemCount(item: QuestionSearchItem): number | null;
|
|
47
|
+
/** Detect media attachments to surface image/audio hints on list rows. */
|
|
48
|
+
export declare function contentMediaFlags(item: QuestionSearchItem): {
|
|
49
|
+
hasImage: boolean;
|
|
50
|
+
hasAudio: boolean;
|
|
51
|
+
};
|
|
52
|
+
/** Small inline icons for image/audio presence. */
|
|
53
|
+
export declare function MediaHints({ item }: {
|
|
54
|
+
item: QuestionSearchItem;
|
|
55
|
+
}): import("react").JSX.Element;
|