@tinyweb_dev/oe-exam-sdk 0.2.14 → 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/README.md +12 -0
- 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/ChooseTheCorrectAnswerClient.js +1 -2
- 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
|
@@ -15,13 +15,31 @@ import { blankHasAnswer, compactFillInBlankAnswers, formatBlankAnswersDisplay, e
|
|
|
15
15
|
import { useDebouncedCallback, usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
16
16
|
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
17
17
|
import { BasicQuestionGroupCard } from '../../groups/BasicQuestionGroupCard';
|
|
18
|
+
import { ImageLabelGroupCard } from '../../groups/ImageLabelGroupCard';
|
|
18
19
|
import { useBasicQuestionGroup } from '../../_shared/hooks/useBasicQuestionGroup';
|
|
19
|
-
function
|
|
20
|
+
function resolveInitialImageLabels(initialData, suggestedAnswers) {
|
|
21
|
+
const fromPayload = initialData?.imageLabels || initialData?.questionGroup?.payload?.imageLabels;
|
|
22
|
+
if (Array.isArray(fromPayload) && fromPayload.length > 0) {
|
|
23
|
+
return fromPayload.map((item) => ({
|
|
24
|
+
imageUrl: item?.imageUrl || '',
|
|
25
|
+
label: item?.label || '',
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
if (suggestedAnswers?.length) {
|
|
29
|
+
return suggestedAnswers.map((label) => ({ imageUrl: '', label }));
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
function FillInBlankCreatorContent({ initialData, onChange, externalErrors, validationRef, groups, questionIndexInPart, partId, questionConfig, groupType, suggestedAnswers, headerExtra, }) {
|
|
34
|
+
const effectiveGroupType = (groupType || 'BASIC').toUpperCase();
|
|
35
|
+
const isImageLabelGroup = effectiveGroupType === 'IMAGE_LABEL';
|
|
36
|
+
const initialImageLabels = resolveInitialImageLabels(initialData, suggestedAnswers);
|
|
20
37
|
// ============ GROUP (shared title / content / image) ============
|
|
21
|
-
const { isGrouped, currentGroupNumber, groupTitle, groupSubtitle, groupContent, imageUrl: groupImageUrl, setGroupTitle, setGroupSubtitle, setGroupContent, setImageUrl: setGroupImageUrl, } = useBasicQuestionGroup({
|
|
38
|
+
const { isGrouped, currentGroupNumber, isFirstInGroup, groupTitle, groupSubtitle, groupContent, imageUrl: groupImageUrl, imageLabels, setGroupTitle, setGroupSubtitle, setGroupContent, setImageUrl: setGroupImageUrl, setImageLabels, } = useBasicQuestionGroup({
|
|
22
39
|
partId,
|
|
23
40
|
groups,
|
|
24
41
|
questionIndexInPart,
|
|
42
|
+
imageLabelCount: isImageLabelGroup ? (initialImageLabels.length || 1) : 0,
|
|
25
43
|
initialData: {
|
|
26
44
|
title: (() => {
|
|
27
45
|
if (initialData?.content?.passageTitle)
|
|
@@ -60,6 +78,7 @@ function FillInBlankCreatorContent({ initialData, onChange, externalErrors, vali
|
|
|
60
78
|
return initialData.groupImageUrl;
|
|
61
79
|
return '';
|
|
62
80
|
})(),
|
|
81
|
+
imageLabels: initialImageLabels,
|
|
63
82
|
},
|
|
64
83
|
});
|
|
65
84
|
// ============ STATE ============
|
|
@@ -232,10 +251,11 @@ function FillInBlankCreatorContent({ initialData, onChange, externalErrors, vali
|
|
|
232
251
|
groupContent: isGrouped ? groupContent : '',
|
|
233
252
|
groupNumber: currentGroupNumber,
|
|
234
253
|
groups,
|
|
254
|
+
...(isGrouped && isImageLabelGroup ? { imageLabels } : {}),
|
|
235
255
|
}),
|
|
236
256
|
};
|
|
237
257
|
}
|
|
238
|
-
}, [validationRef, question, answers, explanation, points, isGrouped, imageUrl, groupImageUrl, audioUrl, includesAudio, showHints, hints, groupTitle, groupSubtitle, groupContent, currentGroupNumber, groups, isExample, validateForm]);
|
|
258
|
+
}, [validationRef, question, answers, explanation, points, isGrouped, imageUrl, groupImageUrl, audioUrl, includesAudio, showHints, hints, groupTitle, groupSubtitle, groupContent, currentGroupNumber, groups, isExample, validateForm, isImageLabelGroup, imageLabels]);
|
|
239
259
|
// ============ SYNC INITIAL DATA ============
|
|
240
260
|
useEffect(() => {
|
|
241
261
|
const initialDataChanged = JSON.stringify(previousInitialDataRef.current) !== JSON.stringify(initialData);
|
|
@@ -290,6 +310,7 @@ function FillInBlankCreatorContent({ initialData, onChange, externalErrors, vali
|
|
|
290
310
|
groupContent,
|
|
291
311
|
groupNumber: currentGroupNumber,
|
|
292
312
|
groups,
|
|
313
|
+
...(isImageLabelGroup ? { imageLabels } : {}),
|
|
293
314
|
});
|
|
294
315
|
previousPayloadRef.current = currentPayload;
|
|
295
316
|
return;
|
|
@@ -310,13 +331,14 @@ function FillInBlankCreatorContent({ initialData, onChange, externalErrors, vali
|
|
|
310
331
|
groupContent: isGrouped ? groupContent : '',
|
|
311
332
|
groupNumber: currentGroupNumber,
|
|
312
333
|
groups,
|
|
334
|
+
...(isGrouped && isImageLabelGroup ? { imageLabels } : {}),
|
|
313
335
|
};
|
|
314
336
|
const payloadString = JSON.stringify(payload);
|
|
315
337
|
if (previousPayloadRef.current !== payloadString) {
|
|
316
338
|
previousPayloadRef.current = payloadString;
|
|
317
339
|
debouncedOnChange(payload);
|
|
318
340
|
}
|
|
319
|
-
}, [question, answers, explanation, points, isGrouped, imageUrl, groupImageUrl, audioUrl, includesAudio, showHints, hints, isExample, groupTitle, groupSubtitle, groupContent, currentGroupNumber, groups, debouncedOnChange]);
|
|
341
|
+
}, [question, answers, explanation, points, isGrouped, imageUrl, groupImageUrl, audioUrl, includesAudio, showHints, hints, isExample, groupTitle, groupSubtitle, groupContent, currentGroupNumber, groups, debouncedOnChange, isImageLabelGroup, imageLabels]);
|
|
320
342
|
// Ensure blank slot exists then update alternative at altIndex
|
|
321
343
|
const updateBlankAnswer = (blankIndex, altIndex, value) => {
|
|
322
344
|
setAnswers((prev) => {
|
|
@@ -394,11 +416,12 @@ function FillInBlankCreatorContent({ initialData, onChange, externalErrors, vali
|
|
|
394
416
|
groupContent: isGrouped ? groupContent : '',
|
|
395
417
|
groupImageUrl: isGrouped ? (groupDisplayPreviewUrl || groupImageUrl) : '',
|
|
396
418
|
groupNumber: isGrouped ? currentGroupNumber : undefined,
|
|
419
|
+
...(isGrouped && isImageLabelGroup ? { imageLabels } : {}),
|
|
397
420
|
};
|
|
398
421
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(Card, { children: _jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx(CardTitle, { children: "Fill In Blank (Preview)" }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Quay l\u1EA1i Edit Mode"] })] }) }) }), _jsx(FillInBlankClient, { questionData: questionData, questionConfig: questionConfig, autoFillCorrectAnswers: true, isReviewMode: false, explanation: explanation })] }));
|
|
399
422
|
}
|
|
400
423
|
// ============ CREATOR UI ============
|
|
401
|
-
return (_jsxs("div", { className: "space-y-4", children: [isGrouped && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, uploadIdPrefix: `fill-in-blank-${partId ?? 'group'}` })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 shadow-sm", children: _jsx(Type, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n v\u00E0o ch\u1ED7 tr\u1ED1ng" }), _jsx("p", { className: "text-xs text-gray-500", children: "H\u1ED7 tr\u1EE3 nhi\u1EC1u ch\u1ED7 tr\u1ED1ng trong m\u1ED9t c\u00E2u h\u1ECFi" })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [headerExtra, _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: !question?.trim() || placeholderIndices.length === 0, className: "gap-2 border-indigo-200 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-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: "flex flex-wrap items-center gap-6", children: [_jsx(PointsInput, { value: points, onChange: (e) => setPoints(parseFloat(e.target.value) || 1) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "show-suggested-words", checked: showHints, onCheckedChange: setShowHints }), _jsx(Label, { htmlFor: "show-suggested-words", className: "cursor-pointer text-sm font-medium text-gray-700", children: "Hi\u1EC3n th\u1ECB t\u1EEB g\u1EE3i \u00FD" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "isExample", checked: isExample, onCheckedChange: setIsExample }), _jsx(Label, { htmlFor: "isExample", className: "cursor-pointer text-sm font-medium text-gray-700", children: "\u0110\u00E1nh d\u1EA5u l\u00E0 c\u00E2u h\u1ECFi Example" })] })] }), _jsx("div", { className: "rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-5 shadow-sm", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100", children: _jsx(Lightbulb, { className: "h-4 w-4 text-blue-600" }) }), _jsxs("div", { className: "text-sm text-blue-900", children: [_jsx("p", { className: "font-semibold text-blue-800", children: "H\u01B0\u1EDBng d\u1EABn t\u1EA1o c\u00E2u h\u1ECFi" }), _jsxs("ul", { className: "mt-2 space-y-1.5 text-blue-700", children: [_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "S\u1EED d\u1EE5ng ", _jsx("code", { className: "mx-1 rounded bg-blue-100 px-1.5 py-0.5 font-mono text-blue-800", children: '{0}' }), ", ", _jsx("code", { className: "mx-1 rounded bg-blue-100 px-1.5 py-0.5 font-mono text-blue-800", children: '{1}' }), ", ", _jsx("code", { className: "mx-1 rounded bg-blue-100 px-1.5 py-0.5 font-mono text-blue-800", children: '{2}' }), "... \u0111\u1EC3 \u0111\u00E1nh d\u1EA5u c\u00E1c ch\u1ED7 tr\u1ED1ng"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "H\u1EC7 th\u1ED1ng s\u1EBD t\u1EF1 \u0111\u1ED9ng t\u1EA1o \u00F4 nh\u1EADp \u0111\u00E1p \u00E1n cho m\u1ED7i ch\u1ED7 tr\u1ED1ng"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "V\u00ED d\u1EE5: \u201CHe ", '{0}', " 10 years old and she ", '{1}', " 12.\u201D"] })] })] })] }) }), !isGrouped && (_jsxs("div", { className: "space-y-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("span", { className: "text-sm font-normal text-gray-500", children: "(T\u00F9y ch\u1ECDn)" })] }), _jsx(FileUpload, { id: "fill-in-blank-image", label: "", accept: "image/*", value: imageUrl || '', onChange: (url) => {
|
|
424
|
+
return (_jsxs("div", { className: "space-y-4", children: [isGrouped && isImageLabelGroup && (_jsx(ImageLabelGroupCard, { groupNumber: currentGroupNumber, imageLabels: imageLabels, onImageLabelsChange: setImageLabels, title: groupTitle, onTitleChange: setGroupTitle, isReadOnly: !isFirstInGroup, uploadIdPrefix: `fill-in-blank-${partId ?? 'image-label'}` })), isGrouped && !isImageLabelGroup && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, uploadIdPrefix: `fill-in-blank-${partId ?? 'group'}` })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 shadow-sm", children: _jsx(Type, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n v\u00E0o ch\u1ED7 tr\u1ED1ng" }), _jsx("p", { className: "text-xs text-gray-500", children: "H\u1ED7 tr\u1EE3 nhi\u1EC1u ch\u1ED7 tr\u1ED1ng trong m\u1ED9t c\u00E2u h\u1ECFi" })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [headerExtra, _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: !question?.trim() || placeholderIndices.length === 0, className: "gap-2 border-indigo-200 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-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: "flex flex-wrap items-center gap-6", children: [_jsx(PointsInput, { value: points, onChange: (e) => setPoints(parseFloat(e.target.value) || 1) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "show-suggested-words", checked: showHints, onCheckedChange: setShowHints }), _jsx(Label, { htmlFor: "show-suggested-words", className: "cursor-pointer text-sm font-medium text-gray-700", children: "Hi\u1EC3n th\u1ECB t\u1EEB g\u1EE3i \u00FD" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "isExample", checked: isExample, onCheckedChange: setIsExample }), _jsx(Label, { htmlFor: "isExample", className: "cursor-pointer text-sm font-medium text-gray-700", children: "\u0110\u00E1nh d\u1EA5u l\u00E0 c\u00E2u h\u1ECFi Example" })] })] }), _jsx("div", { className: "rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-5 shadow-sm", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100", children: _jsx(Lightbulb, { className: "h-4 w-4 text-blue-600" }) }), _jsxs("div", { className: "text-sm text-blue-900", children: [_jsx("p", { className: "font-semibold text-blue-800", children: "H\u01B0\u1EDBng d\u1EABn t\u1EA1o c\u00E2u h\u1ECFi" }), _jsxs("ul", { className: "mt-2 space-y-1.5 text-blue-700", children: [_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "S\u1EED d\u1EE5ng ", _jsx("code", { className: "mx-1 rounded bg-blue-100 px-1.5 py-0.5 font-mono text-blue-800", children: '{0}' }), ", ", _jsx("code", { className: "mx-1 rounded bg-blue-100 px-1.5 py-0.5 font-mono text-blue-800", children: '{1}' }), ", ", _jsx("code", { className: "mx-1 rounded bg-blue-100 px-1.5 py-0.5 font-mono text-blue-800", children: '{2}' }), "... \u0111\u1EC3 \u0111\u00E1nh d\u1EA5u c\u00E1c ch\u1ED7 tr\u1ED1ng"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "H\u1EC7 th\u1ED1ng s\u1EBD t\u1EF1 \u0111\u1ED9ng t\u1EA1o \u00F4 nh\u1EADp \u0111\u00E1p \u00E1n cho m\u1ED7i ch\u1ED7 tr\u1ED1ng"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "V\u00ED d\u1EE5: \u201CHe ", '{0}', " 10 years old and she ", '{1}', " 12.\u201D"] })] })] })] }) }), !isGrouped && (_jsxs("div", { className: "space-y-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("span", { className: "text-sm font-normal text-gray-500", children: "(T\u00F9y ch\u1ECDn)" })] }), _jsx(FileUpload, { id: "fill-in-blank-image", label: "", accept: "image/*", value: imageUrl || '', onChange: (url) => {
|
|
402
425
|
setImageUrl(url);
|
|
403
426
|
}, onPresignedUrlChange: setImagePreviewUrl, maxSize: 5, placeholder: "Upload \u1EA3nh ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }), displayPreviewUrl && (_jsx(ImagePreview, { src: displayPreviewUrl, alt: "Preview", className: "mt-2 h-48 w-full max-w-md rounded-lg border border-gray-200" }))] })), includesAudio && (_jsxs("div", { className: "space-y-2", 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-indigo-500" }), "Audio c\u00E2u h\u1ECFi"] }), _jsx(FileUpload, { id: "fill-in-blank-audio", label: "", accept: "audio/*", value: audioUrl || '', onChange: (url) => {
|
|
404
427
|
setAudioUrl(url);
|
|
@@ -130,6 +130,9 @@ export const fillInBlankRegistration = {
|
|
|
130
130
|
groups: answer?.groups || initialData?.groups,
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
|
+
if (creatorData) {
|
|
134
|
+
creatorData.imageLabels = groupPayload?.imageLabels || answer?.imageLabels || undefined;
|
|
135
|
+
}
|
|
133
136
|
return creatorData;
|
|
134
137
|
},
|
|
135
138
|
getExtraProps: (ctx) => ({
|
|
@@ -139,6 +142,8 @@ export const fillInBlankRegistration = {
|
|
|
139
142
|
questionIndexInPart: ctx.questionIndexInPart,
|
|
140
143
|
partId: ctx.partId,
|
|
141
144
|
questionConfig: ctx.questionConfig,
|
|
145
|
+
groupType: ctx.groupType,
|
|
146
|
+
suggestedAnswers: ctx.suggestedAnswers,
|
|
142
147
|
}),
|
|
143
148
|
wrapOnSave: (data, ctx) => ({
|
|
144
149
|
type: ctx.questionType,
|
|
@@ -4,13 +4,16 @@ export const transformFillInBlank = (question) => {
|
|
|
4
4
|
// Helper: build basicGroupData if any group field is present.
|
|
5
5
|
// NOTE: only `groupImageUrl` belongs to the group card — the per-question `imageUrl`
|
|
6
6
|
// goes into content (same split as CHOOSE_THE_CORRECT_ANSWER).
|
|
7
|
-
const
|
|
7
|
+
const imageLabels = Array.isArray(answerData?.imageLabels) ? answerData.imageLabels : undefined;
|
|
8
|
+
const hasImageLabels = Array.isArray(imageLabels) && imageLabels.some((item) => Boolean(item?.imageUrl?.trim() || item?.label?.trim()));
|
|
9
|
+
const hasGroupData = !!(answerData?.title || answerData?.subtitle || answerData?.groupContent || answerData?.groupImageUrl || hasImageLabels);
|
|
8
10
|
const basicGroupData = hasGroupData
|
|
9
11
|
? {
|
|
10
12
|
title: answerData.title || '',
|
|
11
13
|
subtitle: answerData.subtitle || '',
|
|
12
14
|
content: answerData.groupContent || '',
|
|
13
15
|
imageUrl: answerData.groupImageUrl || '',
|
|
16
|
+
...(hasImageLabels ? { imageLabels } : {}),
|
|
14
17
|
}
|
|
15
18
|
: undefined;
|
|
16
19
|
const groupNumber = answerData?.groupNumber || 1;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface SpeakingCueCardClientProps {
|
|
2
|
+
questionData: {
|
|
3
|
+
cueCardHtml: string;
|
|
4
|
+
isBackup?: boolean;
|
|
5
|
+
expectedAnswers?: string[];
|
|
6
|
+
explanation?: string;
|
|
7
|
+
};
|
|
8
|
+
isReviewMode?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function mapQuestionToSpeakingCueCardData(question: Record<string, unknown> | null | undefined): SpeakingCueCardClientProps['questionData'];
|
|
11
|
+
export declare function SpeakingCueCardClient({ questionData, isReviewMode, }: SpeakingCueCardClientProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Mic, CheckCircle2, Info, CreditCard } from 'lucide-react';
|
|
4
|
+
import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
7
|
+
? value
|
|
8
|
+
: {};
|
|
9
|
+
}
|
|
10
|
+
export function mapQuestionToSpeakingCueCardData(question) {
|
|
11
|
+
const source = asRecord(question);
|
|
12
|
+
const content = asRecord(source.content);
|
|
13
|
+
const answer = asRecord(source.correctAnswer ?? source.correct_answer ?? source.answer);
|
|
14
|
+
const expectedAnswers = Array.isArray(answer.expectedAnswers)
|
|
15
|
+
? answer.expectedAnswers.filter((item) => typeof item === 'string' && item.trim() !== '')
|
|
16
|
+
: [];
|
|
17
|
+
return {
|
|
18
|
+
cueCardHtml: typeof content.cueCardHtml === 'string' ? content.cueCardHtml : '',
|
|
19
|
+
isBackup: content.isBackup === true,
|
|
20
|
+
expectedAnswers,
|
|
21
|
+
explanation: typeof source.explanation === 'string' ? source.explanation : '',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function SpeakingCueCardClient({ questionData, isReviewMode = false, }) {
|
|
25
|
+
const answers = questionData.expectedAnswers || [];
|
|
26
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsx("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-purple-50 to-indigo-50 px-5 py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-purple-500 to-indigo-600 text-white shadow-sm", children: _jsx(CreditCard, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "Th\u1EBB g\u1EE3i \u00FD (Cue Card)" })] }) }), _jsxs("div", { className: "p-5 space-y-5", children: [_jsx("div", { className: "rounded-xl border border-purple-100 bg-purple-50/40 px-5 py-4", children: _jsx(RichTextHtml, { html: questionData.cueCardHtml, className: "text-sm text-slate-800", fallback: _jsx("p", { className: "text-sm text-gray-400", children: "Ch\u01B0a c\u00F3 n\u1ED9i dung cue card" }) }) }), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-purple-50 ring-2 ring-purple-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-purple-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh \u0111\u1ECDc th\u1EBB g\u1EE3i \u00FD, chu\u1EA9n b\u1ECB, r\u1ED3i tr\u1EA3 l\u1EDDi b\u1EB1ng gi\u1ECDng n\u00F3i" })] }), isReviewMode && answers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "\u0110\u00E1p \u00E1n m\u1EABu" })] }), _jsx("div", { className: "space-y-2", children: answers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, `${answer}-${index}`))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-amber-700 leading-relaxed whitespace-pre-wrap", children: questionData.explanation })] }))] })] })] }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Mic, CheckCircle2, Info, FileQuestion, Volume2 } from 'lucide-react';
|
|
4
|
+
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
5
|
+
function sampleAnswers(questionData) {
|
|
6
|
+
const fromExpected = (questionData.expectedAnswers || []).filter((item) => item.trim() !== '');
|
|
7
|
+
if (fromExpected.length > 0)
|
|
8
|
+
return fromExpected;
|
|
9
|
+
return (questionData.sampleAnswers || []).filter((item) => item.trim() !== '');
|
|
10
|
+
}
|
|
11
|
+
export function SpontaneousQAClient({ questionData, isReviewMode = false, }) {
|
|
12
|
+
const { previewUrl: questionAudioUrl } = usePresignedFileUrl(questionData.audioUrl || '');
|
|
13
|
+
const answers = sampleAnswers(questionData);
|
|
14
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsx("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-indigo-50 to-blue-50 px-5 py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-blue-600 text-white shadow-sm", children: _jsx(Mic, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "H\u1ECFi \u0111\u00E1p t\u1EF1 ph\u00E1t" })] }) }), _jsxs("div", { className: "p-5 space-y-5", children: [questionData.questionText && (_jsxs("div", { className: "flex gap-3", children: [_jsx(FileQuestion, { className: "h-5 w-5 text-indigo-500 shrink-0 mt-0.5" }), _jsx("div", { className: "text-base font-semibold text-gray-800", children: questionData.questionText })] })), questionAudioUrl ? (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-blue-100 bg-blue-50/60 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-blue-500 shrink-0" }), _jsx("audio", { controls: true, src: questionAudioUrl, className: "h-8 w-full max-w-md", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })] })) : questionData.audioUrl ? (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-xs text-gray-400", children: "\u0110ang t\u1EA3i audio c\u00E2u h\u1ECFi..." })] })) : (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 audio c\u00E2u h\u1ECFi" })] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh nghe c\u00E2u h\u1ECFi r\u1ED3i nh\u1EA5n micro \u0111\u1EC3 tr\u1EA3 l\u1EDDi b\u1EB1ng gi\u1ECDng n\u00F3i" })] }), isReviewMode && answers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "\u0110\u00E1p \u00E1n m\u1EABu" })] }), _jsx("div", { className: "space-y-2", children: answers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, `${answer}-${index}`))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-amber-700 leading-relaxed whitespace-pre-wrap", children: questionData.explanation })] }))] })] })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function asRecord(value) {
|
|
2
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
3
|
+
? value
|
|
4
|
+
: {};
|
|
5
|
+
}
|
|
6
|
+
function asStringList(value) {
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
return value.filter((item) => typeof item === 'string' && item.trim() !== '');
|
|
9
|
+
}
|
|
10
|
+
if (typeof value === 'string' && value.trim())
|
|
11
|
+
return [value];
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
export function mapQuestionToSpontaneousQAData(question) {
|
|
15
|
+
const source = asRecord(question);
|
|
16
|
+
const content = asRecord(source.content);
|
|
17
|
+
const answer = asRecord(source.correctAnswer ?? source.correct_answer ?? source.answer);
|
|
18
|
+
const expectedAnswers = asStringList(answer.expectedAnswers).length
|
|
19
|
+
? asStringList(answer.expectedAnswers)
|
|
20
|
+
: asStringList(answer.sampleAnswers).length
|
|
21
|
+
? asStringList(answer.sampleAnswers)
|
|
22
|
+
: asStringList(answer.sampleAnswer);
|
|
23
|
+
return {
|
|
24
|
+
inputType: content.inputType === 'AUDIO' ? 'AUDIO' : 'TEXT',
|
|
25
|
+
audioUrl: typeof content.audioUrl === 'string' ? content.audioUrl : '',
|
|
26
|
+
questionText: typeof content.questionText === 'string' ? content.questionText : '',
|
|
27
|
+
expectedAnswers,
|
|
28
|
+
sampleAnswers: expectedAnswers,
|
|
29
|
+
explanation: typeof source.explanation === 'string' ? source.explanation : '',
|
|
30
|
+
isBackup: content.isBackup === true,
|
|
31
|
+
points: typeof source.points === 'number' ? source.points : undefined,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -58,6 +58,7 @@ export function mapQuestionToWfsfData(question) {
|
|
|
58
58
|
const source = asRecord(question);
|
|
59
59
|
const apiContent = asRecord(source.content);
|
|
60
60
|
const answer = asRecord(source.answer);
|
|
61
|
+
const meta = asRecord(source.meta);
|
|
61
62
|
const correctAnswer = asRecord(source.correctAnswer ??
|
|
62
63
|
source.correct_answer ??
|
|
63
64
|
answer.correctAnswer ??
|
|
@@ -66,7 +67,13 @@ export function mapQuestionToWfsfData(question) {
|
|
|
66
67
|
? parseFloat(String(source.total_points))
|
|
67
68
|
: answer.points ?? source.points ?? 1;
|
|
68
69
|
const points = Number.isFinite(Number(pointsRaw)) ? Number(pointsRaw) : 1;
|
|
69
|
-
|
|
70
|
+
// Pearson bank stores rubric on meta.instruction; school/API use content.instruction.
|
|
71
|
+
const instruction = String(apiContent.instruction ||
|
|
72
|
+
answer.instruction ||
|
|
73
|
+
source.instruction ||
|
|
74
|
+
meta.instruction ||
|
|
75
|
+
source.explanation ||
|
|
76
|
+
'');
|
|
70
77
|
const explanation = String(answer.explanation || '');
|
|
71
78
|
const content = resolveHtmlContent(apiContent, answer);
|
|
72
79
|
const rawWordBank = apiContent.wordBank ?? answer.wordBank;
|
|
@@ -34,6 +34,10 @@ export const transformWordFillStructuredForm = (question) => {
|
|
|
34
34
|
audioUrls,
|
|
35
35
|
content: answerData.content,
|
|
36
36
|
};
|
|
37
|
+
const instructionText = String(answerData.instruction ?? '').trim();
|
|
38
|
+
if (instructionText) {
|
|
39
|
+
apiContent.instruction = instructionText;
|
|
40
|
+
}
|
|
37
41
|
if (answerData.blankStyle) {
|
|
38
42
|
apiContent.blankStyle = answerData.blankStyle;
|
|
39
43
|
}
|
|
@@ -1,49 +1,67 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
4
4
|
import { Check, X, ArrowUpDown, Lightbulb } from 'lucide-react';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
6
|
import { cn } from '../../../../shared/lib/utils';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// Remove used words from available pool
|
|
16
|
-
const remaining = [...words];
|
|
17
|
-
initial.forEach((w) => {
|
|
18
|
-
const idx = remaining.indexOf(w);
|
|
19
|
-
if (idx >= 0)
|
|
20
|
-
remaining.splice(idx, 1);
|
|
21
|
-
});
|
|
22
|
-
return remaining;
|
|
7
|
+
function toWordList(value) {
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value.filter((item) => typeof item === 'string');
|
|
10
|
+
}
|
|
11
|
+
if (value && typeof value === 'object') {
|
|
12
|
+
const nested = value.answer;
|
|
13
|
+
if (Array.isArray(nested)) {
|
|
14
|
+
return nested.filter((item) => typeof item === 'string');
|
|
23
15
|
}
|
|
24
|
-
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
function resolveSelectedWords(userAnswer, answer, autoFillCorrectAnswers) {
|
|
20
|
+
if (Array.isArray(userAnswer)) {
|
|
21
|
+
return toWordList(userAnswer);
|
|
22
|
+
}
|
|
23
|
+
const nested = toWordList(userAnswer);
|
|
24
|
+
if (nested.length > 0) {
|
|
25
|
+
return nested;
|
|
26
|
+
}
|
|
27
|
+
return autoFillCorrectAnswers ? answer : [];
|
|
28
|
+
}
|
|
29
|
+
function remainingWords(words, selected) {
|
|
30
|
+
const remaining = [...words];
|
|
31
|
+
selected.forEach((word) => {
|
|
32
|
+
const idx = remaining.indexOf(word);
|
|
33
|
+
if (idx >= 0)
|
|
34
|
+
remaining.splice(idx, 1);
|
|
25
35
|
});
|
|
36
|
+
return remaining;
|
|
37
|
+
}
|
|
38
|
+
export function WordOrderingClient({ questionData, onSubmit, isReviewMode = false, userAnswer, autoFillCorrectAnswers = false, explanation, }) {
|
|
39
|
+
const words = useMemo(() => toWordList(questionData.words), [questionData.words]);
|
|
40
|
+
const answer = useMemo(() => toWordList(questionData.answer), [questionData.answer]);
|
|
41
|
+
const { isExample, imageUrl } = questionData;
|
|
42
|
+
const initialSelected = resolveSelectedWords(userAnswer, answer, autoFillCorrectAnswers);
|
|
43
|
+
const [selectedWords, setSelectedWords] = useState(initialSelected);
|
|
44
|
+
const [availableWords, setAvailableWords] = useState(() => remainingWords(words, initialSelected));
|
|
26
45
|
const { previewUrl: imagePreviewUrl, isLoading } = usePresignedFileUrl(imageUrl || '');
|
|
27
|
-
// Sync with userAnswer changes
|
|
28
46
|
useEffect(() => {
|
|
29
|
-
if (userAnswer)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
if (userAnswer == null)
|
|
48
|
+
return;
|
|
49
|
+
if (!Array.isArray(userAnswer) && toWordList(userAnswer).length === 0)
|
|
50
|
+
return;
|
|
51
|
+
const next = resolveSelectedWords(userAnswer, answer, false);
|
|
52
|
+
setSelectedWords(next);
|
|
53
|
+
setAvailableWords(remainingWords(words, next));
|
|
54
|
+
}, [userAnswer, words, answer]);
|
|
55
|
+
const wordsString = JSON.stringify(words);
|
|
56
|
+
const hasUserAnswer = Array.isArray(userAnswer)
|
|
57
|
+
? true
|
|
58
|
+
: toWordList(userAnswer).length > 0;
|
|
41
59
|
useEffect(() => {
|
|
42
|
-
if (!
|
|
60
|
+
if (!hasUserAnswer && !autoFillCorrectAnswers) {
|
|
43
61
|
setSelectedWords([]);
|
|
44
62
|
setAvailableWords([...words]);
|
|
45
63
|
}
|
|
46
|
-
}, [
|
|
64
|
+
}, [wordsString, hasUserAnswer, autoFillCorrectAnswers, words]);
|
|
47
65
|
const handleSelectWord = useCallback((word, poolIndex) => {
|
|
48
66
|
if (isReviewMode)
|
|
49
67
|
return;
|
|
@@ -32,6 +32,9 @@ import { FillInBlankGroupViewer } from './FillInBlankGroupViewer';
|
|
|
32
32
|
import { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
|
|
33
33
|
import { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
|
|
34
34
|
import { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
|
|
35
|
+
import { SpontaneousQAClient } from '../types/spontaneous-qa/SpontaneousQAClient';
|
|
36
|
+
import { mapQuestionToSpontaneousQAData } from '../types/spontaneous-qa/map-spontaneous-qa-data';
|
|
37
|
+
import { SpeakingCueCardClient, mapQuestionToSpeakingCueCardData, } from '../types/speaking-cue-card/SpeakingCueCardClient';
|
|
35
38
|
import { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
36
39
|
import { AnswerTheQuestionViewer } from './AnswerTheQuestionViewer';
|
|
37
40
|
import { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
@@ -633,7 +636,11 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
633
636
|
return (_jsxs("div", { className: "space-y-3", children: [content.title ? _jsx("h3", { className: "text-center text-lg font-bold uppercase text-slate-800", children: content.title }) : null, content.imageUrl ? (_jsx("div", { className: "flex justify-center", children: _jsx("img", { src: String(content.imageUrl), alt: "", className: "max-h-[180px] rounded-lg object-contain" }) })) : null, _jsx("div", { className: "space-y-2 text-base leading-relaxed text-slate-800", children: paragraph.split('\n').map((line, li) => (_jsx("div", { className: "flex flex-wrap items-baseline", children: renderLine(line, `wfp-${li}`) }, `wfp-l${li}`))) }), isReviewMode && correctAnswers.length > 0 ? (_jsxs("div", { className: "rounded-lg border border-emerald-200 bg-emerald-50/80 p-2.5 text-sm text-emerald-800", children: [_jsx("span", { className: "font-semibold", children: "\u0110\u00E1p \u00E1n: " }), correctAnswers.map((a, i) => (_jsxs("span", { className: "mr-2", children: ["(", i + 1, ") ", _jsx("span", { className: "font-bold underline", children: a || '—' })] }, `wfp-a${i}`)))] })) : null] }));
|
|
634
637
|
};
|
|
635
638
|
const renderQuestion = () => {
|
|
636
|
-
|
|
639
|
+
const resolvedType = String(question.type ||
|
|
640
|
+
question.questionType ||
|
|
641
|
+
question.question_type ||
|
|
642
|
+
'');
|
|
643
|
+
switch (resolvedType) {
|
|
637
644
|
case 'CHOOSE_THE_CORRECT_ANSWER':
|
|
638
645
|
return renderChooseTheCorrectAnswer();
|
|
639
646
|
case 'CHOOSE_THE_CORRECT_ANSWER_GROUP':
|
|
@@ -654,6 +661,10 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
654
661
|
return (_jsx(AnswerTheQuestionViewer, { question: question, isReviewMode: isReviewMode, userAnswer: typeof userAnswer === 'string' ? userAnswer : undefined, onSubmitAnswer: onSubmitAnswer }));
|
|
655
662
|
case 'ANSWER_THE_QUESTION_GROUP':
|
|
656
663
|
return (_jsx(AnswerTheQuestionGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
664
|
+
case 'SPEAKING_CUE_CARD':
|
|
665
|
+
return (_jsx(SpeakingCueCardClient, { questionData: mapQuestionToSpeakingCueCardData(question), isReviewMode: isReviewMode }));
|
|
666
|
+
case 'SPONTANEOUS_QA':
|
|
667
|
+
return (_jsx(SpontaneousQAClient, { questionData: mapQuestionToSpontaneousQAData(question), isReviewMode: isReviewMode }));
|
|
657
668
|
case 'SPONTANEOUS_QA_GROUP':
|
|
658
669
|
return (_jsx(SpontaneousQaGroupViewer, { question: question, isReviewMode: isReviewMode }));
|
|
659
670
|
case 'WORD_ORDER_AND_MATCH_GROUP':
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FillBlankQuestion } from '../../../components/exams/take/types';
|
|
2
|
+
import { type ImageLabelData } from '../../questions/_shared/types/question-group.type';
|
|
2
3
|
interface ReviewListenAndWriteRendererProps {
|
|
3
4
|
partNumber: number;
|
|
4
5
|
questionCount: number;
|
|
@@ -12,6 +13,7 @@ interface ReviewListenAndWriteRendererProps {
|
|
|
12
13
|
questions: FillBlankQuestion[];
|
|
13
14
|
answers: Record<string, unknown>;
|
|
14
15
|
isCorrectMap: Record<string, boolean | null>;
|
|
16
|
+
imageLabels?: ImageLabelData[];
|
|
15
17
|
}
|
|
16
|
-
export declare function ReviewListenAndWriteRenderer({ partNumber, questionCount, instruction, partName, audioUrl, title, exampleText, questions, answers, isCorrectMap, }: ReviewListenAndWriteRendererProps): import("react").JSX.Element;
|
|
18
|
+
export declare function ReviewListenAndWriteRenderer({ partNumber, questionCount, instruction, partName, audioUrl, title, exampleText, questions, answers, isCorrectMap, imageLabels, }: ReviewListenAndWriteRendererProps): import("react").JSX.Element;
|
|
17
19
|
export {};
|
|
@@ -6,7 +6,9 @@ import CambridgeYlePartBanner from '../../../components/themes/cambridge-yle/Cam
|
|
|
6
6
|
import CambridgeYleInstructionBanner from '../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
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';
|
|
11
|
+
export function ReviewListenAndWriteRenderer({ partNumber, questionCount, instruction, partName, audioUrl, title, exampleText, questions, answers, isCorrectMap, imageLabels, }) {
|
|
10
12
|
/**
|
|
11
13
|
* Get answer status from API's isCorrect field
|
|
12
14
|
* Returns: 'correct' | 'incorrect' | 'unanswered'
|
|
@@ -44,7 +46,7 @@ export function ReviewListenAndWriteRenderer({ partNumber, questionCount, instru
|
|
|
44
46
|
return 'unanswered';
|
|
45
47
|
return isBlankCorrect(userValue, accepted) ? 'correct' : 'incorrect';
|
|
46
48
|
};
|
|
47
|
-
return (_jsxs("div", { className: "flex flex-col gap-6 h-full", children: [_jsxs("div", { className: "flex flex-col gap-4 pb-0 pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: true }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, hasExample: !!exampleText, isReviewMode: true }), audioUrl && _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true })] }), _jsxs("div", { className: "flex flex-col gap-6 pb-6 pl-2 pt-2", children: [_jsx("h2", { className: "text-center text-3xl font-bold uppercase text-coral-500", children: title }), _jsx("div", { className: "flex flex-col gap-4", children: questions.map((question) => {
|
|
49
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 h-full", children: [_jsxs("div", { className: "flex flex-col gap-4 pb-0 pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: true }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, hasExample: !!exampleText, isReviewMode: true }), hasImageLabels(imageLabels) && (_jsx(ImageLabelWordBank, { items: imageLabels, className: "rounded-2xl border border-slate-200 bg-slate-50/80 p-4 shadow-sm" })), audioUrl && _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true })] }), _jsxs("div", { className: "flex flex-col gap-6 pb-6 pl-2 pt-2", children: [_jsx("h2", { className: "text-center text-3xl font-bold uppercase text-coral-500", children: title }), _jsx("div", { className: "flex flex-col gap-4", children: questions.map((question) => {
|
|
48
50
|
const status = getAnswerStatus(question.id);
|
|
49
51
|
const userValues = toBlankValues(answers[question.id], countBlanks(question.segments));
|
|
50
52
|
const renderBlank = (blankIndex) => {
|
|
@@ -27,8 +27,8 @@ export function renderDedicatedReviewBody(ctx) {
|
|
|
27
27
|
const apiQuestions = asApiQuestions(questions);
|
|
28
28
|
switch (questionType) {
|
|
29
29
|
case 'FILL_IN_BLANK': {
|
|
30
|
-
const data = transformFillInBlank(apiQuestions);
|
|
31
|
-
return (_jsx(ReviewListenAndWriteRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, instruction: part.instructions || data.instruction, title: data.title, imageUrl: data.imageUrl, audioUrl: data.audioUrl, exampleText: data.exampleText, exampleHighlight: data.exampleHighlight, questions: data.questions, answers: answers, isCorrectMap: isCorrectMap }));
|
|
30
|
+
const data = transformFillInBlank(apiQuestions, { suggestedAnswers: part.suggestedAnswers });
|
|
31
|
+
return (_jsx(ReviewListenAndWriteRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, instruction: part.instructions || data.instruction, title: data.title, imageUrl: data.imageUrl, audioUrl: data.audioUrl, exampleText: data.exampleText, exampleHighlight: data.exampleHighlight, questions: data.questions, answers: answers, isCorrectMap: isCorrectMap, imageLabels: data.imageLabels }));
|
|
32
32
|
}
|
|
33
33
|
case 'WRITE_CORRECT_VERB_FORM': {
|
|
34
34
|
const data = transformWriteCorrectVerbForm(apiQuestions);
|
package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js
CHANGED
|
@@ -13,6 +13,8 @@ import { EcMatchByWritingAnswer } from './EcMatchByWritingAnswer';
|
|
|
13
13
|
import { EcAnswerTheQuestion } from './EcAnswerTheQuestion';
|
|
14
14
|
import { EcWordFillParagraph } from './EcWordFillParagraph';
|
|
15
15
|
import { EcWritingTask } from './EcWritingTask';
|
|
16
|
+
import { MoversListenAndWriteRenderer } from '../../../components/exams/take/components/question-renderers/MoversListenAndWriteRenderer';
|
|
17
|
+
import { transformFillInBlank } from '../../../components/exams/take/utils/question-transformers';
|
|
16
18
|
/**
|
|
17
19
|
* Question types that the branded English Certification theme renders natively
|
|
18
20
|
* as multiple-choice cards (TOEIC + IELTS MCQ variants such as TRUE/FALSE/NOT GIVEN).
|
|
@@ -67,6 +69,12 @@ export function EnglishCertificationQuestionRenderer({ part, partConfig, answers
|
|
|
67
69
|
return (_jsx(EcAnswerTheQuestion, { part: part, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
68
70
|
case 'WORD_FILL_PARAGRAPH':
|
|
69
71
|
return (_jsx(EcWordFillParagraph, { part: part, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
72
|
+
case 'FILL_IN_BLANK': {
|
|
73
|
+
const data = transformFillInBlank(part.questions, {
|
|
74
|
+
suggestedAnswers: part.suggestedAnswers,
|
|
75
|
+
});
|
|
76
|
+
return (_jsx(MoversListenAndWriteRenderer, { partNumber: part.partNo ?? 0, partName: part.name, questionCount: part.questions.filter((q) => !q.is_example).length, instruction: part.instructions || data.instruction, title: data.title, imageUrl: data.groupImageUrl || data.imageUrl, audioUrl: data.audioUrl || part.audioUrl, exampleText: data.exampleText, exampleHighlight: data.exampleHighlight, questions: data.questions, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, showHints: data.showHints, groupContent: data.groupContent, groupTitle: data.groupTitle, imageLabels: data.imageLabels }));
|
|
77
|
+
}
|
|
70
78
|
case 'WRITE_A_SHORT_LETTER':
|
|
71
79
|
return (_jsx(EcWritingTask, { part: part, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
72
80
|
default:
|
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`,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
/** Strip HTML tags/entities so empty TipTap documents (`<p></p>`) count as blank. */
|
|
2
2
|
export declare function richTextToPlainText(value: string | null | undefined): string;
|
|
3
3
|
export declare function isRichTextEmpty(value: string | null | undefined): boolean;
|
|
4
|
+
/** True when HTML has marks/blocks beyond a plain paragraph (bold, list, table, …). */
|
|
5
|
+
export declare function hasRichTextFormatting(value: string | null | undefined): boolean;
|
|
6
|
+
/** Show stored TipTap HTML as editable plain text without collapsing line breaks. */
|
|
7
|
+
export declare function htmlToPlainEditorText(value: string | null | undefined): string;
|