@tinyweb_dev/oe-exam-sdk 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/exam-questions.d.ts +1 -1
- package/dist/api/exam-questions.js +1 -39
- package/dist/api/exam-questions.types.d.ts +0 -28
- package/dist/components/exams/ExamCreator.d.ts +1 -7
- package/dist/components/exams/ExamCreator.js +21 -57
- package/dist/components/exams/ExamQuestionsPageContainer.d.ts +0 -5
- 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 +1 -6
- package/dist/components/exams/take/components/QuestionRenderer.js +2 -2
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.d.ts +1 -7
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +22 -38
- package/dist/components/exams/take/exam-taking.utils.d.ts +2 -2
- package/dist/components/exams/take/exam-taking.utils.js +2 -0
- package/dist/components/exams/take/index.d.ts +0 -6
- package/dist/components/exams/take/index.js +0 -5
- package/dist/components/exams/take/utils/question-transformers.d.ts +2 -2
- package/dist/components/exams/take/utils/question-transformers.js +4 -25
- package/dist/components/questions/_shared/types/word-fill-structured-form.type.d.ts +1 -1
- package/dist/components/questions/question-bank/index.d.ts +0 -1
- package/dist/components/questions/question-bank/index.js +0 -1
- package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.d.ts +2 -2
- package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.js +15 -5
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +39 -15
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +35 -12
- package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +3 -18
- package/dist/components/questions/types/word-fill-structured-form/transform.js +2 -5
- package/dist/components/questions/viewer/QuestionViewer.js +0 -67
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +12 -3
- package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +13 -20
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1 -2
- package/dist/shared/constants/ApiConstant.d.ts +0 -1
- package/dist/shared/constants/ApiConstant.js +0 -1
- package/dist/shared/constants/routes.d.ts +2 -2
- package/dist/shared/constants/routes.js +3 -3
- package/dist/shared/lib/utils/wfsf-blank-match.d.ts +9 -0
- package/dist/shared/lib/utils/wfsf-blank-match.js +60 -0
- package/package.json +1 -1
- package/dist/components/questions/question-bank/QuestionBankPickerDialog.d.ts +0 -45
- package/dist/components/questions/question-bank/QuestionBankPickerDialog.js +0 -209
- package/dist/components/questions/question-bank/QuestionBankPickerRow.d.ts +0 -9
- package/dist/components/questions/question-bank/QuestionBankPickerRow.js +0 -20
- package/dist/components/questions/question-bank/question-bank-picker.utils.d.ts +0 -55
- package/dist/components/questions/question-bank/question-bank-picker.utils.js +0 -408
- package/dist/components/themes/cambridge-yle/assets/images/bg-title-old.png +0 -0
- package/dist/components/themes/cambridge-yle/assets/images/border-old.png +0 -0
- package/dist/components/themes/cambridge-yle/assets/images/close_old.png +0 -0
- package/dist/components/themes/cambridge-yle/assets/images/frame-old.png +0 -0
|
@@ -18,7 +18,6 @@ import { WriteAShortLetterClient } from '../types/write-a-short-letter/WriteASho
|
|
|
18
18
|
import { LookPictureChooseCorrectAnswerClient } from '../types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient';
|
|
19
19
|
import { LookPictureFillBlankChooseAnswerClient } from '../types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient';
|
|
20
20
|
import { FillMissingWordsInGridClient } from '../types/fill-missing-words-in-grid/FillMissingWordsInGridClient';
|
|
21
|
-
import { WordFillStructuredFormClient } from '../types/word-fill-structured-form/WordFillStructuredFormClient';
|
|
22
21
|
import { CrosswordPuzzleClient } from '../types/crossword-puzzle/CrosswordPuzzleClient';
|
|
23
22
|
import { FindWordsInMatrixClient } from '../types/find-words-in-matrix/FindWordsInMatrixClient';
|
|
24
23
|
import { SortWordsIntoCategoriesViewer } from './SortWordsIntoCategoriesViewer';
|
|
@@ -517,72 +516,6 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
517
516
|
onSubmitAnswer?.(answers);
|
|
518
517
|
} }));
|
|
519
518
|
};
|
|
520
|
-
const renderWordFillStructuredForm = () => {
|
|
521
|
-
const content = (question.content && typeof question.content === 'object'
|
|
522
|
-
? question.content
|
|
523
|
-
: {});
|
|
524
|
-
// Pearson preview maps correctAnswer → answer; bank may use either shape.
|
|
525
|
-
const answer = (question.answer ??
|
|
526
|
-
question.correctAnswer ??
|
|
527
|
-
{});
|
|
528
|
-
const answers = answer?.answers && typeof answer.answers === 'object' && !Array.isArray(answer.answers)
|
|
529
|
-
? answer.answers
|
|
530
|
-
: {};
|
|
531
|
-
const html = String(content.content || '');
|
|
532
|
-
const blankRaw = String(content.blankStyle || '').toLowerCase();
|
|
533
|
-
const blankStyle = blankRaw === 'letter' || blankRaw === 'box'
|
|
534
|
-
? 'letter'
|
|
535
|
-
: blankRaw === 'phrase' || blankRaw === 'sentence'
|
|
536
|
-
? 'phrase'
|
|
537
|
-
: 'word';
|
|
538
|
-
const userMap = userAnswer && typeof userAnswer === 'object' && !Array.isArray(userAnswer)
|
|
539
|
-
? userAnswer
|
|
540
|
-
: selectedAnswer && typeof selectedAnswer === 'object' && !Array.isArray(selectedAnswer)
|
|
541
|
-
? selectedAnswer
|
|
542
|
-
: isReviewMode
|
|
543
|
-
? answers
|
|
544
|
-
: {};
|
|
545
|
-
const qNum = typeof question.questionNumber === 'number' && question.questionNumber > 0
|
|
546
|
-
? question.questionNumber
|
|
547
|
-
: undefined;
|
|
548
|
-
const rawWordBank = Array.isArray(content.wordBank) ? content.wordBank : undefined;
|
|
549
|
-
const wordBank = rawWordBank?.map((entry) => {
|
|
550
|
-
if (entry == null || typeof entry !== 'object')
|
|
551
|
-
return null;
|
|
552
|
-
const rec = entry;
|
|
553
|
-
if (Array.isArray(rec.options)) {
|
|
554
|
-
const options = rec.options.map((o) => String(o ?? '').trim()).filter(Boolean);
|
|
555
|
-
return options.length >= 2 ? { options } : null;
|
|
556
|
-
}
|
|
557
|
-
const word = String(rec.word ?? '').trim();
|
|
558
|
-
if (word)
|
|
559
|
-
return rec.isExample ? { word, isExample: true } : { word };
|
|
560
|
-
return null;
|
|
561
|
-
});
|
|
562
|
-
const hasWordOptions = Boolean(wordBank?.some((e) => e != null &&
|
|
563
|
-
'options' in e &&
|
|
564
|
-
Array.isArray(e.options) &&
|
|
565
|
-
(e.options?.length ?? 0) >= 2));
|
|
566
|
-
const hasSharedWords = Boolean(wordBank?.some((e) => e != null && 'word' in e && e.word));
|
|
567
|
-
const viewMode = String(content.viewMode || '').trim() ||
|
|
568
|
-
(hasWordOptions ? 'WITH_OPTIONS' : hasSharedWords ? 'WITH_WORD_BANK' : undefined);
|
|
569
|
-
return (_jsx(WordFillStructuredFormClient, { questionData: {
|
|
570
|
-
title: String(content.title || ''),
|
|
571
|
-
imageUrl: content.imageUrl ? String(content.imageUrl) : undefined,
|
|
572
|
-
content: html,
|
|
573
|
-
blankStyle,
|
|
574
|
-
answers,
|
|
575
|
-
caseSensitive: Boolean(answer?.caseSensitive),
|
|
576
|
-
explanation: question.explanation,
|
|
577
|
-
...(viewMode ? { viewMode } : {}),
|
|
578
|
-
...((hasWordOptions || hasSharedWords) && wordBank ? { wordBank } : {}),
|
|
579
|
-
}, userAnswer: userMap, isReviewMode: isReviewMode, autoFillCorrectAnswers: isReviewMode && Object.keys(userMap).length === 0, explanation: question.explanation, questionNumber: qNum, onAnswerChange: (key, value) => {
|
|
580
|
-
const prev = selectedAnswer && typeof selectedAnswer === 'object' && !Array.isArray(selectedAnswer)
|
|
581
|
-
? selectedAnswer
|
|
582
|
-
: {};
|
|
583
|
-
setSelectedAnswer({ ...prev, [key]: value });
|
|
584
|
-
} }));
|
|
585
|
-
};
|
|
586
519
|
const renderWordFillParagraph = () => {
|
|
587
520
|
const content = (question.content && typeof question.content === 'object'
|
|
588
521
|
? question.content
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Info, Megaphone } from 'lucide-react';
|
|
4
|
+
/** CMS instruction may be plain text or HTML (e.g. `<p>…</p>`). */
|
|
5
|
+
function isHtmlContent(value) {
|
|
6
|
+
return /<[a-z][\s\S]*>/i.test(value);
|
|
7
|
+
}
|
|
8
|
+
function InstructionHtml({ html, className }) {
|
|
9
|
+
return (_jsx("div", { className: className,
|
|
10
|
+
// Exam CMS content — not end-user input.
|
|
11
|
+
dangerouslySetInnerHTML: { __html: html } }));
|
|
12
|
+
}
|
|
4
13
|
export default function CambridgeYleInstructionBanner({ instruction, instructions, hasExample = false, children, isReviewMode = false, compact = false, }) {
|
|
5
14
|
if (isReviewMode)
|
|
6
15
|
return null;
|
|
@@ -11,9 +20,9 @@ export default function CambridgeYleInstructionBanner({ instruction, instruction
|
|
|
11
20
|
? 'flex w-full items-center gap-2 rounded-[14px] border-2 border-dashed border-blue-300 bg-[#F8FBFF] px-3 py-2 transition-all hover:bg-blue-50/50 lg:gap-3 lg:px-4 lg:py-2.5 xl:gap-5 xl:rounded-[24px] xl:px-5 xl:py-4'
|
|
12
21
|
: 'flex w-full items-center gap-4 rounded-[24px] border-2 border-dashed border-blue-300 bg-[#F8FBFF] px-5 py-4 transition-all hover:bg-blue-50/50 sm:gap-5', children: [_jsx("div", { className: compact
|
|
13
22
|
? 'flex h-8 w-8 shrink-0 -rotate-6 items-center justify-center rounded-[10px] bg-[#0070FF] shadow-[3px_3px_0px_rgba(191,219,254,1)] transition-transform hover:rotate-0 lg:h-9 lg:w-9 xl:h-14 xl:w-14 xl:rounded-[16px] xl:shadow-[4px_4px_0px_rgba(191,219,254,1)]'
|
|
14
|
-
: 'flex h-12 w-12 shrink-0 -rotate-6 items-center justify-center rounded-[16px] bg-[#0070FF] shadow-[4px_4px_0px_rgba(191,219,254,1)] transition-transform hover:rotate-0 sm:h-14 sm:w-14', children: _jsx(Megaphone, { className: compact ? 'h-4 w-4 text-white lg:h-5 lg:w-5 xl:h-7 xl:w-7' : 'h-6 w-6 text-white sm:h-7 sm:w-7', strokeWidth: 2.5 }) }), _jsx("div", { className: "flex flex-1 flex-col justify-center", children: _jsxs("
|
|
23
|
+
: 'flex h-12 w-12 shrink-0 -rotate-6 items-center justify-center rounded-[16px] bg-[#0070FF] shadow-[4px_4px_0px_rgba(191,219,254,1)] transition-transform hover:rotate-0 sm:h-14 sm:w-14', children: _jsx(Megaphone, { className: compact ? 'h-4 w-4 text-white lg:h-5 lg:w-5 xl:h-7 xl:w-7' : 'h-6 w-6 text-white sm:h-7 sm:w-7', strokeWidth: 2.5 }) }), _jsx("div", { className: "flex flex-1 flex-col justify-center", children: _jsxs("div", { className: compact
|
|
15
24
|
? 'text-[12px] font-black uppercase leading-tight tracking-wide text-[#002868] lg:text-[13px] xl:text-[18px] xl:leading-snug'
|
|
16
|
-
: 'text-[15px] font-black uppercase leading-snug tracking-wide text-[#002868] sm:text-[18px]', style: { fontFamily: '"SVN-Volte Rounded", "Quicksand", sans-serif' }, children: [instruction, hasExample && (_jsx("span", { className: compact
|
|
25
|
+
: 'text-[15px] font-black uppercase leading-snug tracking-wide text-[#002868] sm:text-[18px]', style: { fontFamily: '"SVN-Volte Rounded", "Quicksand", sans-serif' }, children: [isHtmlContent(instruction) ? (_jsx(InstructionHtml, { html: instruction, className: "contents [&_p]:m-0 [&_p]:inline" })) : (instruction), hasExample && (_jsx("span", { className: compact
|
|
17
26
|
? 'ml-1 inline-block rounded-full bg-blue-100 px-1.5 py-0.5 align-middle text-[10px] font-bold tracking-normal text-[#0070FF] xl:-mt-1 xl:ml-2 xl:px-3 xl:text-[16px]'
|
|
18
|
-
: 'ml-2 -mt-1 inline-block rounded-full bg-blue-100 px-3 py-0.5 align-middle text-[14px] font-bold tracking-normal text-[#0070FF] sm:text-[16px]', children: "THERE IS ONE EXAMPLE" }))] }) })] })), children, hasInstructionsList && (_jsx("div", { className: "rounded-lg border border-blue-200 bg-white p-4 shadow-sm", children: _jsxs("div", { className: "space-y-3", children: [_jsxs("h4", { className: "flex items-center gap-2 font-semibold text-blue-900", children: [_jsx(Info, { className: "h-4 w-4" }), "Instructions"] }), _jsx("ol", { className: "list-inside list-decimal space-y-1.5 text-sm text-gray-700", children: instructions.map((item, index) => (_jsx("li", { className: "leading-relaxed", children: item }, index))) })] }) }))] }));
|
|
27
|
+
: 'ml-2 -mt-1 inline-block rounded-full bg-blue-100 px-3 py-0.5 align-middle text-[14px] font-bold tracking-normal text-[#0070FF] sm:text-[16px]', children: "THERE IS ONE EXAMPLE" }))] }) })] })), children, hasInstructionsList && (_jsx("div", { className: "rounded-lg border border-blue-200 bg-white p-4 shadow-sm", children: _jsxs("div", { className: "space-y-3", children: [_jsxs("h4", { className: "flex items-center gap-2 font-semibold text-blue-900", children: [_jsx(Info, { className: "h-4 w-4" }), "Instructions"] }), _jsx("ol", { className: "list-inside list-decimal space-y-1.5 text-sm text-gray-700", children: instructions.map((item, index) => (_jsx("li", { className: "leading-relaxed", children: isHtmlContent(item) ? (_jsx(InstructionHtml, { html: item, className: "[&_p]:m-0" })) : (item) }, index))) })] }) }))] }));
|
|
19
28
|
}
|
|
@@ -5,6 +5,7 @@ import { ResolvedImage } from '../../../components/common/ResolvedImage';
|
|
|
5
5
|
import { cn } from '../../../shared/lib/utils';
|
|
6
6
|
import { transformWordFillStructuredForm } from '../../../components/exams/take/exam-taking.utils';
|
|
7
7
|
import { englishCertQuestionDomId } from './english-cert-scroll';
|
|
8
|
+
import { expandWfsfCorrectAlts, formatWfsfCorrectLabel, isWfsfBlankCorrect, } from '../../../shared/lib/utils/wfsf-blank-match';
|
|
8
9
|
/**
|
|
9
10
|
* Replace `{key}` placeholders in raw HTML with inline `<input>` elements so the
|
|
10
11
|
* full HTML structure (tables, forms, …) is preserved and inputs render inside
|
|
@@ -25,14 +26,6 @@ function buildHtmlWithInputPlaceholders(html) {
|
|
|
25
26
|
`;
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
|
-
function normalizeStructuredAnswer(value, caseSensitive) {
|
|
29
|
-
const normalized = value
|
|
30
|
-
.trim()
|
|
31
|
-
.replace(/[\p{P}]/gu, '')
|
|
32
|
-
.replace(/\s+/g, ' ')
|
|
33
|
-
.trim();
|
|
34
|
-
return caseSensitive ? normalized : normalized.toLowerCase();
|
|
35
|
-
}
|
|
36
29
|
/**
|
|
37
30
|
* Build the card label from the blank keys (which are the IELTS question numbers).
|
|
38
31
|
* Returns "Questions x–y" for a numeric range, a single "Question n", or falls
|
|
@@ -82,28 +75,28 @@ const StructuredFormBody = memo(function StructuredFormBody({ question, initialA
|
|
|
82
75
|
const key = input.getAttribute('data-blank-key') || '';
|
|
83
76
|
const wrapper = container.querySelector(`[data-blank-wrapper="${key}"]`);
|
|
84
77
|
const userAnswer = initialAnswersRef.current[key] || '';
|
|
85
|
-
const correctAnswer = question.answers[key]
|
|
78
|
+
const correctAnswer = question.answers[key];
|
|
79
|
+
const correctLabel = formatWfsfCorrectLabel(correctAnswer);
|
|
86
80
|
const hasUserAnswer = userAnswer.trim().length > 0;
|
|
87
81
|
const isCorrect = hasUserAnswer &&
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const shouldShowReviewText = isReviewMode && !isCorrect && correctAnswer.trim().length > 0;
|
|
82
|
+
isWfsfBlankCorrect(userAnswer, correctAnswer, question.caseSensitive);
|
|
83
|
+
const shouldShowReviewText = isReviewMode && !isCorrect && correctLabel.length > 0;
|
|
91
84
|
input.value = shouldShowReviewText
|
|
92
|
-
? `${hasUserAnswer ? userAnswer : '(empty)'} → ${
|
|
85
|
+
? `${hasUserAnswer ? userAnswer : '(empty)'} → ${correctLabel}`
|
|
93
86
|
: userAnswer;
|
|
94
87
|
input.disabled = isReviewMode;
|
|
95
88
|
const blankStyle = question.blankStyle || 'word';
|
|
96
89
|
const isLetter = blankStyle === 'letter';
|
|
97
|
-
const isPhrase = blankStyle === 'phrase';
|
|
90
|
+
const isPhrase = blankStyle === 'phrase' || blankStyle === 'translate';
|
|
98
91
|
if (isLetter) {
|
|
99
|
-
|
|
92
|
+
const maxAltLen = expandWfsfCorrectAlts(correctAnswer).reduce((max, alt) => Math.max(max, alt.trim().length), 0);
|
|
93
|
+
input.maxLength = Math.max(1, maxAltLen || 1);
|
|
100
94
|
}
|
|
101
95
|
input.className = cn('ec-blank-input inline-block bg-blue-50/30 text-base font-semibold leading-tight text-[#0f1730] align-baseline outline-none transition-all hover:bg-blue-50/60 focus:bg-blue-50/80', isLetter
|
|
102
96
|
? 'h-8 w-8 rounded border-2 border-[#1167e8] px-0 text-center uppercase'
|
|
103
97
|
: isPhrase
|
|
104
|
-
? 'min-w-[
|
|
105
|
-
:
|
|
106
|
-
'min-w-[4.5rem] max-w-[7.5rem] w-[8ch] rounded-t border-0 border-b-2 border-dashed border-[#1167e8] px-1 py-0.5 text-center focus:border-solid focus:border-[#0b57d0]', isReviewMode
|
|
98
|
+
? 'min-w-[16rem] max-w-[36rem] w-[28ch] rounded-t border-0 border-b-2 border-dashed border-[#1167e8] px-2 py-0.5 text-center focus:border-solid focus:border-[#0b57d0]'
|
|
99
|
+
: 'min-w-[4.5rem] max-w-[8rem] w-[10ch] rounded-t border-0 border-b-2 border-dashed border-[#1167e8] px-1 py-0.5 text-center focus:border-solid focus:border-[#0b57d0]', isReviewMode
|
|
107
100
|
? isCorrect
|
|
108
101
|
? 'border-solid border-[#22c55e] bg-emerald-50 text-[#15803d]'
|
|
109
102
|
: hasUserAnswer
|
|
@@ -111,7 +104,7 @@ const StructuredFormBody = memo(function StructuredFormBody({ question, initialA
|
|
|
111
104
|
: 'border-solid border-[#f97316] bg-amber-50 text-[#c2410c]'
|
|
112
105
|
: '');
|
|
113
106
|
if (!isLetter) {
|
|
114
|
-
input.size = isPhrase ?
|
|
107
|
+
input.size = isPhrase ? 28 : 10;
|
|
115
108
|
}
|
|
116
109
|
// letter inline box — host FE may not scan Tailwind
|
|
117
110
|
if (isLetter) {
|
|
@@ -160,7 +153,7 @@ const StructuredFormBody = memo(function StructuredFormBody({ question, initialA
|
|
|
160
153
|
};
|
|
161
154
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
162
155
|
}, [processedHtml, isReviewMode]);
|
|
163
|
-
return (_jsxs("div", { className: "space-y-3", children: [question.title && (_jsx("h3", { className: "text-base font-semibold text-[#0f1730]", children: question.title })), question.imageUrl && (_jsx("div", { className: "flex justify-center", children: _jsx("div", { className: "overflow-hidden rounded-lg border border-[#d9e4f2]", children: _jsx(ResolvedImage, { src: question.imageUrl, alt: question.title || 'Question image', className: "max-h-56 w-auto object-contain" }) }) })), _jsx("div", { ref: containerRef, className: "ec-structured-content w-full overflow-x-auto rounded-xl border border-[#d9e4f2] bg-[#f4f9ff] p-5 text-base leading-relaxed text-[#0f1730] [&_table]:w-full [&_table]:border-collapse [&_td]:border [&_td]:border-[#d9e4f2] [&_td]:px-4 [&_td]:py-3 [&_td]:align-top [&_th]:border [&_th]:border-[#d9e4f2] [&_th]:bg-[#e9f3ff] [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-semibold" }), isReviewMode && Object.keys(question.answers).length > 0 && (_jsxs("div", { className: "rounded-xl border border-[#d9e4f2] bg-white/70 p-3", children: [_jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wider text-[#64748b]", children: "Correct answers" }), _jsx("div", { className: "mt-1 flex flex-wrap gap-1.5", children: Object.entries(question.answers).map(([key, val]) => (_jsxs("span", { className: "rounded border border-[#b9edc8] bg-[#ddf8e7] px-2 py-0.5 text-xs font-semibold text-[#15803d]", children: [key, ": ", val] }, key))) })] }))] }));
|
|
156
|
+
return (_jsxs("div", { className: "space-y-3", children: [question.title && (_jsx("h3", { className: "text-base font-semibold text-[#0f1730]", children: question.title })), question.imageUrl && (_jsx("div", { className: "flex justify-center", children: _jsx("div", { className: "overflow-hidden rounded-lg border border-[#d9e4f2]", children: _jsx(ResolvedImage, { src: question.imageUrl, alt: question.title || 'Question image', className: "max-h-56 w-auto object-contain" }) }) })), _jsx("div", { ref: containerRef, className: "ec-structured-content w-full overflow-x-auto rounded-xl border border-[#d9e4f2] bg-[#f4f9ff] p-5 text-base leading-relaxed text-[#0f1730] [&_table]:w-full [&_table]:border-collapse [&_td]:border [&_td]:border-[#d9e4f2] [&_td]:px-4 [&_td]:py-3 [&_td]:align-top [&_th]:border [&_th]:border-[#d9e4f2] [&_th]:bg-[#e9f3ff] [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-semibold" }), isReviewMode && Object.keys(question.answers).length > 0 && (_jsxs("div", { className: "rounded-xl border border-[#d9e4f2] bg-white/70 p-3", children: [_jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wider text-[#64748b]", children: "Correct answers" }), _jsx("div", { className: "mt-1 flex flex-wrap gap-1.5", children: Object.entries(question.answers).map(([key, val]) => (_jsxs("span", { className: "rounded border border-[#b9edc8] bg-[#ddf8e7] px-2 py-0.5 text-xs font-semibold text-[#15803d]", children: [key, ": ", formatWfsfCorrectLabel(val)] }, key))) })] }))] }));
|
|
164
157
|
});
|
|
165
158
|
/**
|
|
166
159
|
* Branded English Certification renderer for `WORD_FILL_STRUCTURED_FORM`.
|
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,
|
|
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';
|
|
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,
|
|
19
|
-
export type { EditQuestionBankDialogProps, QuestionBankEditItem,
|
|
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';
|
|
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,7 +24,6 @@ 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';
|
|
28
27
|
export type { QuestionFormState } from './components/questions/_shared/hooks/useQuestionFormState';
|
|
29
28
|
export * from './components/questions/_shared/types';
|
|
30
29
|
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,12 +14,11 @@ 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,
|
|
17
|
+
export { DifficultyLevel, EditQuestionBankDialog, QuestionBankEditorType, 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';
|
|
24
23
|
export * from './components/questions/_shared/types';
|
|
25
24
|
export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, SortWordsIntoCategoriesViewer, ChooseThenAnswerGroupViewer, } from './components/questions/viewer';
|
|
@@ -86,7 +86,6 @@ 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";
|
|
90
89
|
readonly SCHEDULES_SUMMARY: "/api/v1/manage/schedules/summary";
|
|
91
90
|
readonly SCHEDULES_CALENDAR: "/api/v1/manage/schedules/calendar";
|
|
92
91
|
readonly SCHEDULES: "/api/v1/manage/schedules";
|
|
@@ -95,7 +95,6 @@ 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`,
|
|
99
98
|
// Schedules endpoints
|
|
100
99
|
SCHEDULES_SUMMARY: `/api/${API_VERSION}/manage/schedules/summary`,
|
|
101
100
|
SCHEDULES_CALENDAR: `/api/${API_VERSION}/manage/schedules/calendar`,
|
|
@@ -21,8 +21,8 @@ export declare const ROUTES: {
|
|
|
21
21
|
readonly USERS: "/manage/users";
|
|
22
22
|
readonly USERS_CREATE: "/manage/users/create";
|
|
23
23
|
readonly USERS_EDIT: (id: string) => string;
|
|
24
|
-
readonly QUESTION_BANK: "/manage/question-
|
|
25
|
-
readonly QUESTION_CREATE: "/manage/question-
|
|
24
|
+
readonly QUESTION_BANK: "/manage/question-banks";
|
|
25
|
+
readonly QUESTION_CREATE: "/manage/question-banks/create";
|
|
26
26
|
readonly QUESTION_EDIT: (id: string) => string;
|
|
27
27
|
readonly EXAMS: "/manage/exams";
|
|
28
28
|
readonly EXAMS_CREATE: "/manage/exams/create";
|
|
@@ -26,9 +26,9 @@ export const ROUTES = {
|
|
|
26
26
|
USERS: '/manage/users',
|
|
27
27
|
USERS_CREATE: '/manage/users/create',
|
|
28
28
|
USERS_EDIT: (id) => `/manage/users/${id}/edit`,
|
|
29
|
-
QUESTION_BANK: '/manage/question-
|
|
30
|
-
QUESTION_CREATE: '/manage/question-
|
|
31
|
-
QUESTION_EDIT: (id) => `/manage/question-
|
|
29
|
+
QUESTION_BANK: '/manage/question-banks',
|
|
30
|
+
QUESTION_CREATE: '/manage/question-banks/create',
|
|
31
|
+
QUESTION_EDIT: (id) => `/manage/question-banks/${id}/edit`,
|
|
32
32
|
EXAMS: '/manage/exams',
|
|
33
33
|
EXAMS_CREATE: '/manage/exams/create',
|
|
34
34
|
EXAMS_EDIT: (id) => `/manage/exams/${id}/edit`,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type WfsfCorrectValue = string | string[];
|
|
2
|
+
export declare function cleanWfsfWord(str: unknown, caseSensitive?: boolean): string;
|
|
3
|
+
export declare function expandWfsfCorrectAlts(correctVal: unknown): string[];
|
|
4
|
+
export declare function formatWfsfCorrectLabel(correctVal: unknown): string;
|
|
5
|
+
export declare function isWfsfBlankCorrect(userWord: string, correctVal: unknown, caseSensitive?: boolean): boolean;
|
|
6
|
+
export declare function normalizeWfsfAnswersMap(raw: unknown): Record<string, string[]>;
|
|
7
|
+
export declare function compactWfsfAnswersMap(answers: Record<string, string | string[]>): Record<string, string[]>;
|
|
8
|
+
export declare function primaryWfsfAnswers(answers: Record<string, string | string[]>): Record<string, string>;
|
|
9
|
+
export declare function hasWfsfBlankAnswer(value: unknown): boolean;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function cleanWfsfWord(str, caseSensitive = false) {
|
|
2
|
+
const text = str == null ? '' : String(str);
|
|
3
|
+
const cleaned = text
|
|
4
|
+
.trim()
|
|
5
|
+
.replace(/[\p{P}]/gu, '')
|
|
6
|
+
.replace(/\s+/g, ' ')
|
|
7
|
+
.trim();
|
|
8
|
+
return caseSensitive ? cleaned : cleaned.toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
export function expandWfsfCorrectAlts(correctVal) {
|
|
11
|
+
if (Array.isArray(correctVal)) {
|
|
12
|
+
return correctVal.map((v) => String(v ?? '').trim()).filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
if (correctVal == null)
|
|
15
|
+
return [];
|
|
16
|
+
const valStr = String(correctVal);
|
|
17
|
+
if (valStr.includes('/')) {
|
|
18
|
+
return valStr.split('/').map((s) => s.trim()).filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
const trimmed = valStr.trim();
|
|
21
|
+
return trimmed ? [trimmed] : [];
|
|
22
|
+
}
|
|
23
|
+
export function formatWfsfCorrectLabel(correctVal) {
|
|
24
|
+
return expandWfsfCorrectAlts(correctVal).join(' / ');
|
|
25
|
+
}
|
|
26
|
+
export function isWfsfBlankCorrect(userWord, correctVal, caseSensitive = false) {
|
|
27
|
+
const userClean = cleanWfsfWord(userWord, caseSensitive);
|
|
28
|
+
if (!userClean)
|
|
29
|
+
return false;
|
|
30
|
+
return expandWfsfCorrectAlts(correctVal).some((alt) => cleanWfsfWord(alt, caseSensitive) === userClean);
|
|
31
|
+
}
|
|
32
|
+
export function normalizeWfsfAnswersMap(raw) {
|
|
33
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
34
|
+
return {};
|
|
35
|
+
const result = {};
|
|
36
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
37
|
+
const alts = expandWfsfCorrectAlts(value);
|
|
38
|
+
result[key] = alts.length > 0 ? alts : [''];
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
export function compactWfsfAnswersMap(answers) {
|
|
43
|
+
const result = {};
|
|
44
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
45
|
+
const alts = expandWfsfCorrectAlts(value);
|
|
46
|
+
if (alts.length > 0)
|
|
47
|
+
result[key] = alts;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
export function primaryWfsfAnswers(answers) {
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
54
|
+
result[key] = expandWfsfCorrectAlts(value)[0] || '';
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
export function hasWfsfBlankAnswer(value) {
|
|
59
|
+
return expandWfsfCorrectAlts(value).length > 0;
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { QuestionBankFiltersInput, QuestionBankFiltersResult, QuestionSearchItem, SearchQuestionBankInput, SearchQuestionBankResult } from '../../../api/exam-questions.types';
|
|
2
|
-
/**
|
|
3
|
-
* Prefill cascade filters from host context.
|
|
4
|
-
* Values are Pearson meta ids: bookId / unitId / lessonId.
|
|
5
|
-
* `lessonIds` = session textbook lessons (default search scope when lesson select = Tất cả).
|
|
6
|
-
*/
|
|
7
|
-
export type QuestionBankPickerDefaultFilters = {
|
|
8
|
-
bookId?: string;
|
|
9
|
-
unitId?: string;
|
|
10
|
-
lessonId?: string;
|
|
11
|
-
/** Session lesson ids — used when lesson dropdown is "Tất cả". */
|
|
12
|
-
lessonIds?: string[];
|
|
13
|
-
questionType?: string;
|
|
14
|
-
/** Optional labels so prefill renders before cascade options load. */
|
|
15
|
-
bookLabel?: string;
|
|
16
|
-
unitLabel?: string;
|
|
17
|
-
};
|
|
18
|
-
/** Session textbook lesson → merge into Lesson select + default search scope. */
|
|
19
|
-
export type QuestionBankLessonSuggestion = {
|
|
20
|
-
lessonId?: string | null;
|
|
21
|
-
label?: string | null;
|
|
22
|
-
/** @deprecated use lessonId */
|
|
23
|
-
lesson?: string | null;
|
|
24
|
-
book?: string | null;
|
|
25
|
-
unit?: string | null;
|
|
26
|
-
};
|
|
27
|
-
export interface QuestionBankPickerDialogProps {
|
|
28
|
-
open: boolean;
|
|
29
|
-
onOpenChange: (open: boolean) => void;
|
|
30
|
-
searchQuestions: (input: SearchQuestionBankInput) => Promise<SearchQuestionBankResult>;
|
|
31
|
-
getQuestionFilters?: (input?: QuestionBankFiltersInput) => Promise<QuestionBankFiltersResult>;
|
|
32
|
-
defaultFilters?: QuestionBankPickerDefaultFilters;
|
|
33
|
-
/** Session lessons: options + default multi-lesson search scope. */
|
|
34
|
-
lessonSuggestions?: QuestionBankLessonSuggestion[];
|
|
35
|
-
onConfirm: (items: QuestionSearchItem[]) => void;
|
|
36
|
-
title?: string;
|
|
37
|
-
description?: string;
|
|
38
|
-
confirmLabel?: string;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Multi-select bank picker. Cascade filters use stable meta ids:
|
|
42
|
-
* bookId → unitId → lessonId (labels from meta.*Name).
|
|
43
|
-
* Default lesson scope = session lessonIds when dropdown is "Tất cả".
|
|
44
|
-
*/
|
|
45
|
-
export declare function QuestionBankPickerDialog({ open, onOpenChange, searchQuestions, getQuestionFilters, defaultFilters, lessonSuggestions, onConfirm, title, description, confirmLabel, }: QuestionBankPickerDialogProps): import("react").JSX.Element;
|