@tinyweb_dev/oe-exam-sdk 0.1.29 → 0.1.31

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 (56) 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 +5 -1
  12. package/dist/components/exams/take/utils/answer-transformers.js +2 -1
  13. package/dist/components/exams/take/utils/question-transformers.d.ts +6 -1
  14. package/dist/components/exams/take/utils/question-transformers.js +33 -0
  15. package/dist/components/questions/_shared/config/question-types.config.js +6 -0
  16. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +32 -0
  17. package/dist/components/questions/_shared/types/answer-the-question-group.type.js +1 -0
  18. package/dist/components/questions/creator/question-type-registry.js +2 -0
  19. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +2 -0
  20. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -0
  21. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.d.ts +2 -0
  22. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +183 -0
  23. package/dist/components/questions/types/answer-the-question-group/index.d.ts +5 -0
  24. package/dist/components/questions/types/answer-the-question-group/index.js +5 -0
  25. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.d.ts +4 -0
  26. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +83 -0
  27. package/dist/components/questions/types/answer-the-question-group/register.d.ts +2 -0
  28. package/dist/components/questions/types/answer-the-question-group/register.js +36 -0
  29. package/dist/components/questions/types/answer-the-question-group/transform.d.ts +2 -0
  30. package/dist/components/questions/types/answer-the-question-group/transform.js +72 -0
  31. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -1
  32. package/dist/components/questions/viewer/QuestionViewer.js +2 -2
  33. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +4 -4
  34. package/dist/components/results/ReviewQuestionRenderer.js +1 -0
  35. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +19 -0
  36. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +27 -0
  37. package/dist/components/results/renderers/review-question-body-dedicated.d.ts +1 -0
  38. package/dist/components/results/renderers/review-question-body-dedicated.js +47 -2
  39. package/dist/components/results/review-data.js +12 -0
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.js +1 -1
  42. package/dist/shared/constants/question-skills.js +2 -0
  43. package/dist/shared/lib/i18n/messages/en/admin.d.ts +5 -0
  44. package/dist/shared/lib/i18n/messages/en/admin.js +5 -0
  45. package/dist/shared/lib/i18n/messages/en.d.ts +5 -0
  46. package/dist/shared/lib/i18n/messages/vi/admin.d.ts +5 -0
  47. package/dist/shared/lib/i18n/messages/vi/admin.js +5 -0
  48. package/dist/shared/lib/i18n/messages/vi.d.ts +5 -0
  49. package/dist/shared/lib/utils/question-reverse-transform.js +39 -0
  50. package/dist/shared/lib/utils/question-transform.js +2 -0
  51. package/dist/shared/types/common.types.d.ts +1 -1
  52. package/dist/shared/types/questions/answer-the-question-group.d.ts +39 -0
  53. package/dist/shared/types/questions/answer-the-question-group.js +1 -0
  54. package/dist/shared/types/questions/index.d.ts +1 -0
  55. package/dist/shared/types/questions/index.js +2 -0
  56. package/package.json +1 -1
@@ -0,0 +1,4 @@
1
+ import type { AnswerTheQuestionGroupData, AnswerTheQuestionGroupItemData } from '../../_shared/types/answer-the-question-group.type';
2
+ export declare function createEmptyAnswerTheQuestionGroupItem(questionNumber: number): AnswerTheQuestionGroupItemData;
3
+ export declare function mapAnswerTheQuestionGroupItem(item: unknown, index: number, fallbackAnswers?: string[]): AnswerTheQuestionGroupItemData;
4
+ export declare function mapQuestionToAnswerTheQuestionGroupData(question: Record<string, unknown> | null | undefined): AnswerTheQuestionGroupData;
@@ -0,0 +1,83 @@
1
+ const DEFAULT_INSTRUCTION = 'Read the text again and answer the questions.';
2
+ function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ function asRecord(value) {
6
+ return isRecord(value) ? value : {};
7
+ }
8
+ function asString(value, fallback = '') {
9
+ return typeof value === 'string' ? value : fallback;
10
+ }
11
+ function normalizeExpectedAnswers(value) {
12
+ if (!Array.isArray(value)) {
13
+ return [];
14
+ }
15
+ return value
16
+ .filter((item) => typeof item === 'string')
17
+ .map((item) => item.trim())
18
+ .filter((item) => item !== '');
19
+ }
20
+ export function createEmptyAnswerTheQuestionGroupItem(questionNumber) {
21
+ return {
22
+ question: '',
23
+ expectedAnswers: [''],
24
+ isExample: false,
25
+ points: 1,
26
+ questionNumber,
27
+ };
28
+ }
29
+ export function mapAnswerTheQuestionGroupItem(item, index, fallbackAnswers) {
30
+ const record = asRecord(item);
31
+ const content = asRecord(record.content);
32
+ const correctAnswer = asRecord(record.correctAnswer);
33
+ const isExample = record.isExample === true;
34
+ const nested = normalizeExpectedAnswers(correctAnswer.expectedAnswers);
35
+ const expectedAnswers = nested.length > 0
36
+ ? nested
37
+ : (normalizeExpectedAnswers(fallbackAnswers).length > 0
38
+ ? normalizeExpectedAnswers(fallbackAnswers)
39
+ : ['']);
40
+ return {
41
+ question: asString(content.question) || asString(record.question),
42
+ expectedAnswers,
43
+ isExample,
44
+ points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
45
+ questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
46
+ };
47
+ }
48
+ export function mapQuestionToAnswerTheQuestionGroupData(question) {
49
+ const source = asRecord(question);
50
+ const content = asRecord(source.content);
51
+ const answer = asRecord(source.answer);
52
+ const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
53
+ const explanation = String(source.explanation || answer.explanation || '');
54
+ const fallbackAnswers = Array.isArray(correctAnswer.answer)
55
+ ? correctAnswer.answer
56
+ : [];
57
+ if (Array.isArray(content.items)) {
58
+ const items = content.items.map((item, index) => mapAnswerTheQuestionGroupItem(item, index, fallbackAnswers[index]?.expectedAnswers));
59
+ return {
60
+ instruction: asString(asRecord(content.meta).instruction) || DEFAULT_INSTRUCTION,
61
+ passage: asString(asRecord(content.meta).passage),
62
+ items,
63
+ explanation,
64
+ points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
65
+ };
66
+ }
67
+ if (Array.isArray(answer.items)) {
68
+ return {
69
+ instruction: asString(answer.instruction, DEFAULT_INSTRUCTION),
70
+ passage: asString(answer.passage),
71
+ items: answer.items,
72
+ explanation,
73
+ points: typeof answer.points === 'number' ? answer.points : 1,
74
+ };
75
+ }
76
+ return {
77
+ instruction: DEFAULT_INSTRUCTION,
78
+ passage: '',
79
+ items: [createEmptyAnswerTheQuestionGroupItem(1)],
80
+ explanation,
81
+ points: 1,
82
+ };
83
+ }
@@ -0,0 +1,2 @@
1
+ import type { QuestionTypeRegistration } from '../../creator/question-type-registry';
2
+ export declare const answerTheQuestionGroupRegistration: QuestionTypeRegistration;
@@ -0,0 +1,36 @@
1
+ import { AnswerTheQuestionGroupCreator } from './AnswerTheQuestionGroupCreator';
2
+ import { mapQuestionToAnswerTheQuestionGroupData } from './map-answer-the-question-group-data';
3
+ export const answerTheQuestionGroupRegistration = {
4
+ component: AnswerTheQuestionGroupCreator,
5
+ transformInitialData: (initialData) => mapQuestionToAnswerTheQuestionGroupData(initialData),
6
+ getExtraProps: (ctx) => ({
7
+ onUnsavedChangesChange: ctx.onUnsavedChangesChange,
8
+ validationRef: ctx.validationRef,
9
+ }),
10
+ wrapOnSave: (data, ctx) => {
11
+ const payload = data;
12
+ return {
13
+ type: ctx.questionType,
14
+ content: ctx.state.content,
15
+ points: payload.points ?? ctx.state.points ?? 1,
16
+ level: ctx.state.level,
17
+ difficulty: ctx.state.difficulty,
18
+ skill: ctx.state.skill,
19
+ answer: payload,
20
+ explanation: payload.explanation || '',
21
+ };
22
+ },
23
+ wrapOnChange: (data, ctx) => {
24
+ const payload = data;
25
+ return {
26
+ type: ctx.questionType,
27
+ content: ctx.state.content,
28
+ points: payload.points ?? ctx.state.points ?? 1,
29
+ level: ctx.state.level,
30
+ difficulty: ctx.state.difficulty,
31
+ skill: ctx.state.skill,
32
+ answer: payload,
33
+ explanation: payload.explanation || '',
34
+ };
35
+ },
36
+ };
@@ -0,0 +1,2 @@
1
+ import type { TransformHandler } from '../../../../shared/lib/utils/question-transform-types';
2
+ export declare const transformAnswerTheQuestionGroup: TransformHandler;
@@ -0,0 +1,72 @@
1
+ const CHILD_QUESTION_TYPE = 'ANSWER_THE_QUESTION';
2
+ const GRADING_TYPE = 'AI';
3
+ const DEFAULT_INSTRUCTION = 'Read the text again and answer the questions.';
4
+ function normalizeExpectedAnswers(value) {
5
+ if (!Array.isArray(value)) {
6
+ return [];
7
+ }
8
+ return value
9
+ .filter((item) => typeof item === 'string')
10
+ .map((item) => item.trim())
11
+ .filter((item) => item !== '');
12
+ }
13
+ function sumGradablePoints(items) {
14
+ return items.reduce((total, item) => {
15
+ if (item.isExample) {
16
+ return total;
17
+ }
18
+ const points = typeof item.points === 'number' ? item.points : 1;
19
+ return total + points;
20
+ }, 0);
21
+ }
22
+ function isContentEmpty(html) {
23
+ return !html || html === '<p></p>' || html === '<p><br></p>' || html.trim() === '';
24
+ }
25
+ export const transformAnswerTheQuestionGroup = (question) => {
26
+ const answerData = question.answer;
27
+ const items = Array.isArray(answerData?.items) ? answerData.items : [];
28
+ if (items.length === 0) {
29
+ return {
30
+ apiContent: {
31
+ meta: {
32
+ instruction: DEFAULT_INSTRUCTION,
33
+ gradingType: GRADING_TYPE,
34
+ isAiGraded: true,
35
+ },
36
+ items: [],
37
+ },
38
+ apiCorrectAnswer: { answer: [] },
39
+ };
40
+ }
41
+ const apiItems = items.map((item, index) => {
42
+ const expectedAnswers = normalizeExpectedAnswers(item.expectedAnswers);
43
+ return {
44
+ questionType: CHILD_QUESTION_TYPE,
45
+ ...(item.isExample ? { isExample: true } : {}),
46
+ content: {
47
+ question: item.question || '',
48
+ },
49
+ correctAnswer: { expectedAnswers },
50
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
51
+ questionNumber: item.questionNumber || index + 1,
52
+ };
53
+ });
54
+ const passage = answerData?.passage?.trim();
55
+ return {
56
+ apiContent: {
57
+ meta: {
58
+ instruction: answerData?.instruction || DEFAULT_INSTRUCTION,
59
+ ...(!isContentEmpty(passage) ? { passage } : {}),
60
+ gradingType: GRADING_TYPE,
61
+ isAiGraded: true,
62
+ },
63
+ items: apiItems,
64
+ },
65
+ apiCorrectAnswer: {
66
+ answer: apiItems.map((item) => ({
67
+ expectedAnswers: item.correctAnswer.expectedAnswers,
68
+ })),
69
+ },
70
+ totalPoints: sumGradablePoints(items),
71
+ };
72
+ };
@@ -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
@@ -343,7 +343,7 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
343
343
  regionNodes: answer?.regionNodes || [],
344
344
  instructions: answer?.instructions || [],
345
345
  };
346
- return (_jsx(ReadAndColorObjectsClient, { questionData: questionData, onSubmit: (userConnections) => {
346
+ return (_jsx(ReadAndColorObjectsClient, { questionData: questionData, isReviewMode: isReviewMode, onSubmit: (userConnections) => {
347
347
  setSelectedAnswer(userConnections);
348
348
  onSubmitAnswer?.(userConnections);
349
349
  } }));
@@ -600,7 +600,7 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
600
600
  case 'CROSS_OUT_WORD_GROUP':
601
601
  return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode }));
602
602
  case 'SPONTANEOUS_QA_GROUP':
603
- return _jsx(SpontaneousQaGroupViewer, { question: question });
603
+ return (_jsx(SpontaneousQaGroupViewer, { question: question, isReviewMode: isReviewMode }));
604
604
  case 'WORD_ORDER_AND_MATCH_GROUP':
605
605
  case 'WORD_ORDER_AND_MATCH':
606
606
  return (_jsx(WordOrderAndMatchGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
@@ -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) {
@@ -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 = []) {
@@ -156,6 +157,17 @@ function transformStudentAnswer(questionId, questionType, value) {
156
157
  typeof item === 'string' ? item : '',
157
158
  ]));
158
159
  }
160
+ if (questionType === 'ANSWER_THE_QUESTION_GROUP') {
161
+ const answer = Array.isArray(record.answer)
162
+ ? record.answer
163
+ : Array.isArray(value)
164
+ ? value
165
+ : [];
166
+ return Object.fromEntries(answer.map((item, index) => [
167
+ `${questionId}-item-${index}`,
168
+ typeof item === 'string' ? item : '',
169
+ ]));
170
+ }
159
171
  if (questionType === 'CROSS_OUT_WORD_GROUP') {
160
172
  const answer = Array.isArray(record.answer)
161
173
  ? record.answer
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,
@@ -198,6 +199,7 @@ export const QUESTION_TYPE_LABELS = {
198
199
  'READING_PASSAGE_FILL': 'Đọc - Điền đoạn văn',
199
200
  'FILL_BLANK': 'Điền chỗ trống',
200
201
  'ANSWER_THE_QUESTION': 'Nghe - Đọc câu hỏi và viết đáp án',
202
+ 'ANSWER_THE_QUESTION_GROUP': 'Đọc và trả lời câu hỏi (AI)',
201
203
  'WORD_ORDERING': 'Sắp xếp từ',
202
204
  'WORD_FILL_PARAGRAPH': 'Điền từ vào đoạn văn',
203
205
  'MATCH_BY_WRITING_ANSWER': 'Ghép đáp án bằng cách viết',
@@ -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";