@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.
Files changed (80) hide show
  1. package/dist/components/exams/ExamPreviewDialog.d.ts +9 -3
  2. package/dist/components/exams/ExamPreviewDialog.js +23 -5
  3. package/dist/components/exams/ExamPreviewPartContent.d.ts +2 -1
  4. package/dist/components/exams/ExamPreviewPartContent.js +3 -2
  5. package/dist/components/exams/ExamPreviewSidebar.d.ts +3 -1
  6. package/dist/components/exams/ExamPreviewSidebar.js +7 -2
  7. package/dist/components/exams/exam-preview.mode.d.ts +4 -0
  8. package/dist/components/exams/exam-preview.mode.js +5 -0
  9. package/dist/components/exams/index.d.ts +1 -0
  10. package/dist/components/exams/index.js +1 -0
  11. package/dist/components/exams/take/components/QuestionRenderer.js +10 -2
  12. package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.d.ts +3 -0
  13. package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +285 -0
  14. package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
  15. package/dist/components/exams/take/components/question-renderers/index.js +1 -0
  16. package/dist/components/exams/take/types.d.ts +30 -0
  17. package/dist/components/exams/take/utils/answer-transformers.js +37 -6
  18. package/dist/components/exams/take/utils/question-transformers.d.ts +28 -1
  19. package/dist/components/exams/take/utils/question-transformers.js +69 -0
  20. package/dist/components/questions/_shared/config/question-types.config.js +12 -0
  21. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +32 -0
  22. package/dist/components/questions/_shared/types/answer-the-question-group.type.js +1 -0
  23. package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +16 -0
  24. package/dist/components/questions/_shared/types/crossword-puzzle.type.js +1 -0
  25. package/dist/components/questions/components/QuestionSearchDropdown.js +1 -1
  26. package/dist/components/questions/creator/question-type-registry.js +4 -0
  27. package/dist/components/questions/question-bank/QuestionEditorRenderer.d.ts +2 -1
  28. package/dist/components/questions/question-bank/QuestionEditorRenderer.js +16 -0
  29. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +2 -0
  30. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -0
  31. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.d.ts +2 -0
  32. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +183 -0
  33. package/dist/components/questions/types/answer-the-question-group/index.d.ts +5 -0
  34. package/dist/components/questions/types/answer-the-question-group/index.js +5 -0
  35. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.d.ts +4 -0
  36. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +83 -0
  37. package/dist/components/questions/types/answer-the-question-group/register.d.ts +2 -0
  38. package/dist/components/questions/types/answer-the-question-group/register.js +36 -0
  39. package/dist/components/questions/types/answer-the-question-group/transform.d.ts +2 -0
  40. package/dist/components/questions/types/answer-the-question-group/transform.js +72 -0
  41. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +11 -0
  42. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +69 -0
  43. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.d.ts +8 -0
  44. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +213 -0
  45. package/dist/components/questions/types/crossword-puzzle/index.d.ts +4 -0
  46. package/dist/components/questions/types/crossword-puzzle/index.js +4 -0
  47. package/dist/components/questions/types/crossword-puzzle/register.d.ts +2 -0
  48. package/dist/components/questions/types/crossword-puzzle/register.js +29 -0
  49. package/dist/components/questions/types/crossword-puzzle/transform.d.ts +2 -0
  50. package/dist/components/questions/types/crossword-puzzle/transform.js +75 -0
  51. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -1
  52. package/dist/components/questions/viewer/QuestionViewer.js +9 -6
  53. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +4 -4
  54. package/dist/components/results/ReviewQuestionRenderer.js +1 -0
  55. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +19 -0
  56. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +27 -0
  57. package/dist/components/results/renderers/review-question-body-dedicated.d.ts +1 -0
  58. package/dist/components/results/renderers/review-question-body-dedicated.js +47 -2
  59. package/dist/components/results/renderers/review-question-body-movers.js +7 -2
  60. package/dist/components/results/review-data.js +20 -4
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.js +1 -1
  63. package/dist/shared/constants/question-skills.js +4 -0
  64. package/dist/shared/lib/i18n/messages/en/admin.d.ts +5 -0
  65. package/dist/shared/lib/i18n/messages/en/admin.js +5 -0
  66. package/dist/shared/lib/i18n/messages/en.d.ts +5 -0
  67. package/dist/shared/lib/i18n/messages/vi/admin.d.ts +5 -0
  68. package/dist/shared/lib/i18n/messages/vi/admin.js +5 -0
  69. package/dist/shared/lib/i18n/messages/vi.d.ts +5 -0
  70. package/dist/shared/lib/utils/question-reverse-transform.js +93 -0
  71. package/dist/shared/lib/utils/question-transform-validation.js +12 -0
  72. package/dist/shared/lib/utils/question-transform.js +4 -0
  73. package/dist/shared/types/common.types.d.ts +1 -1
  74. package/dist/shared/types/questions/answer-the-question-group.d.ts +39 -0
  75. package/dist/shared/types/questions/answer-the-question-group.js +1 -0
  76. package/dist/shared/types/questions/crossword-puzzle.d.ts +50 -0
  77. package/dist/shared/types/questions/crossword-puzzle.js +10 -0
  78. package/dist/shared/types/questions/index.d.ts +2 -0
  79. package/dist/shared/types/questions/index.js +4 -0
  80. package/package.json +1 -1
@@ -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 = true, }) {
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 shows Bài N + instruction inside the client — no outer card label.
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 has its own bordered shell — avoid double Card border.
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
- result.cellAnswersMap[answer.questionId] = { cells: correct.cells, caseSensitive: correct.caseSensitive === true };
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);