@tinyweb_dev/oe-exam-sdk 0.1.30 → 0.1.32
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/ExamPreviewDialog.d.ts +9 -3
- package/dist/components/exams/ExamPreviewDialog.js +23 -5
- package/dist/components/exams/ExamPreviewPartContent.d.ts +2 -1
- package/dist/components/exams/ExamPreviewPartContent.js +3 -2
- package/dist/components/exams/ExamPreviewSidebar.d.ts +3 -1
- package/dist/components/exams/ExamPreviewSidebar.js +7 -2
- package/dist/components/exams/exam-preview.mode.d.ts +4 -0
- package/dist/components/exams/exam-preview.mode.js +5 -0
- package/dist/components/exams/index.d.ts +1 -0
- package/dist/components/exams/index.js +1 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +10 -2
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.d.ts +3 -0
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +285 -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/types.d.ts +30 -0
- package/dist/components/exams/take/utils/answer-transformers.js +37 -6
- package/dist/components/exams/take/utils/question-transformers.d.ts +28 -1
- package/dist/components/exams/take/utils/question-transformers.js +69 -0
- package/dist/components/questions/_shared/config/question-types.config.js +12 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +32 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.js +1 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +16 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.js +1 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -1
- package/dist/components/questions/creator/question-type-registry.js +4 -0
- package/dist/components/questions/question-bank/QuestionEditorRenderer.d.ts +2 -1
- package/dist/components/questions/question-bank/QuestionEditorRenderer.js +16 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +183 -0
- package/dist/components/questions/types/answer-the-question-group/index.d.ts +5 -0
- package/dist/components/questions/types/answer-the-question-group/index.js +5 -0
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.d.ts +4 -0
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +83 -0
- package/dist/components/questions/types/answer-the-question-group/register.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question-group/register.js +36 -0
- package/dist/components/questions/types/answer-the-question-group/transform.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question-group/transform.js +72 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +11 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +69 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.d.ts +8 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +213 -0
- package/dist/components/questions/types/crossword-puzzle/index.d.ts +4 -0
- package/dist/components/questions/types/crossword-puzzle/index.js +4 -0
- package/dist/components/questions/types/crossword-puzzle/register.d.ts +2 -0
- package/dist/components/questions/types/crossword-puzzle/register.js +29 -0
- package/dist/components/questions/types/crossword-puzzle/transform.d.ts +2 -0
- package/dist/components/questions/types/crossword-puzzle/transform.js +75 -0
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -1
- package/dist/components/questions/viewer/QuestionViewer.js +9 -6
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +4 -4
- package/dist/components/results/ReviewQuestionRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +19 -0
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +27 -0
- package/dist/components/results/renderers/review-question-body-dedicated.d.ts +1 -0
- package/dist/components/results/renderers/review-question-body-dedicated.js +47 -2
- package/dist/components/results/renderers/review-question-body-movers.js +7 -2
- package/dist/components/results/review-data.js +20 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +4 -0
- package/dist/shared/lib/i18n/messages/en/admin.d.ts +5 -0
- package/dist/shared/lib/i18n/messages/en/admin.js +5 -0
- package/dist/shared/lib/i18n/messages/en.d.ts +5 -0
- package/dist/shared/lib/i18n/messages/vi/admin.d.ts +5 -0
- package/dist/shared/lib/i18n/messages/vi/admin.js +5 -0
- package/dist/shared/lib/i18n/messages/vi.d.ts +5 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +93 -0
- package/dist/shared/lib/utils/question-transform-validation.js +12 -0
- package/dist/shared/lib/utils/question-transform.js +4 -0
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/answer-the-question-group.d.ts +39 -0
- package/dist/shared/types/questions/answer-the-question-group.js +1 -0
- package/dist/shared/types/questions/crossword-puzzle.d.ts +50 -0
- package/dist/shared/types/questions/crossword-puzzle.js +10 -0
- package/dist/shared/types/questions/index.d.ts +2 -0
- package/dist/shared/types/questions/index.js +4 -0
- package/package.json +1 -1
package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js
CHANGED
|
@@ -23,7 +23,7 @@ function PreviewImageItem({ url, label }) {
|
|
|
23
23
|
// eslint-disable-next-line @next/next/no-img-element
|
|
24
24
|
_jsx("img", { src: previewUrl, alt: label || 'Image', className: "w-full h-full object-cover" })) : (_jsx("div", { className: "w-full h-full flex items-center justify-center text-gray-300 text-xs", children: "\u0110ang t\u1EA3i..." })) }), label && _jsx("span", { className: "text-xs text-center text-gray-600 font-medium", children: label })] }));
|
|
25
25
|
}
|
|
26
|
-
export function SpeakingConversationPreviewClient({ questionData, isReviewMode =
|
|
26
|
+
export function SpeakingConversationPreviewClient({ questionData, isReviewMode = false, }) {
|
|
27
27
|
const isPartnerQuestion = questionData.responder === 'PARTNER';
|
|
28
28
|
const hasImages = questionData.images && questionData.images.length > 0;
|
|
29
29
|
const gridCols = !hasImages || questionData.images.length <= 2
|
|
@@ -19,6 +19,7 @@ import { LookPictureChooseCorrectAnswerClient } from '../types/look-picture-choo
|
|
|
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
21
|
import { WordFillStructuredFormClient } from '../types/word-fill-structured-form/WordFillStructuredFormClient';
|
|
22
|
+
import { CrosswordPuzzleClient } from '../types/crossword-puzzle/CrosswordPuzzleClient';
|
|
22
23
|
import { parseFillBlankStemLine } from '../_shared/utils/parseFillBlankStem';
|
|
23
24
|
import { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
|
|
24
25
|
import { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
|
|
@@ -343,7 +344,7 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
343
344
|
regionNodes: answer?.regionNodes || [],
|
|
344
345
|
instructions: answer?.instructions || [],
|
|
345
346
|
};
|
|
346
|
-
return (_jsx(ReadAndColorObjectsClient, { questionData: questionData, onSubmit: (userConnections) => {
|
|
347
|
+
return (_jsx(ReadAndColorObjectsClient, { questionData: questionData, isReviewMode: isReviewMode, onSubmit: (userConnections) => {
|
|
347
348
|
setSelectedAnswer(userConnections);
|
|
348
349
|
onSubmitAnswer?.(userConnections);
|
|
349
350
|
} }));
|
|
@@ -600,7 +601,7 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
600
601
|
case 'CROSS_OUT_WORD_GROUP':
|
|
601
602
|
return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode }));
|
|
602
603
|
case 'SPONTANEOUS_QA_GROUP':
|
|
603
|
-
return _jsx(SpontaneousQaGroupViewer, { question: question });
|
|
604
|
+
return (_jsx(SpontaneousQaGroupViewer, { question: question, isReviewMode: isReviewMode }));
|
|
604
605
|
case 'WORD_ORDER_AND_MATCH_GROUP':
|
|
605
606
|
case 'WORD_ORDER_AND_MATCH':
|
|
606
607
|
return (_jsx(WordOrderAndMatchGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
|
|
@@ -643,6 +644,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
643
644
|
return renderWordFillParagraph();
|
|
644
645
|
case 'WORD_FILL_STRUCTURED_FORM':
|
|
645
646
|
return (_jsx(WordFillStructuredFormViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
647
|
+
case 'CROSSWORD_PUZZLE':
|
|
648
|
+
return (_jsx(CrosswordPuzzleClient, { content: question.content, correctAnswer: question.answer, isReviewMode: isReviewMode, userAnswers: userAnswer }));
|
|
646
649
|
default:
|
|
647
650
|
return _jsxs("div", { children: ["Lo\u1EA1i c\u00E2u h\u1ECFi kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3: ", question.type] });
|
|
648
651
|
}
|
|
@@ -653,8 +656,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
653
656
|
* parseFillBlankStemLine (or paragraph renderer). Dumping raw text shows markdown.
|
|
654
657
|
*/
|
|
655
658
|
const resolveCardTitle = () => {
|
|
656
|
-
// WFSF
|
|
657
|
-
if (question.type === 'WORD_FILL_STRUCTURED_FORM') {
|
|
659
|
+
// WFSF and Crossword show title/instruction inside the client — no outer card label.
|
|
660
|
+
if (question.type === 'WORD_FILL_STRUCTURED_FORM' || question.type === 'CROSSWORD_PUZZLE') {
|
|
658
661
|
return null;
|
|
659
662
|
}
|
|
660
663
|
// Modern fill stems live entirely in the body; title is a short label.
|
|
@@ -676,8 +679,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
676
679
|
return stripLightweightMarkdown(raw) || 'Câu hỏi';
|
|
677
680
|
};
|
|
678
681
|
const cardTitle = resolveCardTitle();
|
|
679
|
-
// WFSF already
|
|
680
|
-
if (question.type === 'WORD_FILL_STRUCTURED_FORM') {
|
|
682
|
+
// WFSF and Crossword already have their own bordered shell — avoid double Card border.
|
|
683
|
+
if (question.type === 'WORD_FILL_STRUCTURED_FORM' || question.type === 'CROSSWORD_PUZZLE') {
|
|
681
684
|
return (_jsx("div", { className: "space-y-4", children: renderQuestion() }));
|
|
682
685
|
}
|
|
683
686
|
return (_jsxs(Card, { children: [cardTitle ? (_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center justify-between", children: [_jsx("span", { className: "min-w-0 pr-2 text-base font-semibold leading-snug", children: cardTitle }), _jsxs("span", { className: "shrink-0 text-sm font-normal text-gray-500", children: [question.points, " \u0111i\u1EC3m"] })] }) })) : null, _jsx(CardContent, { className: cardTitle ? 'space-y-4' : 'space-y-4 pt-4', children: renderQuestion() })] }));
|
|
@@ -72,11 +72,11 @@ function resolveGroup(question) {
|
|
|
72
72
|
],
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
-
function QaPromptCard({ item, index }) {
|
|
76
|
-
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gray-50 px-4 py-2.5", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || index + 1] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-2 p-4", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: item.questionText || 'Câu hỏi chưa được nhập' }), item.audioUrl ? (_jsxs("p", { className: "text-xs text-gray-500", children: ["Audio: ", item.audioUrl] })) : null, item.expectedAnswers.length > 0 ? (_jsxs("div", { className: "rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-2 text-sm text-emerald-800", children: [_jsx("p", { className: "mb-1 font-medium", children: "G\u1EE3i \u00FD \u0111\u00E1p \u00E1n" }), _jsx("ul", { className: "list-disc space-y-1 pl-4", children: item.expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })) : null] })] }));
|
|
75
|
+
function QaPromptCard({ item, index, isReviewMode = false, }) {
|
|
76
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gray-50 px-4 py-2.5", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || index + 1] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-2 p-4", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: item.questionText || 'Câu hỏi chưa được nhập' }), item.audioUrl ? (_jsxs("p", { className: "text-xs text-gray-500", children: ["Audio: ", item.audioUrl] })) : null, isReviewMode && item.expectedAnswers.length > 0 ? (_jsxs("div", { className: "rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-2 text-sm text-emerald-800", children: [_jsx("p", { className: "mb-1 font-medium", children: "G\u1EE3i \u00FD \u0111\u00E1p \u00E1n" }), _jsx("ul", { className: "list-disc space-y-1 pl-4", children: item.expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })) : null] })] }));
|
|
77
77
|
}
|
|
78
|
-
export function SpontaneousQaGroupViewer({ question, isReviewMode, }) {
|
|
78
|
+
export function SpontaneousQaGroupViewer({ question, isReviewMode = false, }) {
|
|
79
79
|
const { instruction, parts } = resolveGroup(question);
|
|
80
80
|
const showPartChrome = parts.length > 1 || parts.some((part) => part.partTitle || part.topic);
|
|
81
|
-
return (_jsxs("div", { className: "space-y-5", children: [instruction ? (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: instruction })) : null, parts.map((part) => (_jsxs("section", { className: "space-y-3", children: [showPartChrome ? (_jsxs("div", { children: [part.partTitle ? (_jsx("h3", { className: "text-base font-bold tracking-wide text-sky-600", children: part.partTitle })) : null, part.topic ? (_jsx("p", { className: "text-sm font-semibold text-slate-600", children: part.topic })) : null] })) : null, _jsx("div", { className: "space-y-3", children: part.questions.map((item, index) => (_jsx(QaPromptCard, { item: item, index: index }, `${part.partNumber}-${item.questionNumber}-${index}`))) })] }, `part-${part.partNumber}`)))] }));
|
|
81
|
+
return (_jsxs("div", { className: "space-y-5", children: [instruction ? (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: instruction })) : null, parts.map((part) => (_jsxs("section", { className: "space-y-3", children: [showPartChrome ? (_jsxs("div", { children: [part.partTitle ? (_jsx("h3", { className: "text-base font-bold tracking-wide text-sky-600", children: part.partTitle })) : null, part.topic ? (_jsx("p", { className: "text-sm font-semibold text-slate-600", children: part.topic })) : null] })) : null, _jsx("div", { className: "space-y-3", children: part.questions.map((item, index) => (_jsx(QaPromptCard, { item: item, index: index, isReviewMode: isReviewMode }, `${part.partNumber}-${item.questionNumber}-${index}`))) })] }, `part-${part.partNumber}`)))] }));
|
|
82
82
|
}
|
|
@@ -34,6 +34,7 @@ function renderQuestionBody({ part, reviewData, hidePerQuestionGrading = false,
|
|
|
34
34
|
correctAnswerTextMap: reviewData.correctAnswerTextMap,
|
|
35
35
|
correctOptionIdMap: reviewData.correctOptionIdMap,
|
|
36
36
|
rawCorrectAnswerMap: reviewData.rawCorrectAnswerMap,
|
|
37
|
+
rawStudentAnswerMap: reviewData.rawStudentAnswerMap,
|
|
37
38
|
questionScoreMap: reviewData.questionScoreMap,
|
|
38
39
|
hidePerQuestionGrading,
|
|
39
40
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AnswerTheQuestionQuestion } from '../../../components/exams/take/utils/question-transformers';
|
|
2
|
+
export interface ReviewAnswerTheQuestionGroupItemMeta {
|
|
3
|
+
expectedAnswers: string[];
|
|
4
|
+
isCorrect: boolean | null;
|
|
5
|
+
feedbackEn?: string;
|
|
6
|
+
feedbackVi?: string;
|
|
7
|
+
}
|
|
8
|
+
interface ReviewAnswerTheQuestionGroupRendererProps {
|
|
9
|
+
partNumber: number;
|
|
10
|
+
partName?: string;
|
|
11
|
+
questionCount: number;
|
|
12
|
+
instruction: string;
|
|
13
|
+
groupContent?: string;
|
|
14
|
+
questions: AnswerTheQuestionQuestion[];
|
|
15
|
+
answers: Record<string, string>;
|
|
16
|
+
itemMeta: Record<string, ReviewAnswerTheQuestionGroupItemMeta>;
|
|
17
|
+
}
|
|
18
|
+
export declare function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, questions, answers, itemMeta, }: ReviewAnswerTheQuestionGroupRendererProps): import("react").JSX.Element;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { BookOpen, Check, Sparkles, Star, X } from 'lucide-react';
|
|
4
|
+
import CambridgeYlePartBanner from '../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
5
|
+
import CambridgeYleInstructionBanner from '../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
6
|
+
export function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, questions, answers, itemMeta, }) {
|
|
7
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), groupContent && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: groupContent } })] })), questions.map((question) => {
|
|
8
|
+
const meta = itemMeta[question.id];
|
|
9
|
+
const studentAnswer = answers[question.id] || '';
|
|
10
|
+
const expectedAnswers = meta?.expectedAnswers || [];
|
|
11
|
+
const isCorrect = question.isExample ? true : meta?.isCorrect;
|
|
12
|
+
const feedback = meta?.feedbackVi || meta?.feedbackEn;
|
|
13
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: `flex h-7 w-7 items-center justify-center rounded-full text-sm font-bold text-white ${question.isExample
|
|
14
|
+
? 'bg-orange-400'
|
|
15
|
+
: isCorrect === true
|
|
16
|
+
? 'bg-green-500'
|
|
17
|
+
: isCorrect === false
|
|
18
|
+
? 'bg-red-500'
|
|
19
|
+
: 'bg-blue-500'}`, children: question.questionNumber }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", question.questionNumber] }), question.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] })), _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] })] }), isCorrect === true && _jsx(Check, { className: "h-4 w-4 text-green-600" }), isCorrect === false && _jsx(X, { className: "h-4 w-4 text-red-600" })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: question.question }), _jsx("div", { className: `rounded-lg border px-3 py-2 text-sm ${isCorrect === true
|
|
20
|
+
? 'border-green-200 bg-green-50 text-green-800'
|
|
21
|
+
: isCorrect === false
|
|
22
|
+
? 'border-red-200 bg-red-50 text-red-800'
|
|
23
|
+
: 'border-gray-200 bg-gray-50 text-gray-700'}`, children: question.isExample
|
|
24
|
+
? (question.exampleAnswer || '(Câu mẫu)')
|
|
25
|
+
: (studentAnswer || '(Chưa trả lời)') }), expectedAnswers.length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n m\u1EABu (AI)" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })), feedback && (_jsx("p", { className: "text-xs text-violet-700", children: feedback }))] })] }, question.id));
|
|
26
|
+
})] }));
|
|
27
|
+
}
|
|
@@ -6,6 +6,7 @@ export interface DedicatedBodyContext {
|
|
|
6
6
|
correctAnswerTextMap: Record<string, string>;
|
|
7
7
|
correctOptionIdMap: Record<string, string>;
|
|
8
8
|
rawCorrectAnswerMap: Record<string, unknown>;
|
|
9
|
+
rawStudentAnswerMap: Record<string, unknown>;
|
|
9
10
|
questionScoreMap: Record<string, {
|
|
10
11
|
score: number | null;
|
|
11
12
|
maxScore: number;
|
|
@@ -4,8 +4,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
4
4
|
* Dedicated review renderer cases for ReviewQuestionRenderer.
|
|
5
5
|
* Source: oe-exam-fe ReviewQuestionRenderer.tsx
|
|
6
6
|
*/
|
|
7
|
-
import { transformChooseCorrectAdjective, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformColorObjects, transformFillInBlank, transformWriteCorrectVerbForm, } from '../../../components/exams/take/utils/question-transformers';
|
|
7
|
+
import { transformAnswerTheQuestionGroup, transformChooseCorrectAdjective, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformColorObjects, transformFillInBlank, transformWriteCorrectVerbForm, } from '../../../components/exams/take/utils/question-transformers';
|
|
8
8
|
import { normalizeColoringAssignments } from '../../../components/exams/take/utils/coloring-assignments';
|
|
9
|
+
import { ReviewAnswerTheQuestionGroupRenderer } from './ReviewAnswerTheQuestionGroupRenderer';
|
|
9
10
|
import { ReviewChooseAdjectiveRenderer } from './ReviewChooseAdjectiveRenderer';
|
|
10
11
|
import { ReviewChooseBestAnswerRenderer } from './ReviewChooseBestAnswerRenderer';
|
|
11
12
|
import { ReviewColoringRenderer } from './ReviewColoringRenderer';
|
|
@@ -20,7 +21,7 @@ function asApiQuestions(questions) {
|
|
|
20
21
|
return questions;
|
|
21
22
|
}
|
|
22
23
|
export function renderDedicatedReviewBody(ctx) {
|
|
23
|
-
const { part, answers, isCorrectMap, correctAnswerTextMap, correctOptionIdMap, rawCorrectAnswerMap, questionScoreMap, hidePerQuestionGrading, } = ctx;
|
|
24
|
+
const { part, answers, isCorrectMap, correctAnswerTextMap, correctOptionIdMap, rawCorrectAnswerMap, rawStudentAnswerMap, questionScoreMap, hidePerQuestionGrading, } = ctx;
|
|
24
25
|
const { partNo, questionType, questions } = part;
|
|
25
26
|
const questionCount = questions.length;
|
|
26
27
|
const apiQuestions = asApiQuestions(questions);
|
|
@@ -72,6 +73,50 @@ export function renderDedicatedReviewBody(ctx) {
|
|
|
72
73
|
case 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE': {
|
|
73
74
|
return (_jsx(ReviewListenDragObjectsRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, questions: questions, studentAnswers: answers, rawCorrectAnswerMap: rawCorrectAnswerMap, isCorrectMap: isCorrectMap, questionScoreMap: questionScoreMap }));
|
|
74
75
|
}
|
|
76
|
+
case 'ANSWER_THE_QUESTION_GROUP': {
|
|
77
|
+
const parent = questions[0];
|
|
78
|
+
if (!parent) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const data = transformAnswerTheQuestionGroup(apiQuestions);
|
|
82
|
+
const parentId = parent.id || '';
|
|
83
|
+
const parentContent = asRecord(parent.content);
|
|
84
|
+
const items = Array.isArray(parentContent.items) ? parentContent.items : [];
|
|
85
|
+
const parentCorrect = asRecord(rawCorrectAnswerMap[parentId]);
|
|
86
|
+
const parentCorrectAnswers = Array.isArray(parentCorrect.answer)
|
|
87
|
+
? parentCorrect.answer
|
|
88
|
+
: [];
|
|
89
|
+
const rawStudent = asRecord(rawStudentAnswerMap[parentId]);
|
|
90
|
+
const llmAssessment = asRecord(rawStudent.llmAssessment);
|
|
91
|
+
const llmItems = Array.isArray(llmAssessment.items) ? llmAssessment.items : [];
|
|
92
|
+
const llmByKey = new Map(llmItems
|
|
93
|
+
.map((item) => asRecord(item))
|
|
94
|
+
.filter((item) => typeof item.key === 'string')
|
|
95
|
+
.map((item) => [String(item.key), item]));
|
|
96
|
+
const itemMeta = Object.fromEntries(items.map((rawItem, index) => {
|
|
97
|
+
const item = asRecord(rawItem);
|
|
98
|
+
const itemCorrect = asRecord(item.correctAnswer);
|
|
99
|
+
const itemId = `${parentId}-item-${index}`;
|
|
100
|
+
const nested = Array.isArray(itemCorrect.expectedAnswers)
|
|
101
|
+
? itemCorrect.expectedAnswers.filter((value) => typeof value === 'string' && value.trim() !== '')
|
|
102
|
+
: [];
|
|
103
|
+
const fallbackRaw = asRecord(parentCorrectAnswers[index]);
|
|
104
|
+
const fallback = Array.isArray(fallbackRaw.expectedAnswers)
|
|
105
|
+
? fallbackRaw.expectedAnswers.filter((value) => typeof value === 'string' && value.trim() !== '')
|
|
106
|
+
: [];
|
|
107
|
+
const llmItem = asRecord(llmByKey.get(String(index)));
|
|
108
|
+
const feedback = asRecord(llmItem.feedback);
|
|
109
|
+
return [itemId, {
|
|
110
|
+
expectedAnswers: nested.length > 0 ? nested : fallback,
|
|
111
|
+
isCorrect: item.isExample === true
|
|
112
|
+
? true
|
|
113
|
+
: (typeof llmItem.isCorrect === 'boolean' ? llmItem.isCorrect : null),
|
|
114
|
+
feedbackEn: typeof feedback.en === 'string' ? feedback.en : undefined,
|
|
115
|
+
feedbackVi: typeof feedback.vi === 'string' ? feedback.vi : undefined,
|
|
116
|
+
}];
|
|
117
|
+
}));
|
|
118
|
+
return (_jsx(ReviewAnswerTheQuestionGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, groupContent: data.groupContent, questions: data.questions, answers: answers, itemMeta: itemMeta }));
|
|
119
|
+
}
|
|
75
120
|
case 'SPONTANEOUS_QA_GROUP': {
|
|
76
121
|
const parent = questions[0];
|
|
77
122
|
if (!parent) {
|
|
@@ -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, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, } from '../../../components/exams/take/components/question-renderers';
|
|
8
|
-
import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWriteSentences, transformWriteShortLetter, } from '../../../components/exams/take/utils/question-transformers';
|
|
7
|
+
import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, } from '../../../components/exams/take/components/question-renderers';
|
|
8
|
+
import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, 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;
|
|
@@ -142,6 +142,11 @@ export function renderMoversReviewBody(ctx) {
|
|
|
142
142
|
const data = transformWordFillStructuredForm(apiQuestions);
|
|
143
143
|
return (_jsx(MoversWordFillStructuredFormRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, audioUrl: data.audioUrl || part.audioUrl, groupTitle: data.groupTitle, groupContent: data.groupContent, groupImageUrl: data.groupImageUrl, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true }));
|
|
144
144
|
}
|
|
145
|
+
case 'CROSSWORD_PUZZLE': {
|
|
146
|
+
const data = transformCrosswordPuzzle(apiQuestions);
|
|
147
|
+
const firstQuestion = apiQuestions[0];
|
|
148
|
+
return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, title: data.title, gridSize: data.gridSize, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: handleAnswerChange, isReviewMode: true, caseSensitive: firstQuestion?.correct_answer?.caseSensitive || false }));
|
|
149
|
+
}
|
|
145
150
|
default:
|
|
146
151
|
return null;
|
|
147
152
|
}
|
|
@@ -6,6 +6,7 @@ const FLATTENED_ANSWER_TYPES = new Set([
|
|
|
6
6
|
'READ_AND_COLOR_OBJECTS',
|
|
7
7
|
'IMAGE_OBJECT_MATCHING',
|
|
8
8
|
'CHOOSE_THE_CORRECT_ANSWER_GROUP',
|
|
9
|
+
'ANSWER_THE_QUESTION_GROUP',
|
|
9
10
|
'CROSS_OUT_WORD_GROUP',
|
|
10
11
|
]);
|
|
11
12
|
export function transformResultAnswersForReview(answers, templateParts = []) {
|
|
@@ -136,8 +137,12 @@ function addCorrectAnswerMaps(result, answer) {
|
|
|
136
137
|
texts.forEach((text, index) => { result.correctAnswerTextMap[`${answer.questionId}-blank-${index}`] = text; });
|
|
137
138
|
if (answer.questionType === 'LABEL_THE_PICTURE' && isRecord(correct.labels))
|
|
138
139
|
Object.entries(correct.labels).forEach(([id, value]) => { result.correctAnswerTextMap[id] = String(value); });
|
|
139
|
-
if (answer.questionType === 'FILL_MISSING_WORDS_IN_GRID' && isRecord(correct.cells)) {
|
|
140
|
-
|
|
140
|
+
if ((answer.questionType === 'FILL_MISSING_WORDS_IN_GRID' || answer.questionType === 'CROSSWORD_PUZZLE') && isRecord(correct.cells || correct.answers)) {
|
|
141
|
+
const rawCells = (correct.cells || correct.answers);
|
|
142
|
+
result.cellAnswersMap[answer.questionId] = {
|
|
143
|
+
cells: Object.fromEntries(Object.entries(rawCells).map(([k, v]) => [k, { correctAnswer: String(v) }])),
|
|
144
|
+
caseSensitive: correct.caseSensitive === true,
|
|
145
|
+
};
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
function transformStudentAnswer(questionId, questionType, value) {
|
|
@@ -156,6 +161,17 @@ function transformStudentAnswer(questionId, questionType, value) {
|
|
|
156
161
|
typeof item === 'string' ? item : '',
|
|
157
162
|
]));
|
|
158
163
|
}
|
|
164
|
+
if (questionType === 'ANSWER_THE_QUESTION_GROUP') {
|
|
165
|
+
const answer = Array.isArray(record.answer)
|
|
166
|
+
? record.answer
|
|
167
|
+
: Array.isArray(value)
|
|
168
|
+
? value
|
|
169
|
+
: [];
|
|
170
|
+
return Object.fromEntries(answer.map((item, index) => [
|
|
171
|
+
`${questionId}-item-${index}`,
|
|
172
|
+
typeof item === 'string' ? item : '',
|
|
173
|
+
]));
|
|
174
|
+
}
|
|
159
175
|
if (questionType === 'CROSS_OUT_WORD_GROUP') {
|
|
160
176
|
const answer = Array.isArray(record.answer)
|
|
161
177
|
? record.answer
|
|
@@ -192,8 +208,8 @@ function transformStudentAnswer(questionId, questionType, value) {
|
|
|
192
208
|
const keys = Object.keys(record).filter((key) => /^\d+$/.test(key)).sort((a, b) => Number(a) - Number(b));
|
|
193
209
|
return keys.map((key) => record[key]);
|
|
194
210
|
}
|
|
195
|
-
if (questionType === 'READ_AND_COLOR_OBJECTS' || questionType === 'IMAGE_OBJECT_MATCHING')
|
|
196
|
-
return record.answers ?? value;
|
|
211
|
+
if (questionType === 'READ_AND_COLOR_OBJECTS' || questionType === 'IMAGE_OBJECT_MATCHING' || questionType === 'CROSSWORD_PUZZLE')
|
|
212
|
+
return record.cells ?? record.answers ?? value;
|
|
197
213
|
if (questionType === 'WRITE_A_SHORT_LETTER')
|
|
198
214
|
return typeof value === 'string' ? value : record.answer ?? '';
|
|
199
215
|
return record.answer ?? value;
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ 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
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
|
-
export { CreateExamPageContainer, ExamPreviewDialog, ExamQuestionsPageContainer, ExamTakingPageContainer, ExamViewDialog, StudentContestRoomsPageContainer, StudentExamEntryPageContainer } from './components/exams';
|
|
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';
|
|
13
13
|
export type { ResultReviewPageContainerProps } from './components/results';
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export * as Results from './components/results';
|
|
|
10
10
|
export * as Themes from './components/themes';
|
|
11
11
|
export * as Shared from './shared';
|
|
12
12
|
export { buildExamTemplatePayload, createContestRoomsApi, createExamCreateApi, createExamEntryApi, createExamQuestionsApi, createExamQuestionsFetchApi, createExamTakingApi, createMockContestRoomsApi, createMockCreateExamApi, createMockExamEntryApi, createMockExamQuestionsApi, createMockExamTakingApi, createMockResultReviewApi, createResultReviewApi } from './api';
|
|
13
|
-
export { CreateExamPageContainer, ExamPreviewDialog, ExamQuestionsPageContainer, ExamTakingPageContainer, ExamViewDialog, StudentContestRoomsPageContainer, StudentExamEntryPageContainer } from './components/exams';
|
|
13
|
+
export { CreateExamPageContainer, ExamPreviewDialog, ExamPreviewMode, ExamQuestionsPageContainer, ExamTakingPageContainer, ExamViewDialog, StudentContestRoomsPageContainer, StudentExamEntryPageContainer } from './components/exams';
|
|
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';
|
|
@@ -48,6 +48,7 @@ export const QUESTION_TYPE_SKILLS = {
|
|
|
48
48
|
'FILL_MISSING_WORDS_IN_GRID': SkillEnum.READING,
|
|
49
49
|
'READ_PASSAGE_AND_ANSWER_QUESTIONS': SkillEnum.READING,
|
|
50
50
|
'ANSWER_THE_QUESTION': SkillEnum.READING,
|
|
51
|
+
'ANSWER_THE_QUESTION_GROUP': SkillEnum.READING,
|
|
51
52
|
'WORD_ORDERING': SkillEnum.READING,
|
|
52
53
|
'WORD_FILL_PARAGRAPH': SkillEnum.READING,
|
|
53
54
|
'MATCH_BY_WRITING_ANSWER': SkillEnum.READING,
|
|
@@ -77,6 +78,7 @@ export const QUESTION_TYPE_SKILLS = {
|
|
|
77
78
|
'READING_PASSAGE_FILL': SkillEnum.READING,
|
|
78
79
|
'FILL_BLANK': SkillEnum.READING,
|
|
79
80
|
'WORD_FILL_STRUCTURED_FORM': SkillEnum.READING,
|
|
81
|
+
'CROSSWORD_PUZZLE': SkillEnum.READING,
|
|
80
82
|
// ========================================
|
|
81
83
|
// WRITING SKILLS (Viết)
|
|
82
84
|
// ========================================
|
|
@@ -198,10 +200,12 @@ export const QUESTION_TYPE_LABELS = {
|
|
|
198
200
|
'READING_PASSAGE_FILL': 'Đọc - Điền đoạn văn',
|
|
199
201
|
'FILL_BLANK': 'Điền chỗ trống',
|
|
200
202
|
'ANSWER_THE_QUESTION': 'Nghe - Đọc câu hỏi và viết đáp án',
|
|
203
|
+
'ANSWER_THE_QUESTION_GROUP': 'Đọc và trả lời câu hỏi (AI)',
|
|
201
204
|
'WORD_ORDERING': 'Sắp xếp từ',
|
|
202
205
|
'WORD_FILL_PARAGRAPH': 'Điền từ vào đoạn văn',
|
|
203
206
|
'MATCH_BY_WRITING_ANSWER': 'Ghép đáp án bằng cách viết',
|
|
204
207
|
'WORD_FILL_STRUCTURED_FORM': 'Điền từ vào biểu mẫu có cấu trúc',
|
|
208
|
+
'CROSSWORD_PUZZLE': 'Giải ô chữ (Crossword)',
|
|
205
209
|
// ========================================
|
|
206
210
|
// WRITING SKILLS (Viết)
|
|
207
211
|
// ========================================
|
|
@@ -1477,6 +1477,9 @@ declare const admin: {
|
|
|
1477
1477
|
readonly 'admin.exams.template.loading': "Loading exam information...";
|
|
1478
1478
|
readonly 'admin.exams.template.loadingFallback': "Loading...";
|
|
1479
1479
|
readonly 'admin.exams.preview.title': "Preview Exam: {name}";
|
|
1480
|
+
readonly 'admin.exams.preview.mode.label': "Preview mode:";
|
|
1481
|
+
readonly 'admin.exams.preview.mode.student': "Student View";
|
|
1482
|
+
readonly 'admin.exams.preview.mode.answers': "Answer Key";
|
|
1480
1483
|
readonly 'admin.exams.preview.examInfo': "Exam Information";
|
|
1481
1484
|
readonly 'admin.exams.preview.duration': "Duration:";
|
|
1482
1485
|
readonly 'admin.exams.preview.questionCount': "Questions:";
|
|
@@ -1503,6 +1506,8 @@ declare const admin: {
|
|
|
1503
1506
|
readonly 'admin.exams.preview.next': "Next";
|
|
1504
1507
|
readonly 'admin.exams.preview.questionLabel': "Question {number}";
|
|
1505
1508
|
readonly 'admin.exams.preview.footerNote': "This is a preview mode to check the layout and content of the exam.";
|
|
1509
|
+
readonly 'admin.exams.preview.footerNoteStudent': "Preview mode: Student test taking (You can try answering questions).";
|
|
1510
|
+
readonly 'admin.exams.preview.footerNoteAnswers': "Preview mode: Showing correct answers and explanations.";
|
|
1506
1511
|
readonly 'admin.exams.preview.close': "Close";
|
|
1507
1512
|
readonly 'admin.exams.preview.loadError': "Unable to load questions. Please try again.";
|
|
1508
1513
|
readonly 'admin.exams.preview.points': "{count} pts";
|
|
@@ -1624,6 +1624,9 @@ const admin = {
|
|
|
1624
1624
|
'admin.exams.template.loadingFallback': 'Loading...',
|
|
1625
1625
|
// Preview dialog
|
|
1626
1626
|
'admin.exams.preview.title': 'Preview Exam: {name}',
|
|
1627
|
+
'admin.exams.preview.mode.label': 'Preview mode:',
|
|
1628
|
+
'admin.exams.preview.mode.student': 'Student View',
|
|
1629
|
+
'admin.exams.preview.mode.answers': 'Answer Key',
|
|
1627
1630
|
'admin.exams.preview.examInfo': 'Exam Information',
|
|
1628
1631
|
'admin.exams.preview.duration': 'Duration:',
|
|
1629
1632
|
'admin.exams.preview.questionCount': 'Questions:',
|
|
@@ -1650,6 +1653,8 @@ const admin = {
|
|
|
1650
1653
|
'admin.exams.preview.next': 'Next',
|
|
1651
1654
|
'admin.exams.preview.questionLabel': 'Question {number}',
|
|
1652
1655
|
'admin.exams.preview.footerNote': 'This is a preview mode to check the layout and content of the exam.',
|
|
1656
|
+
'admin.exams.preview.footerNoteStudent': 'Preview mode: Student test taking (You can try answering questions).',
|
|
1657
|
+
'admin.exams.preview.footerNoteAnswers': 'Preview mode: Showing correct answers and explanations.',
|
|
1653
1658
|
'admin.exams.preview.close': 'Close',
|
|
1654
1659
|
'admin.exams.preview.loadError': 'Unable to load questions. Please try again.',
|
|
1655
1660
|
'admin.exams.preview.points': '{count} pts',
|
|
@@ -1634,6 +1634,9 @@ declare const en: {
|
|
|
1634
1634
|
readonly 'admin.exams.template.loading': "Loading exam information...";
|
|
1635
1635
|
readonly 'admin.exams.template.loadingFallback': "Loading...";
|
|
1636
1636
|
readonly 'admin.exams.preview.title': "Preview Exam: {name}";
|
|
1637
|
+
readonly 'admin.exams.preview.mode.label': "Preview mode:";
|
|
1638
|
+
readonly 'admin.exams.preview.mode.student': "Student View";
|
|
1639
|
+
readonly 'admin.exams.preview.mode.answers': "Answer Key";
|
|
1637
1640
|
readonly 'admin.exams.preview.examInfo': "Exam Information";
|
|
1638
1641
|
readonly 'admin.exams.preview.duration': "Duration:";
|
|
1639
1642
|
readonly 'admin.exams.preview.questionCount': "Questions:";
|
|
@@ -1660,6 +1663,8 @@ declare const en: {
|
|
|
1660
1663
|
readonly 'admin.exams.preview.next': "Next";
|
|
1661
1664
|
readonly 'admin.exams.preview.questionLabel': "Question {number}";
|
|
1662
1665
|
readonly 'admin.exams.preview.footerNote': "This is a preview mode to check the layout and content of the exam.";
|
|
1666
|
+
readonly 'admin.exams.preview.footerNoteStudent': "Preview mode: Student test taking (You can try answering questions).";
|
|
1667
|
+
readonly 'admin.exams.preview.footerNoteAnswers': "Preview mode: Showing correct answers and explanations.";
|
|
1663
1668
|
readonly 'admin.exams.preview.close': "Close";
|
|
1664
1669
|
readonly 'admin.exams.preview.loadError': "Unable to load questions. Please try again.";
|
|
1665
1670
|
readonly 'admin.exams.preview.points': "{count} pts";
|
|
@@ -1477,6 +1477,9 @@ declare const admin: {
|
|
|
1477
1477
|
readonly 'admin.exams.template.loading': "Đang tải thông tin đề thi...";
|
|
1478
1478
|
readonly 'admin.exams.template.loadingFallback': "Đang tải...";
|
|
1479
1479
|
readonly 'admin.exams.preview.title': "Xem trước đề thi: {name}";
|
|
1480
|
+
readonly 'admin.exams.preview.mode.label': "Chế độ xem:";
|
|
1481
|
+
readonly 'admin.exams.preview.mode.student': "Học sinh làm bài";
|
|
1482
|
+
readonly 'admin.exams.preview.mode.answers': "Xem đáp án đúng";
|
|
1480
1483
|
readonly 'admin.exams.preview.examInfo': "Thông tin đề thi";
|
|
1481
1484
|
readonly 'admin.exams.preview.duration': "Thời gian:";
|
|
1482
1485
|
readonly 'admin.exams.preview.questionCount': "Số câu hỏi:";
|
|
@@ -1503,6 +1506,8 @@ declare const admin: {
|
|
|
1503
1506
|
readonly 'admin.exams.preview.next': "Tiếp";
|
|
1504
1507
|
readonly 'admin.exams.preview.questionLabel': "Câu {number}";
|
|
1505
1508
|
readonly 'admin.exams.preview.footerNote': "Đây là chế độ xem trước để kiểm tra bố cục và nội dung đề thi.";
|
|
1509
|
+
readonly 'admin.exams.preview.footerNoteStudent': "Chế độ xem trước: Học sinh làm bài (Có thể thao tác thử trên câu hỏi).";
|
|
1510
|
+
readonly 'admin.exams.preview.footerNoteAnswers': "Chế độ xem trước: Đang hiển thị đáp án đúng và hướng dẫn giải.";
|
|
1506
1511
|
readonly 'admin.exams.preview.close': "Đóng";
|
|
1507
1512
|
readonly 'admin.exams.preview.loadError': "Không thể tải câu hỏi. Vui lòng thử lại.";
|
|
1508
1513
|
readonly 'admin.exams.preview.points': "{count} điểm";
|
|
@@ -1624,6 +1624,9 @@ const admin = {
|
|
|
1624
1624
|
'admin.exams.template.loadingFallback': 'Đang tải...',
|
|
1625
1625
|
// Preview dialog
|
|
1626
1626
|
'admin.exams.preview.title': 'Xem trước đề thi: {name}',
|
|
1627
|
+
'admin.exams.preview.mode.label': 'Chế độ xem:',
|
|
1628
|
+
'admin.exams.preview.mode.student': 'Học sinh làm bài',
|
|
1629
|
+
'admin.exams.preview.mode.answers': 'Xem đáp án đúng',
|
|
1627
1630
|
'admin.exams.preview.examInfo': 'Thông tin đề thi',
|
|
1628
1631
|
'admin.exams.preview.duration': 'Thời gian:',
|
|
1629
1632
|
'admin.exams.preview.questionCount': 'Số câu hỏi:',
|
|
@@ -1650,6 +1653,8 @@ const admin = {
|
|
|
1650
1653
|
'admin.exams.preview.next': 'Tiếp',
|
|
1651
1654
|
'admin.exams.preview.questionLabel': 'Câu {number}',
|
|
1652
1655
|
'admin.exams.preview.footerNote': 'Đây là chế độ xem trước để kiểm tra bố cục và nội dung đề thi.',
|
|
1656
|
+
'admin.exams.preview.footerNoteStudent': 'Chế độ xem trước: Học sinh làm bài (Có thể thao tác thử trên câu hỏi).',
|
|
1657
|
+
'admin.exams.preview.footerNoteAnswers': 'Chế độ xem trước: Đang hiển thị đáp án đúng và hướng dẫn giải.',
|
|
1653
1658
|
'admin.exams.preview.close': 'Đóng',
|
|
1654
1659
|
'admin.exams.preview.loadError': 'Không thể tải câu hỏi. Vui lòng thử lại.',
|
|
1655
1660
|
'admin.exams.preview.points': '{count} điểm',
|
|
@@ -1634,6 +1634,9 @@ declare const vi: {
|
|
|
1634
1634
|
readonly 'admin.exams.template.loading': "Đang tải thông tin đề thi...";
|
|
1635
1635
|
readonly 'admin.exams.template.loadingFallback': "Đang tải...";
|
|
1636
1636
|
readonly 'admin.exams.preview.title': "Xem trước đề thi: {name}";
|
|
1637
|
+
readonly 'admin.exams.preview.mode.label': "Chế độ xem:";
|
|
1638
|
+
readonly 'admin.exams.preview.mode.student': "Học sinh làm bài";
|
|
1639
|
+
readonly 'admin.exams.preview.mode.answers': "Xem đáp án đúng";
|
|
1637
1640
|
readonly 'admin.exams.preview.examInfo': "Thông tin đề thi";
|
|
1638
1641
|
readonly 'admin.exams.preview.duration': "Thời gian:";
|
|
1639
1642
|
readonly 'admin.exams.preview.questionCount': "Số câu hỏi:";
|
|
@@ -1660,6 +1663,8 @@ declare const vi: {
|
|
|
1660
1663
|
readonly 'admin.exams.preview.next': "Tiếp";
|
|
1661
1664
|
readonly 'admin.exams.preview.questionLabel': "Câu {number}";
|
|
1662
1665
|
readonly 'admin.exams.preview.footerNote': "Đây là chế độ xem trước để kiểm tra bố cục và nội dung đề thi.";
|
|
1666
|
+
readonly 'admin.exams.preview.footerNoteStudent': "Chế độ xem trước: Học sinh làm bài (Có thể thao tác thử trên câu hỏi).";
|
|
1667
|
+
readonly 'admin.exams.preview.footerNoteAnswers': "Chế độ xem trước: Đang hiển thị đáp án đúng và hướng dẫn giải.";
|
|
1663
1668
|
readonly 'admin.exams.preview.close': "Đóng";
|
|
1664
1669
|
readonly 'admin.exams.preview.loadError': "Không thể tải câu hỏi. Vui lòng thử lại.";
|
|
1665
1670
|
readonly 'admin.exams.preview.points': "{count} điểm";
|
|
@@ -186,6 +186,44 @@ const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
|
|
|
186
186
|
},
|
|
187
187
|
};
|
|
188
188
|
};
|
|
189
|
+
const reverseAnswerTheQuestionGroup = (apiQuestion) => {
|
|
190
|
+
const content = asRecord(apiQuestion.content);
|
|
191
|
+
const answer = readAnswer(apiQuestion);
|
|
192
|
+
const fallbackAnswers = asArray(answer.answer);
|
|
193
|
+
const items = asArray(content.items).map((rawItem, index) => {
|
|
194
|
+
const item = asRecord(rawItem);
|
|
195
|
+
const itemContent = asRecord(item.content);
|
|
196
|
+
const itemAnswer = asRecord(item.correctAnswer);
|
|
197
|
+
const nested = asArray(itemAnswer.expectedAnswers)
|
|
198
|
+
.filter((value) => typeof value === 'string')
|
|
199
|
+
.map((value) => value.trim())
|
|
200
|
+
.filter((value) => value !== '');
|
|
201
|
+
const fallback = asRecord(fallbackAnswers[index]);
|
|
202
|
+
const fallbackExpected = asArray(fallback.expectedAnswers)
|
|
203
|
+
.filter((value) => typeof value === 'string')
|
|
204
|
+
.map((value) => value.trim())
|
|
205
|
+
.filter((value) => value !== '');
|
|
206
|
+
const expectedAnswers = nested.length > 0 ? nested : (fallbackExpected.length > 0 ? fallbackExpected : ['']);
|
|
207
|
+
const isExample = item.isExample === true;
|
|
208
|
+
return {
|
|
209
|
+
question: asString(itemContent.question),
|
|
210
|
+
expectedAnswers,
|
|
211
|
+
isExample,
|
|
212
|
+
points: isExample ? 0 : asNumber(item.points, 1),
|
|
213
|
+
questionNumber: asNumber(item.questionNumber, index + 1),
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
content: '',
|
|
218
|
+
answer: {
|
|
219
|
+
instruction: asString(asRecord(content.meta).instruction, 'Read the text again and answer the questions.'),
|
|
220
|
+
passage: asString(asRecord(content.meta).passage),
|
|
221
|
+
items,
|
|
222
|
+
explanation: asString(apiQuestion.explanation),
|
|
223
|
+
points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
};
|
|
189
227
|
const reverseTrueFalseGroup = (apiQuestion) => {
|
|
190
228
|
const content = asRecord(apiQuestion.content);
|
|
191
229
|
const answer = readAnswer(apiQuestion);
|
|
@@ -367,6 +405,7 @@ const reverseSimpleAnswer = (apiQuestion) => {
|
|
|
367
405
|
const handlers = {
|
|
368
406
|
CHOOSE_THE_CORRECT_ANSWER: reverseChooseTheCorrectAnswer,
|
|
369
407
|
CHOOSE_THE_CORRECT_ANSWER_GROUP: reverseChooseTheCorrectAnswerGroup,
|
|
408
|
+
ANSWER_THE_QUESTION_GROUP: reverseAnswerTheQuestionGroup,
|
|
370
409
|
TRUE_FALSE_GROUP: reverseTrueFalseGroup,
|
|
371
410
|
CROSS_OUT_WORD_GROUP: reverseCrossOutWordGroup,
|
|
372
411
|
WORD_ORDER_AND_MATCH_GROUP: reverseWordOrderAndMatchGroup,
|
|
@@ -407,7 +446,61 @@ const handlers = {
|
|
|
407
446
|
SPEAKING_CONVERSATION: reverseGroupPayload,
|
|
408
447
|
SPEAKING_CUE_CARD: reverseSimpleAnswer,
|
|
409
448
|
GN_SPEAKING_INTERVIEW: reverseGroupPayload,
|
|
449
|
+
CROSSWORD_PUZZLE: reverseCrosswordPuzzle,
|
|
410
450
|
};
|
|
451
|
+
function reverseCrosswordPuzzle(apiQuestion) {
|
|
452
|
+
const apiContent = (apiQuestion.content || {});
|
|
453
|
+
const apiAnswer = (apiQuestion.correctAnswer || apiQuestion.correct_answer || apiQuestion.answer || {});
|
|
454
|
+
const rows = apiContent.gridSize?.rows || apiContent.rows || 12;
|
|
455
|
+
const cols = apiContent.gridSize?.cols || apiContent.cols || 12;
|
|
456
|
+
const title = apiContent.title || '';
|
|
457
|
+
const across = Array.isArray(apiContent.clues?.across) ? apiContent.clues.across : [];
|
|
458
|
+
const down = Array.isArray(apiContent.clues?.down) ? apiContent.clues.down : [];
|
|
459
|
+
const answers = apiAnswer.answers || {};
|
|
460
|
+
const clues = [];
|
|
461
|
+
for (const a of across) {
|
|
462
|
+
clues.push({
|
|
463
|
+
id: `across-${a.number}-${Date.now()}-${Math.random()}`,
|
|
464
|
+
number: a.number,
|
|
465
|
+
clue: a.clue || '',
|
|
466
|
+
startRow: a.startRow || 1,
|
|
467
|
+
startCol: a.startCol || 1,
|
|
468
|
+
length: a.length || 1,
|
|
469
|
+
direction: 'across',
|
|
470
|
+
answer: answers[`across-${a.number}`] || '',
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
for (const d of down) {
|
|
474
|
+
clues.push({
|
|
475
|
+
id: `down-${d.number}-${Date.now()}-${Math.random()}`,
|
|
476
|
+
number: d.number,
|
|
477
|
+
clue: d.clue || '',
|
|
478
|
+
startRow: d.startRow || 1,
|
|
479
|
+
startCol: d.startCol || 1,
|
|
480
|
+
length: d.length || 1,
|
|
481
|
+
direction: 'down',
|
|
482
|
+
answer: answers[`down-${d.number}`] || '',
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
content: {
|
|
487
|
+
title,
|
|
488
|
+
rows,
|
|
489
|
+
cols,
|
|
490
|
+
gridSize: { rows, cols },
|
|
491
|
+
clues: { across, down },
|
|
492
|
+
},
|
|
493
|
+
answer: {
|
|
494
|
+
title,
|
|
495
|
+
rows,
|
|
496
|
+
cols,
|
|
497
|
+
clues,
|
|
498
|
+
answers,
|
|
499
|
+
caseSensitive: apiAnswer.caseSensitive || false,
|
|
500
|
+
explanation: apiQuestion.explanation || '',
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
411
504
|
export function transformApiQuestionToFrontend(apiQuestion) {
|
|
412
505
|
const questionType = String(apiQuestion.questionType ?? apiQuestion.question_type ?? apiQuestion.type ?? '');
|
|
413
506
|
const handlerResult = (handlers[questionType] ?? defaultReverse)(apiQuestion);
|