@tinyweb_dev/oe-exam-sdk 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +7 -3
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -4
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.d.ts +13 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.js +30 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/utils/answer-transformers.js +8 -1
- package/dist/components/exams/take/utils/question-transformers.d.ts +16 -1
- package/dist/components/exams/take/utils/question-transformers.js +49 -4
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/questions/_shared/config/question-types.config.js +18 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/index.d.ts +1 -0
- package/dist/components/questions/_shared/types/index.js +1 -0
- package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.d.ts +56 -0
- package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.js +2 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +35 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +2 -0
- package/dist/components/questions/_shared/types/word-ordering-group.type.d.ts +34 -0
- package/dist/components/questions/_shared/types/word-ordering-group.type.js +2 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +3 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
- package/dist/components/questions/creator/question-type-registry.js +6 -0
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +6 -4
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +16 -4
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +2 -3
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +6 -4
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +16 -2
- package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +74 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.js +222 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/index.d.ts +5 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/index.js +5 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.d.ts +8 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.js +199 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/register.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/register.js +36 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/transform.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/transform.js +66 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +65 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +95 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +6 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +16 -0
- package/dist/components/questions/types/match-word-to-picture-group/index.d.ts +5 -0
- package/dist/components/questions/types/match-word-to-picture-group/index.js +5 -0
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.d.ts +4 -0
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +77 -0
- package/dist/components/questions/types/match-word-to-picture-group/register.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/register.js +36 -0
- package/dist/components/questions/types/match-word-to-picture-group/transform.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/transform.js +36 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +25 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.js +106 -0
- package/dist/components/questions/types/word-ordering-group/index.d.ts +5 -0
- package/dist/components/questions/types/word-ordering-group/index.js +5 -0
- package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.d.ts +4 -0
- package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.js +74 -0
- package/dist/components/questions/types/word-ordering-group/register.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/register.js +36 -0
- package/dist/components/questions/types/word-ordering-group/transform.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/transform.js +33 -0
- package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.d.ts +8 -0
- package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.js +49 -0
- package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +2 -2
- package/dist/components/questions/viewer/QuestionViewer.js +6 -0
- package/dist/components/questions/viewer/WordOrderingGroupViewer.d.ts +7 -0
- package/dist/components/questions/viewer/WordOrderingGroupViewer.js +29 -0
- package/dist/components/questions/viewer/index.d.ts +2 -0
- package/dist/components/questions/viewer/index.js +2 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +3 -2
- package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
- package/dist/components/results/renderers/review-question-body-movers.js +6 -2
- package/dist/components/results/review-data.js +13 -0
- package/dist/components/shared/RichTextEditor.d.ts +4 -2
- package/dist/components/shared/RichTextEditor.js +34 -11
- package/dist/components/shared/RichTextHtml.d.ts +8 -0
- package/dist/components/shared/RichTextHtml.js +9 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +6 -0
- package/dist/shared/lib/rich-text.d.ts +3 -0
- package/dist/shared/lib/rich-text.js +24 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +111 -2
- package/dist/shared/lib/utils/question-transform.js +6 -0
- package/dist/shared/lib/validation/choose-correct-answer.validation.js +4 -2
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
- package/dist/shared/types/questions/index.d.ts +3 -0
- package/dist/shared/types/questions/index.js +6 -0
- package/dist/shared/types/questions/match-columns-to-make-sentences.d.ts +50 -0
- package/dist/shared/types/questions/match-columns-to-make-sentences.js +1 -0
- package/dist/shared/types/questions/match-word-to-picture-group.d.ts +31 -0
- package/dist/shared/types/questions/match-word-to-picture-group.js +1 -0
- package/dist/shared/types/questions/word-ordering-group.d.ts +28 -0
- package/dist/shared/types/questions/word-ordering-group.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Question } from '../../../shared/types/entities/question.types';
|
|
2
|
+
import type { MatchColumnsStudentItemAnswer } from '../_shared/types/match-columns-to-make-sentences.type';
|
|
3
|
+
export declare function MatchColumnsToMakeSentencesViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
|
|
4
|
+
question: Question;
|
|
5
|
+
isReviewMode?: boolean;
|
|
6
|
+
userAnswer?: unknown;
|
|
7
|
+
onSubmitAnswer?: (answer: MatchColumnsStudentItemAnswer[]) => void;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { MatchColumnsToMakeSentencesClient } from '../types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient';
|
|
5
|
+
import { mapQuestionToMatchColumnsGroupData } from '../types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data';
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function parseAnswers(value, length) {
|
|
10
|
+
const source = Array.isArray(value)
|
|
11
|
+
? value
|
|
12
|
+
: isRecord(value) && Array.isArray(value.answer)
|
|
13
|
+
? value.answer
|
|
14
|
+
: null;
|
|
15
|
+
if (!source)
|
|
16
|
+
return undefined;
|
|
17
|
+
return Array.from({ length }, (_, index) => {
|
|
18
|
+
const raw = source[index];
|
|
19
|
+
if (!isRecord(raw))
|
|
20
|
+
return { fragmentIds: [], sentence: '' };
|
|
21
|
+
return {
|
|
22
|
+
fragmentIds: Array.isArray(raw.fragmentIds)
|
|
23
|
+
? raw.fragmentIds.filter((id) => typeof id === 'string')
|
|
24
|
+
: [],
|
|
25
|
+
sentence: typeof raw.sentence === 'string' ? raw.sentence : '',
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function MatchColumnsToMakeSentencesViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
30
|
+
const questionData = mapQuestionToMatchColumnsGroupData(question);
|
|
31
|
+
const itemCount = questionData.items.length;
|
|
32
|
+
const parsedUser = parseAnswers(userAnswer, itemCount);
|
|
33
|
+
const questionKey = `${question.id}:${itemCount}`;
|
|
34
|
+
const [cachedKey, setCachedKey] = useState(questionKey);
|
|
35
|
+
const [localAnswers, setLocalAnswers] = useState(() => parsedUser ?? questionData.items.map((item) => (item.isExample
|
|
36
|
+
? { fragmentIds: item.fragmentIds, sentence: item.sentence }
|
|
37
|
+
: { fragmentIds: [item.stemFragmentId], sentence: '' })));
|
|
38
|
+
if (cachedKey !== questionKey) {
|
|
39
|
+
setCachedKey(questionKey);
|
|
40
|
+
setLocalAnswers(parsedUser ?? questionData.items.map((item) => (item.isExample
|
|
41
|
+
? { fragmentIds: item.fragmentIds, sentence: item.sentence }
|
|
42
|
+
: { fragmentIds: [item.stemFragmentId], sentence: '' })));
|
|
43
|
+
}
|
|
44
|
+
const userAnswers = parsedUser ?? localAnswers;
|
|
45
|
+
return (_jsx(MatchColumnsToMakeSentencesClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: isReviewMode ? undefined : (next) => {
|
|
46
|
+
setLocalAnswers(next);
|
|
47
|
+
onSubmitAnswer?.(next);
|
|
48
|
+
} }));
|
|
49
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { MatchWordToPictureGroupClient } from '../types/match-word-to-picture/MatchWordToPictureGroupClient';
|
|
4
|
-
import { mapQuestionToMatchWordToPictureGroupData } from '../types/match-word-to-picture/map-match-word-to-picture-group-data';
|
|
3
|
+
import { MatchWordToPictureGroupClient } from '../types/match-word-to-picture-group/MatchWordToPictureGroupClient';
|
|
4
|
+
import { mapQuestionToMatchWordToPictureGroupData } from '../types/match-word-to-picture-group/map-match-word-to-picture-group-data';
|
|
5
5
|
export function MatchWordToPictureGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
6
6
|
const questionData = mapQuestionToMatchWordToPictureGroupData(question);
|
|
7
7
|
const reviewAnswers = isReviewMode && !userAnswer
|
|
@@ -34,6 +34,8 @@ import { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
|
34
34
|
import { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
35
35
|
import { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
36
36
|
import { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|
|
37
|
+
import { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
|
|
38
|
+
import { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
|
|
37
39
|
export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false, userAnswer, }) {
|
|
38
40
|
const [selectedAnswer, setSelectedAnswer] = useState(userAnswer || null);
|
|
39
41
|
const [playCount, setPlayCount] = useState(0);
|
|
@@ -641,6 +643,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
641
643
|
return (_jsx(FillInBlankGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
642
644
|
case 'MATCHING_WITH_LINES_GROUP':
|
|
643
645
|
return (_jsx(MatchingWithLinesGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
646
|
+
case 'MATCH_COLUMNS_TO_MAKE_SENTENCES':
|
|
647
|
+
return (_jsx(MatchColumnsToMakeSentencesViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
644
648
|
case 'CROSS_OUT_WORD_GROUP':
|
|
645
649
|
return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode }));
|
|
646
650
|
case 'ANSWER_THE_QUESTION_GROUP':
|
|
@@ -650,6 +654,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
650
654
|
case 'WORD_ORDER_AND_MATCH_GROUP':
|
|
651
655
|
case 'WORD_ORDER_AND_MATCH':
|
|
652
656
|
return (_jsx(WordOrderAndMatchGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
657
|
+
case 'WORD_ORDERING_GROUP':
|
|
658
|
+
return (_jsx(WordOrderingGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
653
659
|
case 'MATCH_WORD_TO_PICTURE_GROUP':
|
|
654
660
|
return (_jsx(MatchWordToPictureGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
655
661
|
case 'MATCH_WORD_TO_PICTURE':
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Question } from '../../../shared/types/entities/question.types';
|
|
2
|
+
export declare function WordOrderingGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
|
|
3
|
+
question: Question;
|
|
4
|
+
isReviewMode?: boolean;
|
|
5
|
+
userAnswer?: unknown;
|
|
6
|
+
onSubmitAnswer?: (answer: string[][]) => void;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { WordOrderingGroupClient } from '../types/word-ordering-group/WordOrderingGroupClient';
|
|
5
|
+
import { mapQuestionToWordOrderingGroupData } from '../types/word-ordering-group/map-word-ordering-group-data';
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function parseAnswers(value, length) {
|
|
10
|
+
const source = Array.isArray(value)
|
|
11
|
+
? value
|
|
12
|
+
: isRecord(value) && Array.isArray(value.answer)
|
|
13
|
+
? value.answer
|
|
14
|
+
: null;
|
|
15
|
+
if (!source)
|
|
16
|
+
return undefined;
|
|
17
|
+
return Array.from({ length }, (_, index) => Array.isArray(source[index])
|
|
18
|
+
? source[index].filter((token) => typeof token === 'string')
|
|
19
|
+
: []);
|
|
20
|
+
}
|
|
21
|
+
export function WordOrderingGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
|
|
22
|
+
const questionData = mapQuestionToWordOrderingGroupData(question);
|
|
23
|
+
const parsedUser = parseAnswers(userAnswer, questionData.items.length);
|
|
24
|
+
const [localAnswers, setLocalAnswers] = useState(() => parsedUser ?? questionData.items.map((item) => (item.isExample ? item.answer : [])));
|
|
25
|
+
return (_jsx(WordOrderingGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: parsedUser ?? localAnswers, onAnswerChange: isReviewMode ? undefined : (next) => {
|
|
26
|
+
setLocalAnswers(next);
|
|
27
|
+
onSubmitAnswer?.(next);
|
|
28
|
+
} }));
|
|
29
|
+
}
|
|
@@ -12,3 +12,5 @@ export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
|
12
12
|
export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
13
13
|
export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
14
14
|
export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|
|
15
|
+
export { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
|
|
16
|
+
export { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
|
|
@@ -12,3 +12,5 @@ export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
|
|
|
12
12
|
export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
|
|
13
13
|
export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
14
14
|
export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|
|
15
|
+
export { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
|
|
16
|
+
export { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Check, X } from 'lucide-react';
|
|
4
4
|
import { ResolvedImage } from '../../../components/common/ResolvedImage';
|
|
5
|
+
import { RichTextHtml } from '../../../components/shared/RichTextHtml';
|
|
5
6
|
import CambridgeYlePartBanner from '../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
6
7
|
import CambridgeYleInstructionBanner from '../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
7
8
|
import CambridgeYleAudioPlayer from '../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
|
|
@@ -43,9 +44,9 @@ function ReviewQuestionItem({ question, selectedOptionIds, isCorrect, correctOpt
|
|
|
43
44
|
}
|
|
44
45
|
return _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 border-gray-300 bg-white ${shapeClass}` });
|
|
45
46
|
};
|
|
46
|
-
return (_jsxs("div", { className: "mb-4", children: [_jsxs("
|
|
47
|
+
return (_jsxs("div", { className: "mb-4", children: [_jsxs("div", { className: "mb-2 flex items-start gap-1.5 text-lg font-bold text-gray-900", children: [_jsxs("span", { children: [question.questionNumber, "."] }), _jsx(RichTextHtml, { html: question.questionText, className: "flex-1 font-bold text-gray-900" })] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })), question.audioUrl && (_jsx("div", { className: "mb-3", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: question.audioUrl, compact: true }) })), _jsx(ChooseBestAnswerImages, { imageUrl: question.imageUrl, className: "mb-3" }), isImageOptions ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("div", { className: `relative flex flex-col items-center gap-1 rounded-lg border-2 p-2 cursor-default ${getOptionStyle(option.id)}`, children: [_jsx("span", { className: "text-xs font-medium text-gray-600", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "h-28 w-28 rounded object-contain" }), renderIndicator(option.id)] }, option.id))) })) : (
|
|
47
48
|
/* Options — text variant */
|
|
48
|
-
_jsx("div", { className: "flex flex-col gap-2 pl-2", children: question.options.map((option) => (_jsxs("div", { className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 cursor-default ${getOptionStyle(option.id)}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx(
|
|
49
|
+
_jsx("div", { className: "flex flex-col gap-2 pl-2", children: question.options.map((option) => (_jsxs("div", { className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 cursor-default ${getOptionStyle(option.id)}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx(RichTextHtml, { html: option.text, className: "flex-1 text-sm text-gray-700" }), renderIndicator(option.id)] }, option.id))) }))] }));
|
|
49
50
|
}
|
|
50
51
|
// Article Card for ARTICLES groupType (e.g., GN QE RWL Part 2)
|
|
51
52
|
function ArticleCard({ article, label, }) {
|
|
@@ -40,7 +40,7 @@ export function renderDedicatedReviewBody(ctx) {
|
|
|
40
40
|
}
|
|
41
41
|
case 'CHOOSE_THE_CORRECT_ANSWER_GROUP': {
|
|
42
42
|
const data = transformChooseTheCorrectAnswerGroup(apiQuestions);
|
|
43
|
-
return (_jsx(ReviewChooseBestAnswerRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl, questions: data.questions, answers: answers, isCorrectMap: isCorrectMap, correctOptionIdMap: correctOptionIdMap, example: data.example }));
|
|
43
|
+
return (_jsx(ReviewChooseBestAnswerRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl, imageUrl: data.imageUrl, questions: data.questions, answers: answers, isCorrectMap: isCorrectMap, correctOptionIdMap: correctOptionIdMap, example: data.example }));
|
|
44
44
|
}
|
|
45
45
|
case 'CHOOSE_CORRECT_ADJECTIVE': {
|
|
46
46
|
const data = transformChooseCorrectAdjective(apiQuestions);
|
|
@@ -4,8 +4,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
4
4
|
* Movers fallback review cases (isReviewMode=true).
|
|
5
5
|
* Source: oe-exam-fe ReviewQuestionRenderer.tsx
|
|
6
6
|
*/
|
|
7
|
-
import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, } from '../../../components/exams/take/components/question-renderers';
|
|
8
|
-
import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformMatchingWithLinesGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, } from '../../../components/exams/take/utils/question-transformers';
|
|
7
|
+
import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, } from '../../../components/exams/take/components/question-renderers';
|
|
8
|
+
import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformMatchingWithLinesGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWordOrderingGroupTake, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, } from '../../../components/exams/take/utils/question-transformers';
|
|
9
9
|
import { normalizeCrossOutWordGroupAnswer, transformCrossOutWordGroup, } from '../../../components/exams/take/utils/cross-out-word-group.transformer';
|
|
10
10
|
function asApiQuestions(questions) {
|
|
11
11
|
return questions;
|
|
@@ -189,6 +189,10 @@ export function renderMoversReviewBody(ctx) {
|
|
|
189
189
|
const data = transformMatchWordToPicture(apiQuestions);
|
|
190
190
|
return (_jsx(MoversMatchWordToPictureRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, wordBank: data.wordBank, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerTextMap: correctAnswerTextMap, isCorrectMap: isCorrectMap }));
|
|
191
191
|
}
|
|
192
|
+
case 'WORD_ORDERING_GROUP': {
|
|
193
|
+
const data = transformWordOrderingGroupTake(apiQuestions);
|
|
194
|
+
return (_jsx(MoversWordOrderingGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: data.questions.filter((q) => !q.isExample).length, data: data, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true }));
|
|
195
|
+
}
|
|
192
196
|
case 'WORD_ORDERING': {
|
|
193
197
|
const data = transformWordOrdering(apiQuestions);
|
|
194
198
|
return (_jsx(MoversWordOrderingRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, rawCorrectAnswerMap: rawCorrectAnswerMap, isCorrectMap: isCorrectMap }));
|
|
@@ -231,6 +231,19 @@ function transformStudentAnswer(questionId, questionType, value) {
|
|
|
231
231
|
return [`${questionId}-item-${index}`, { answers: [] }];
|
|
232
232
|
}));
|
|
233
233
|
}
|
|
234
|
+
if (questionType === 'WORD_ORDERING_GROUP') {
|
|
235
|
+
const answer = Array.isArray(record.answer)
|
|
236
|
+
? record.answer
|
|
237
|
+
: Array.isArray(value)
|
|
238
|
+
? value
|
|
239
|
+
: [];
|
|
240
|
+
return Object.fromEntries(answer.map((item, index) => [
|
|
241
|
+
`${questionId}-item-${index}`,
|
|
242
|
+
Array.isArray(item)
|
|
243
|
+
? item.filter((token) => typeof token === 'string')
|
|
244
|
+
: [],
|
|
245
|
+
]));
|
|
246
|
+
}
|
|
234
247
|
if (questionType === 'MATCHING_WITH_LINES_GROUP') {
|
|
235
248
|
const answer = Array.isArray(record.answer)
|
|
236
249
|
? record.answer
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
2
|
+
interface RichTextEditorProps {
|
|
3
3
|
value?: string;
|
|
4
4
|
onChange: (html: string) => void;
|
|
5
5
|
label?: React.ReactNode;
|
|
6
6
|
disabled?: boolean;
|
|
7
7
|
minHeightClassName?: string;
|
|
8
|
+
variant?: 'default' | 'compact';
|
|
8
9
|
}
|
|
9
|
-
export declare function RichTextEditor({ value, onChange, label, disabled, minHeightClassName, }: RichTextEditorProps): React.JSX.Element;
|
|
10
|
+
export declare function RichTextEditor({ value, onChange, label, disabled, minHeightClassName, variant, }: RichTextEditorProps): React.JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -3,16 +3,43 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import { useEditor, EditorContent } from '@tiptap/react';
|
|
5
5
|
import StarterKit from '@tiptap/starter-kit';
|
|
6
|
+
import Underline from '@tiptap/extension-underline';
|
|
7
|
+
import TextAlign from '@tiptap/extension-text-align';
|
|
8
|
+
import { Table, TableRow, TableCell, TableHeader } from '@tiptap/extension-table';
|
|
9
|
+
import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, Code, Columns3, Italic, List, ListOrdered, Minus, Quote, Redo2, RemoveFormatting, Rows3, SquareCode, Strikethrough, TableIcon, Trash2, Underline as UnderlineIcon, Undo2, } from 'lucide-react';
|
|
6
10
|
const EMPTY_CONTENT = '<p></p>';
|
|
7
|
-
const HEADING_LEVELS = [1, 2, 3];
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const HEADING_LEVELS = [1, 2, 3, 4];
|
|
12
|
+
const EDITOR_EXTENSIONS = [
|
|
13
|
+
StarterKit.configure({
|
|
14
|
+
heading: { levels: [...HEADING_LEVELS] },
|
|
15
|
+
}),
|
|
16
|
+
Underline,
|
|
17
|
+
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
|
18
|
+
Table.configure({ resizable: false }),
|
|
19
|
+
TableRow,
|
|
20
|
+
TableHeader,
|
|
21
|
+
TableCell,
|
|
22
|
+
];
|
|
23
|
+
const EDITOR_TABLE_CLASS = '[&_table]:my-2 [&_table]:w-full [&_table]:border-collapse [&_th]:border [&_td]:border [&_th]:border-slate-300 [&_td]:border-slate-300 [&_th]:bg-slate-50 [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1';
|
|
24
|
+
function toolbarButtonClass(isActive) {
|
|
25
|
+
return `inline-flex h-7 min-w-7 items-center justify-center rounded-md px-1.5 transition-colors disabled:opacity-30 ${isActive ? 'bg-indigo-600 text-white' : 'text-slate-600 hover:bg-slate-100'}`;
|
|
10
26
|
}
|
|
11
|
-
|
|
27
|
+
function ToolbarGroup({ children }) {
|
|
28
|
+
return (_jsx("div", { className: "flex items-center gap-0.5 rounded-md bg-white px-0.5 py-0.5 shadow-sm ring-1 ring-slate-200/80", children: children }));
|
|
29
|
+
}
|
|
30
|
+
function ToolbarButton({ editor, disabled, active, title, onClick, children, }) {
|
|
31
|
+
return (_jsx("button", { type: "button", title: title, disabled: disabled || !editor, onClick: onClick, className: toolbarButtonClass(!!active), children: children }));
|
|
32
|
+
}
|
|
33
|
+
function EditorToolbar({ editor, disabled, }) {
|
|
34
|
+
const iconClass = 'h-4 w-4';
|
|
35
|
+
return (_jsxs("div", { className: "relative flex flex-wrap items-center gap-1 rounded-t-md border-b border-slate-200 bg-slate-100 px-1.5 py-1", children: [_jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { editor: editor, disabled: disabled, title: "Ho\u00E0n t\u00E1c", onClick: () => editor?.chain().focus().undo().run(), children: _jsx(Undo2, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, title: "L\u00E0m l\u1EA1i", onClick: () => editor?.chain().focus().redo().run(), children: _jsx(Redo2, { className: iconClass, strokeWidth: 2 }) })] }), _jsx(ToolbarGroup, { children: HEADING_LEVELS.map((level) => (_jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('heading', { level }), title: `Heading ${level}`, onClick: () => editor?.chain().focus().toggleHeading({ level }).run(), children: _jsxs("span", { className: "text-[11px] font-bold", children: ["H", level] }) }, level))) }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('bold'), title: "\u0110\u1EADm", onClick: () => editor?.chain().focus().toggleBold().run(), children: _jsx(Bold, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('italic'), title: "Nghi\u00EAng", onClick: () => editor?.chain().focus().toggleItalic().run(), children: _jsx(Italic, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('underline'), title: "G\u1EA1ch ch\u00E2n", onClick: () => editor?.chain().focus().toggleUnderline().run(), children: _jsx(UnderlineIcon, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('strike'), title: "G\u1EA1ch ngang", onClick: () => editor?.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('code'), title: "Code", onClick: () => editor?.chain().focus().toggleCode().run(), children: _jsx(Code, { className: iconClass, strokeWidth: 2 }) })] }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('bulletList'), title: "Danh s\u00E1ch", onClick: () => editor?.chain().focus().toggleBulletList().run(), children: _jsx(List, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('orderedList'), title: "Danh s\u00E1ch s\u1ED1", onClick: () => editor?.chain().focus().toggleOrderedList().run(), children: _jsx(ListOrdered, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('blockquote'), title: "Tr\u00EDch d\u1EABn", onClick: () => editor?.chain().focus().toggleBlockquote().run(), children: _jsx(Quote, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('codeBlock'), title: "Kh\u1ED1i code", onClick: () => editor?.chain().focus().toggleCodeBlock().run(), children: _jsx(SquareCode, { className: iconClass, strokeWidth: 2 }) })] }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive({ textAlign: 'left' }), title: "C\u0103n tr\u00E1i", onClick: () => editor?.chain().focus().setTextAlign('left').run(), children: _jsx(AlignLeft, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive({ textAlign: 'center' }), title: "C\u0103n gi\u1EEFa", onClick: () => editor?.chain().focus().setTextAlign('center').run(), children: _jsx(AlignCenter, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive({ textAlign: 'right' }), title: "C\u0103n ph\u1EA3i", onClick: () => editor?.chain().focus().setTextAlign('right').run(), children: _jsx(AlignRight, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive({ textAlign: 'justify' }), title: "C\u0103n \u0111\u1EC1u", onClick: () => editor?.chain().focus().setTextAlign('justify').run(), children: _jsx(AlignJustify, { className: iconClass, strokeWidth: 2 }) })] }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { editor: editor, disabled: disabled, title: "\u0110\u01B0\u1EDDng k\u1EBB ngang", onClick: () => editor?.chain().focus().setHorizontalRule().run(), children: _jsx(Minus, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, title: "Ch\u00E8n b\u1EA3ng", onClick: () => editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), children: _jsx(TableIcon, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled || !editor?.can().addRowAfter(), title: "Th\u00EAm h\u00E0ng", onClick: () => editor?.chain().focus().addRowAfter().run(), children: _jsx(Rows3, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled || !editor?.can().addColumnAfter(), title: "Th\u00EAm c\u1ED9t", onClick: () => editor?.chain().focus().addColumnAfter().run(), children: _jsx(Columns3, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled || !editor?.can().deleteTable(), title: "X\u00F3a b\u1EA3ng", onClick: () => editor?.chain().focus().deleteTable().run(), children: _jsx(Trash2, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, title: "X\u00F3a \u0111\u1ECBnh d\u1EA1ng", onClick: () => editor?.chain().focus().unsetAllMarks().clearNodes().run(), children: _jsx(RemoveFormatting, { className: iconClass, strokeWidth: 2 }) })] })] }));
|
|
36
|
+
}
|
|
37
|
+
export function RichTextEditor({ value, onChange, label, disabled = false, minHeightClassName = 'min-h-[250px]', variant = 'default', }) {
|
|
12
38
|
const [mounted, setMounted] = useState(false);
|
|
39
|
+
const isCompact = variant === 'compact';
|
|
13
40
|
const editor = useEditor({
|
|
14
41
|
immediatelyRender: false,
|
|
15
|
-
extensions:
|
|
42
|
+
extensions: EDITOR_EXTENSIONS,
|
|
16
43
|
editable: !disabled,
|
|
17
44
|
content: value || EMPTY_CONTENT,
|
|
18
45
|
onUpdate: ({ editor: currentEditor }) => {
|
|
@@ -20,7 +47,7 @@ export function RichTextEditor({ value, onChange, label, disabled = false, minHe
|
|
|
20
47
|
},
|
|
21
48
|
editorProps: {
|
|
22
49
|
attributes: {
|
|
23
|
-
class: `tiptap-editor focus:outline-none ${minHeightClassName} p-4 bg-white
|
|
50
|
+
class: `tiptap-editor focus:outline-none ${minHeightClassName} ${isCompact ? 'p-2' : 'p-4'} bg-white rounded-b-md prose prose-sm max-w-none ${EDITOR_TABLE_CLASS}`,
|
|
24
51
|
},
|
|
25
52
|
},
|
|
26
53
|
});
|
|
@@ -39,9 +66,5 @@ export function RichTextEditor({ value, onChange, label, disabled = false, minHe
|
|
|
39
66
|
if (!mounted) {
|
|
40
67
|
return null;
|
|
41
68
|
}
|
|
42
|
-
return (_jsxs("div", { className:
|
|
43
|
-
?.chain()
|
|
44
|
-
.focus()
|
|
45
|
-
.toggleHeading({ level })
|
|
46
|
-
.run(), disabled: disabled, className: `${getToolbarButtonClass(!!editor?.isActive('heading', { level }))} font-bold`, children: ["H", level] }, level))), _jsx("div", { className: "mx-1 h-6 w-px self-center bg-gray-300" }), _jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleBulletList().run(), disabled: disabled, className: getToolbarButtonClass(!!editor?.isActive('bulletList')), children: "\u2022 List" }), _jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleOrderedList().run(), disabled: disabled, className: getToolbarButtonClass(!!editor?.isActive('orderedList')), children: "1. List" })] }), _jsx(EditorContent, { editor: editor })] })] }));
|
|
69
|
+
return (_jsxs("div", { className: `flex w-full flex-col ${isCompact ? 'gap-0' : 'gap-4'}`, children: [label ? _jsx("div", { children: label }) : null, _jsxs("div", { className: "flex flex-col overflow-hidden rounded-md border border-gray-300 shadow-sm", children: [_jsx(EditorToolbar, { editor: editor, disabled: disabled }), _jsx(EditorContent, { editor: editor })] })] }));
|
|
47
70
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
interface RichTextHtmlProps {
|
|
3
|
+
html?: string | null;
|
|
4
|
+
className?: string;
|
|
5
|
+
fallback?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export declare function RichTextHtml({ html, className, fallback }: RichTextHtmlProps): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../../shared/lib/utils';
|
|
3
|
+
const RICH_TEXT_CLASS = 'rich-text-html max-w-none [&_p]:m-0 [&_p+p]:mt-1 [&_h1]:mb-1 [&_h1]:mt-0 [&_h1]:text-xl [&_h1]:font-bold [&_h2]:mb-1 [&_h2]:mt-0 [&_h2]:text-lg [&_h2]:font-bold [&_h3]:mb-1 [&_h3]:mt-0 [&_h3]:text-base [&_h3]:font-bold [&_h4]:mb-1 [&_h4]:mt-0 [&_h4]:text-sm [&_h4]:font-bold [&_ul]:my-1 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-1 [&_ol]:list-decimal [&_ol]:pl-5 [&_blockquote]:my-1 [&_blockquote]:border-l-2 [&_blockquote]:border-slate-300 [&_blockquote]:pl-3 [&_pre]:my-1 [&_pre]:overflow-x-auto [&_pre]:rounded [&_pre]:bg-slate-100 [&_pre]:p-2 [&_code]:rounded [&_code]:bg-slate-100 [&_code]:px-1 [&_u]:underline [&_s]:line-through [&_table]:my-2 [&_table]:w-full [&_table]:border-collapse [&_th]:border [&_td]:border [&_th]:border-slate-300 [&_td]:border-slate-300 [&_th]:bg-slate-50 [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1 [&_hr]:my-2';
|
|
4
|
+
export function RichTextHtml({ html, className, fallback = null }) {
|
|
5
|
+
if (!html) {
|
|
6
|
+
return fallback ? _jsx(_Fragment, { children: fallback }) : null;
|
|
7
|
+
}
|
|
8
|
+
return (_jsx("div", { className: cn(RICH_TEXT_CLASS, className), dangerouslySetInnerHTML: { __html: html } }));
|
|
9
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -26,4 +26,4 @@ export { validateQuestion } from './components/questions/_shared/utils/question-
|
|
|
26
26
|
export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
|
|
27
27
|
export type { QuestionFormState } from './components/questions/_shared/hooks/useQuestionFormState';
|
|
28
28
|
export * from './components/questions/_shared/types';
|
|
29
|
-
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, } from './components/questions/viewer';
|
|
29
|
+
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, } from './components/questions/viewer';
|
package/dist/index.js
CHANGED
|
@@ -21,4 +21,4 @@ export { QUESTION_TYPE_CONFIG, QUESTION_TYPES_WITH_DEDICATED_COMPONENTS } from '
|
|
|
21
21
|
export { validateQuestion } from './components/questions/_shared/utils/question-validation';
|
|
22
22
|
export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
|
|
23
23
|
export * from './components/questions/_shared/types';
|
|
24
|
-
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, } from './components/questions/viewer';
|
|
24
|
+
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, } from './components/questions/viewer';
|
|
@@ -84,6 +84,9 @@ export const QUESTION_TYPE_SKILLS = {
|
|
|
84
84
|
'WORD_FILL_STRUCTURED_FORM': SkillEnum.READING,
|
|
85
85
|
'CROSSWORD_PUZZLE': SkillEnum.READING,
|
|
86
86
|
'FIND_WORDS_IN_MATRIX': SkillEnum.READING,
|
|
87
|
+
'MATCH_COLUMNS_TO_MAKE_SENTENCES': SkillEnum.READING,
|
|
88
|
+
'MATCH_WORD_TO_PICTURE_GROUP': SkillEnum.READING,
|
|
89
|
+
'WORD_ORDERING_GROUP': SkillEnum.READING,
|
|
87
90
|
// ========================================
|
|
88
91
|
// WRITING SKILLS (Viết)
|
|
89
92
|
// ========================================
|
|
@@ -216,6 +219,9 @@ export const QUESTION_TYPE_LABELS = {
|
|
|
216
219
|
'WORD_FILL_STRUCTURED_FORM': 'Điền từ vào biểu mẫu có cấu trúc',
|
|
217
220
|
'CROSSWORD_PUZZLE': 'Giải ô chữ (Crossword)',
|
|
218
221
|
'FIND_WORDS_IN_MATRIX': 'Tìm từ trong ma trận (Word Search)',
|
|
222
|
+
'MATCH_COLUMNS_TO_MAKE_SENTENCES': 'Ghép cột thành câu',
|
|
223
|
+
'MATCH_WORD_TO_PICTURE_GROUP': 'Nối từ với tranh (nhóm)',
|
|
224
|
+
'WORD_ORDERING_GROUP': 'Sắp xếp từ (nhóm)',
|
|
219
225
|
// ========================================
|
|
220
226
|
// WRITING SKILLS (Viết)
|
|
221
227
|
// ========================================
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const HTML_TAG_RE = /<[^>]*>/g;
|
|
2
|
+
const HTML_ENTITIES = {
|
|
3
|
+
' ': ' ',
|
|
4
|
+
'&': '&',
|
|
5
|
+
'<': '<',
|
|
6
|
+
'>': '>',
|
|
7
|
+
'"': '"',
|
|
8
|
+
''': "'",
|
|
9
|
+
};
|
|
10
|
+
/** Strip HTML tags/entities so empty TipTap documents (`<p></p>`) count as blank. */
|
|
11
|
+
export function richTextToPlainText(value) {
|
|
12
|
+
if (!value)
|
|
13
|
+
return '';
|
|
14
|
+
return value
|
|
15
|
+
.replace(HTML_TAG_RE, ' ')
|
|
16
|
+
.replace(/ |&|<|>|"|'/gi, (entity) => {
|
|
17
|
+
return HTML_ENTITIES[entity.toLowerCase()] ?? entity;
|
|
18
|
+
})
|
|
19
|
+
.replace(/\s+/g, ' ')
|
|
20
|
+
.trim();
|
|
21
|
+
}
|
|
22
|
+
export function isRichTextEmpty(value) {
|
|
23
|
+
return richTextToPlainText(value) === '';
|
|
24
|
+
}
|
|
@@ -226,7 +226,9 @@ function normalizeChooseAnswerGroupImageUrl(value) {
|
|
|
226
226
|
const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
|
|
227
227
|
const content = asRecord(apiQuestion.content);
|
|
228
228
|
const answer = readAnswer(apiQuestion);
|
|
229
|
-
const fallbackAnswers = asArray(answer.answer).map((item) => asString(item))
|
|
229
|
+
const fallbackAnswers = asArray(answer.answer).map((item) => (asString(item) || (Array.isArray(item)
|
|
230
|
+
? asString(item.find((token) => typeof token === 'string' && token.trim() !== ''))
|
|
231
|
+
: '')));
|
|
230
232
|
const items = asArray(content.items).map((rawItem, index) => {
|
|
231
233
|
const item = asRecord(rawItem);
|
|
232
234
|
const itemContent = asRecord(item.content);
|
|
@@ -251,7 +253,13 @@ const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
|
|
|
251
253
|
{ id: 'opt-2', text: '' },
|
|
252
254
|
{ id: 'opt-3', text: '' },
|
|
253
255
|
],
|
|
254
|
-
correctAnswer: asString(itemAnswer.answer)
|
|
256
|
+
correctAnswer: asString(itemAnswer.answer)
|
|
257
|
+
|| (Array.isArray(itemAnswer.answer)
|
|
258
|
+
? asString(itemAnswer.answer.find((token) => typeof token === 'string' && token.trim() !== ''))
|
|
259
|
+
: '')
|
|
260
|
+
|| fallbackAnswers[index]
|
|
261
|
+
|| options[0]?.id
|
|
262
|
+
|| 'opt-1',
|
|
255
263
|
isExample,
|
|
256
264
|
points: isExample ? 0 : asNumber(item.points, 1),
|
|
257
265
|
questionNumber: asNumber(item.questionNumber, index + 1),
|
|
@@ -260,12 +268,14 @@ const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
|
|
|
260
268
|
};
|
|
261
269
|
});
|
|
262
270
|
const audioUrl = asString(asRecord(content.meta).audioUrl).trim();
|
|
271
|
+
const imageUrl = normalizeChooseAnswerGroupImageUrl(asRecord(content.meta).imageUrl);
|
|
263
272
|
return {
|
|
264
273
|
content: '',
|
|
265
274
|
answer: {
|
|
266
275
|
instruction: asString(asRecord(content.meta).instruction, 'Choose the correct answer.'),
|
|
267
276
|
passage: asString(asRecord(content.meta).passage),
|
|
268
277
|
...(audioUrl ? { audioUrl } : {}),
|
|
278
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
269
279
|
items,
|
|
270
280
|
explanation: asString(apiQuestion.explanation),
|
|
271
281
|
points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
|
|
@@ -603,6 +613,102 @@ const reverseSimpleAnswer = (apiQuestion) => {
|
|
|
603
613
|
},
|
|
604
614
|
};
|
|
605
615
|
};
|
|
616
|
+
const reverseWordOrderingGroup = (apiQuestion) => {
|
|
617
|
+
const content = asRecord(apiQuestion.content);
|
|
618
|
+
const answer = readAnswer(apiQuestion);
|
|
619
|
+
const fallbackAnswers = asArray(answer.answer);
|
|
620
|
+
const items = asArray(content.items);
|
|
621
|
+
const meta = asRecord(content.meta);
|
|
622
|
+
const mappedItems = items.map((rawItem, index) => {
|
|
623
|
+
const item = asRecord(rawItem);
|
|
624
|
+
const itemContent = asRecord(item.content);
|
|
625
|
+
const itemAnswer = asRecord(item.correctAnswer);
|
|
626
|
+
const isExample = item.isExample === true;
|
|
627
|
+
const words = asArray(itemContent.words).map((token) => asString(token)).filter(Boolean);
|
|
628
|
+
const itemTokens = asArray(itemAnswer.answer).map((token) => asString(token)).filter(Boolean);
|
|
629
|
+
const fallbackTokens = asArray(fallbackAnswers[index]).map((token) => asString(token)).filter(Boolean);
|
|
630
|
+
return {
|
|
631
|
+
words,
|
|
632
|
+
answer: itemTokens.length ? itemTokens : fallbackTokens,
|
|
633
|
+
isExample,
|
|
634
|
+
points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
635
|
+
questionNumber: typeof item.questionNumber === 'number' ? item.questionNumber : index + 1,
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
return {
|
|
639
|
+
content: '',
|
|
640
|
+
answer: {
|
|
641
|
+
instruction: asString(meta.instruction, 'Order the words.'),
|
|
642
|
+
items: mappedItems,
|
|
643
|
+
explanation: asString(apiQuestion.explanation),
|
|
644
|
+
points: mappedItems.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
};
|
|
648
|
+
const reverseMatchWordToPictureGroup = (apiQuestion) => {
|
|
649
|
+
const content = asRecord(apiQuestion.content);
|
|
650
|
+
const answer = readAnswer(apiQuestion);
|
|
651
|
+
const fallbackAnswers = asArray(answer.answer);
|
|
652
|
+
const items = asArray(content.items);
|
|
653
|
+
const meta = asRecord(content.meta);
|
|
654
|
+
const mappedItems = items.map((rawItem, index) => {
|
|
655
|
+
const item = asRecord(rawItem);
|
|
656
|
+
const itemContent = asRecord(item.content);
|
|
657
|
+
const itemAnswer = asRecord(item.correctAnswer);
|
|
658
|
+
const isExample = item.isExample === true;
|
|
659
|
+
return {
|
|
660
|
+
imageUrl: asString(itemContent.imageUrl),
|
|
661
|
+
correctAnswer: asString(itemAnswer.answer) || asString(fallbackAnswers[index]),
|
|
662
|
+
isExample,
|
|
663
|
+
points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
664
|
+
questionNumber: typeof item.questionNumber === 'number' ? item.questionNumber : index + 1,
|
|
665
|
+
};
|
|
666
|
+
});
|
|
667
|
+
return {
|
|
668
|
+
content: '',
|
|
669
|
+
answer: {
|
|
670
|
+
instruction: asString(meta.instruction, 'Look and complete.'),
|
|
671
|
+
wordBank: asArray(meta.wordBank).map((word) => asString(word)).filter(Boolean),
|
|
672
|
+
items: mappedItems,
|
|
673
|
+
explanation: asString(apiQuestion.explanation),
|
|
674
|
+
points: mappedItems.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
|
|
675
|
+
},
|
|
676
|
+
};
|
|
677
|
+
};
|
|
678
|
+
const reverseMatchColumnsToMakeSentences = (apiQuestion) => {
|
|
679
|
+
const content = asRecord(apiQuestion.content);
|
|
680
|
+
const answer = readAnswer(apiQuestion);
|
|
681
|
+
const fallbackAnswers = asArray(answer.answer);
|
|
682
|
+
const items = asArray(content.items);
|
|
683
|
+
const columns = asArray(content.columns);
|
|
684
|
+
const meta = asRecord(content.meta);
|
|
685
|
+
const mappedItems = items.map((rawItem, index) => {
|
|
686
|
+
const item = asRecord(rawItem);
|
|
687
|
+
const itemContent = asRecord(item.content);
|
|
688
|
+
const itemAnswer = asRecord(asRecord(item.correctAnswer).answer ?? fallbackAnswers[index]);
|
|
689
|
+
const fragmentIds = asArray(itemAnswer.fragmentIds).map((id) => asString(id)).filter(Boolean);
|
|
690
|
+
const isExample = item.isExample === true;
|
|
691
|
+
return {
|
|
692
|
+
stemFragmentId: asString(itemContent.stemFragmentId) || fragmentIds[0] || '',
|
|
693
|
+
fragmentIds,
|
|
694
|
+
sentence: asString(itemAnswer.sentence),
|
|
695
|
+
isExample,
|
|
696
|
+
points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
697
|
+
questionNumber: typeof item.questionNumber === 'number' ? item.questionNumber : index + 1,
|
|
698
|
+
};
|
|
699
|
+
});
|
|
700
|
+
return {
|
|
701
|
+
content: '',
|
|
702
|
+
answer: {
|
|
703
|
+
title: asString(meta.title),
|
|
704
|
+
instruction: asString(meta.instruction, 'Match the information to make sentences. You can use the same words more than once.'),
|
|
705
|
+
columns,
|
|
706
|
+
items: mappedItems,
|
|
707
|
+
explanation: asString(apiQuestion.explanation),
|
|
708
|
+
points: mappedItems.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
};
|
|
606
712
|
const handlers = {
|
|
607
713
|
CHOOSE_THE_CORRECT_ANSWER: reverseChooseTheCorrectAnswer,
|
|
608
714
|
CHOOSE_THE_CORRECT_ANSWER_GROUP: reverseChooseTheCorrectAnswerGroup,
|
|
@@ -653,6 +759,9 @@ const handlers = {
|
|
|
653
759
|
GN_SPEAKING_INTERVIEW: reverseGroupPayload,
|
|
654
760
|
CROSSWORD_PUZZLE: reverseCrosswordPuzzle,
|
|
655
761
|
FIND_WORDS_IN_MATRIX: reverseFindWordsInMatrix,
|
|
762
|
+
MATCH_COLUMNS_TO_MAKE_SENTENCES: reverseMatchColumnsToMakeSentences,
|
|
763
|
+
MATCH_WORD_TO_PICTURE_GROUP: reverseMatchWordToPictureGroup,
|
|
764
|
+
WORD_ORDERING_GROUP: reverseWordOrderingGroup,
|
|
656
765
|
};
|
|
657
766
|
function reverseWriteAShortLetter(apiQuestion) {
|
|
658
767
|
const content = asRecord(apiQuestion.content);
|
|
@@ -47,6 +47,9 @@ import { transformGNSpeakingInterview } from '../../../components/questions/type
|
|
|
47
47
|
import { transformWordFillStructuredForm } from '../../../components/questions/types/word-fill-structured-form/transform';
|
|
48
48
|
import { transformCrosswordPuzzle } from '../../../components/questions/types/crossword-puzzle/transform';
|
|
49
49
|
import { transformFindWordsInMatrix } from '../../../components/questions/types/find-words-in-matrix/transform';
|
|
50
|
+
import { transformMatchColumnsToMakeSentences } from '../../../components/questions/types/match-columns-to-make-sentences/transform';
|
|
51
|
+
import { transformMatchWordToPictureGroup } from '../../../components/questions/types/match-word-to-picture-group/transform';
|
|
52
|
+
import { transformWordOrderingGroup } from '../../../components/questions/types/word-ordering-group/transform';
|
|
50
53
|
// ─── Handler map ────────────────────────────────────────────────────────────
|
|
51
54
|
const questionTransformHandlers = {
|
|
52
55
|
// Look picture
|
|
@@ -100,6 +103,9 @@ const questionTransformHandlers = {
|
|
|
100
103
|
WORD_FILL_STRUCTURED_FORM: transformWordFillStructuredForm,
|
|
101
104
|
CROSSWORD_PUZZLE: transformCrosswordPuzzle,
|
|
102
105
|
FIND_WORDS_IN_MATRIX: transformFindWordsInMatrix,
|
|
106
|
+
MATCH_COLUMNS_TO_MAKE_SENTENCES: transformMatchColumnsToMakeSentences,
|
|
107
|
+
MATCH_WORD_TO_PICTURE_GROUP: transformMatchWordToPictureGroup,
|
|
108
|
+
WORD_ORDERING_GROUP: transformWordOrderingGroup,
|
|
103
109
|
};
|
|
104
110
|
/**
|
|
105
111
|
* Transform frontend question format to API format.
|