@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
@@ -1,4 +1,5 @@
1
1
  import type { ExamQuestionsSdkApi } from '../../api/exam-questions.types';
2
+ import { ExamPreviewMode } from './exam-preview.mode';
2
3
  export interface ExamPreviewDialogProps {
3
4
  open: boolean;
4
5
  onOpenChange: (open: boolean) => void;
@@ -8,12 +9,17 @@ export interface ExamPreviewDialogProps {
8
9
  api: ExamQuestionsSdkApi;
9
10
  /** Fallback heading shown until the exam name loads. */
10
11
  title?: string;
12
+ /** Initial preview mode. Default is student view without answers. */
13
+ defaultMode?: ExamPreviewMode;
11
14
  }
12
15
  /**
13
16
  * Read-only popup that previews an exam by id.
14
17
  *
18
+ * Supports two modes:
19
+ * - 'student' (default): interactive student taking view without revealing answers
20
+ * - 'answers': review view with correct answers highlighted and explanations
21
+ *
15
22
  * UI mirrors the host app: one Part per screen (all questions of that part),
16
- * Next/Prev navigates between parts. No attempt is created — data comes from
17
- * {@link ExamQuestionsSdkApi}. Questions render via QuestionViewer in review mode.
23
+ * Next/Prev navigates between parts. Data comes from {@link ExamQuestionsSdkApi}.
18
24
  */
19
- export declare function ExamPreviewDialog({ open, onOpenChange, examId, api, title, }: ExamPreviewDialogProps): import("react").JSX.Element;
25
+ export declare function ExamPreviewDialog({ open, onOpenChange, examId, api, title, defaultMode, }: ExamPreviewDialogProps): import("react").JSX.Element;
@@ -1,30 +1,42 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useRef, useState } from 'react';
4
- import { Eye } from 'lucide-react';
4
+ import { CheckCircle2, Eye, GraduationCap } from 'lucide-react';
5
5
  import { Button } from '../../components/ui/button';
6
6
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../components/ui/dialog';
7
7
  import { useT } from '../../shared/lib/i18n';
8
+ import { cn } from '../../shared/lib/utils';
9
+ import { ExamPreviewMode } from './exam-preview.mode';
8
10
  import { ExamPreviewPartContent } from './ExamPreviewPartContent';
9
11
  import { ExamPreviewSidebar } from './ExamPreviewSidebar';
10
12
  import { getQuestionNumber, getQuestionPoints, groupQuestionsByPart, sumQuestionPoints, unwrapApiData, } from './exam-preview.types';
11
13
  /**
12
14
  * Read-only popup that previews an exam by id.
13
15
  *
16
+ * Supports two modes:
17
+ * - 'student' (default): interactive student taking view without revealing answers
18
+ * - 'answers': review view with correct answers highlighted and explanations
19
+ *
14
20
  * UI mirrors the host app: one Part per screen (all questions of that part),
15
- * Next/Prev navigates between parts. No attempt is created — data comes from
16
- * {@link ExamQuestionsSdkApi}. Questions render via QuestionViewer in review mode.
21
+ * Next/Prev navigates between parts. Data comes from {@link ExamQuestionsSdkApi}.
17
22
  */
18
- export function ExamPreviewDialog({ open, onOpenChange, examId, api, title, }) {
23
+ export function ExamPreviewDialog({ open, onOpenChange, examId, api, title, defaultMode = ExamPreviewMode.STUDENT, }) {
19
24
  const t = useT();
20
25
  const [exam, setExam] = useState(null);
21
26
  const [questions, setQuestions] = useState([]);
22
27
  const [currentPartIndex, setCurrentPartIndex] = useState(0);
23
28
  const [showPartsDetail, setShowPartsDetail] = useState(false);
29
+ const [previewMode, setPreviewMode] = useState(defaultMode);
24
30
  const [loading, setLoading] = useState(false);
25
31
  const [error, setError] = useState(null);
26
32
  const [reloadKey, setReloadKey] = useState(0);
27
33
  const contentRef = useRef(null);
34
+ // Sync / reset previewMode whenever dialog opens
35
+ useEffect(() => {
36
+ if (open) {
37
+ setPreviewMode(defaultMode);
38
+ }
39
+ }, [open, defaultMode]);
28
40
  // Fetch exam + questions whenever the dialog opens for an exam.
29
41
  useEffect(() => {
30
42
  if (!open || !examId) {
@@ -117,5 +129,11 @@ export function ExamPreviewDialog({ open, onOpenChange, examId, api, title, }) {
117
129
  scrollToTop();
118
130
  }
119
131
  };
120
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-h-[95vh] max-w-7xl overflow-y-auto", children: [_jsx(DialogHeader, { children: _jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(Eye, { className: "h-5 w-5 text-blue-600" }), t('admin.exams.preview.title', { name: heading })] }) }), _jsxs("div", { className: "flex h-[80vh] flex-col gap-4 sm:flex-row", children: [_jsx(ExamPreviewSidebar, { examName: heading, duration: duration, totalPoints: totalPoints, questions: questions, questionsByPart: questionsByPart, currentPart: currentPart, currentPartIndex: currentPartIndex, totalParts: totalParts, progress: progress, loading: loading, showPartsDetail: showPartsDetail, onTogglePartsDetail: () => setShowPartsDetail((value) => !value), onGoToPart: goToPart }), _jsx(ExamPreviewPartContent, { loading: loading, error: error, questions: questions, currentPart: currentPart, currentPartQuestions: currentPartQuestions, currentPartIndex: currentPartIndex, totalParts: totalParts, partDisplayName: partDisplayName, partTotalPoints: partTotalPoints, contentRef: contentRef, onRetry: () => setReloadKey((key) => key + 1), onPreviousPart: goToPreviousPart, onNextPart: goToNextPart })] }), _jsx("div", { className: "border-t pt-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-sm text-gray-600", children: t('admin.exams.preview.footerNote') }), _jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), className: "border-gray-300", children: t('admin.exams.preview.close') })] }) })] }) }));
132
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-h-[95vh] max-w-7xl overflow-y-auto", children: [_jsxs(DialogHeader, { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between border-b pb-3 pr-8", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2 text-lg font-bold", children: [_jsx(Eye, { className: "h-5 w-5 text-blue-600" }), t('admin.exams.preview.title', { name: heading })] }), _jsxs("div", { className: "inline-flex items-center rounded-lg border border-gray-200 bg-gray-100 p-1 shadow-inner", children: [_jsxs("button", { type: "button", onClick: () => setPreviewMode(ExamPreviewMode.STUDENT), className: cn('inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-semibold transition-all', previewMode === ExamPreviewMode.STUDENT
133
+ ? 'bg-white text-blue-700 shadow-sm ring-1 ring-black/5'
134
+ : 'text-gray-600 hover:bg-gray-200/70 hover:text-gray-900'), children: [_jsx(GraduationCap, { className: "h-4 w-4" }), _jsx("span", { children: t('admin.exams.preview.mode.student') })] }), _jsxs("button", { type: "button", onClick: () => setPreviewMode(ExamPreviewMode.ANSWERS), className: cn('inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-semibold transition-all', previewMode === ExamPreviewMode.ANSWERS
135
+ ? 'bg-emerald-600 text-white shadow-sm'
136
+ : 'text-gray-600 hover:bg-gray-200/70 hover:text-gray-900'), children: [_jsx(CheckCircle2, { className: "h-4 w-4" }), _jsx("span", { children: t('admin.exams.preview.mode.answers') })] })] })] }), _jsxs("div", { className: "flex h-[80vh] flex-col gap-4 sm:flex-row", children: [_jsx(ExamPreviewSidebar, { examName: heading, duration: duration, totalPoints: totalPoints, questions: questions, questionsByPart: questionsByPart, currentPart: currentPart, currentPartIndex: currentPartIndex, totalParts: totalParts, progress: progress, loading: loading, showPartsDetail: showPartsDetail, previewMode: previewMode, onTogglePartsDetail: () => setShowPartsDetail((value) => !value), onGoToPart: goToPart }), _jsx(ExamPreviewPartContent, { loading: loading, error: error, questions: questions, currentPart: currentPart, currentPartQuestions: currentPartQuestions, currentPartIndex: currentPartIndex, totalParts: totalParts, partDisplayName: partDisplayName, partTotalPoints: partTotalPoints, contentRef: contentRef, isReviewMode: previewMode === ExamPreviewMode.ANSWERS, onRetry: () => setReloadKey((key) => key + 1), onPreviousPart: goToPreviousPart, onNextPart: goToNextPart })] }), _jsx("div", { className: "border-t pt-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-sm text-gray-600", children: previewMode === ExamPreviewMode.ANSWERS
137
+ ? t('admin.exams.preview.footerNoteAnswers')
138
+ : t('admin.exams.preview.footerNoteStudent') }), _jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), className: "border-gray-300", children: t('admin.exams.preview.close') })] }) })] }) }));
121
139
  }
@@ -11,8 +11,9 @@ export interface ExamPreviewPartContentProps {
11
11
  partDisplayName: string;
12
12
  partTotalPoints: number;
13
13
  contentRef: RefObject<HTMLDivElement | null>;
14
+ isReviewMode?: boolean;
14
15
  onRetry: () => void;
15
16
  onPreviousPart: () => void;
16
17
  onNextPart: () => void;
17
18
  }
18
- export declare function ExamPreviewPartContent({ loading, error, questions, currentPart, currentPartQuestions, currentPartIndex, totalParts, partDisplayName, partTotalPoints, contentRef, onRetry, onPreviousPart, onNextPart, }: ExamPreviewPartContentProps): import("react").JSX.Element;
19
+ export declare function ExamPreviewPartContent({ loading, error, questions, currentPart, currentPartQuestions, currentPartIndex, totalParts, partDisplayName, partTotalPoints, contentRef, isReviewMode, onRetry, onPreviousPart, onNextPart, }: ExamPreviewPartContentProps): import("react").JSX.Element;
@@ -5,8 +5,9 @@ import { Badge } from '../../components/ui/badge';
5
5
  import { Button } from '../../components/ui/button';
6
6
  import { QuestionViewer } from '../questions/viewer/QuestionViewer';
7
7
  import { useT } from '../../shared/lib/i18n';
8
+ import { ExamPreviewMode } from './exam-preview.mode';
8
9
  import { difficultyBadgeClass, getQuestionDifficulty, getQuestionNumber, getQuestionPoints, } from './exam-preview.types';
9
- export function ExamPreviewPartContent({ loading, error, questions, currentPart, currentPartQuestions, currentPartIndex, totalParts, partDisplayName, partTotalPoints, contentRef, onRetry, onPreviousPart, onNextPart, }) {
10
+ export function ExamPreviewPartContent({ loading, error, questions, currentPart, currentPartQuestions, currentPartIndex, totalParts, partDisplayName, partTotalPoints, contentRef, isReviewMode = false, onRetry, onPreviousPart, onNextPart, }) {
10
11
  const t = useT();
11
12
  return (_jsx("div", { className: "flex-1 overflow-y-auto", children: _jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center justify-between border-b pb-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold", children: loading ? (_jsx("span", { className: "text-gray-400", children: t('admin.exams.preview.loading') })) : totalParts > 0 ? (t('admin.exams.preview.partIndex', {
12
13
  index: String(currentPartIndex + 1),
@@ -22,6 +23,6 @@ export function ExamPreviewPartContent({ loading, error, questions, currentPart,
22
23
  number: String(qNumber),
23
24
  }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: "outline", children: t('admin.exams.preview.points', {
24
25
  count: String(getQuestionPoints(question)),
25
- }) }), _jsx(Badge, { className: difficultyBadgeClass(difficulty), children: difficulty })] })] }), _jsx(QuestionViewer, { question: question, isReviewMode: true })] }, question.id));
26
+ }) }), _jsx(Badge, { className: difficultyBadgeClass(difficulty), children: difficulty })] })] }), _jsx(QuestionViewer, { question: question, isReviewMode: isReviewMode }, `${question.id}-${isReviewMode ? ExamPreviewMode.ANSWERS : ExamPreviewMode.STUDENT}`)] }, question.id));
26
27
  }) })) : null }), !loading && totalParts > 0 && (_jsxs("div", { className: "mt-auto flex items-center justify-between border-t border-gray-200 bg-gray-50 px-4 py-3", children: [_jsxs(Button, { variant: "outline", onClick: onPreviousPart, disabled: currentPartIndex === 0, className: "flex h-9 items-center gap-2 px-3 text-sm", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('admin.exams.preview.prevPart') }), _jsx("span", { className: "sm:hidden", children: t('admin.exams.preview.prev') })] }), _jsxs("div", { className: "text-sm text-gray-600", children: [currentPartIndex + 1, " / ", totalParts] }), _jsxs(Button, { variant: "outline", onClick: onNextPart, disabled: currentPartIndex === totalParts - 1, className: "flex h-9 items-center gap-2 px-3 text-sm", children: [_jsx("span", { className: "hidden sm:inline", children: t('admin.exams.preview.nextPart') }), _jsx("span", { className: "sm:hidden", children: t('admin.exams.preview.next') }), _jsx(ArrowRight, { className: "h-4 w-4" })] })] }))] }) }));
27
28
  }
@@ -1,3 +1,4 @@
1
+ import { ExamPreviewMode } from './exam-preview.mode';
1
2
  import { type PreviewQuestion, type QuestionsByPart } from './exam-preview.types';
2
3
  export interface ExamPreviewSidebarProps {
3
4
  examName: string;
@@ -11,7 +12,8 @@ export interface ExamPreviewSidebarProps {
11
12
  progress: number;
12
13
  loading: boolean;
13
14
  showPartsDetail: boolean;
15
+ previewMode?: ExamPreviewMode;
14
16
  onTogglePartsDetail: () => void;
15
17
  onGoToPart: (index: number) => void;
16
18
  }
17
- export declare function ExamPreviewSidebar({ examName, duration, totalPoints, questions, questionsByPart, currentPart, currentPartIndex, totalParts, progress, loading, showPartsDetail, onTogglePartsDetail, onGoToPart, }: ExamPreviewSidebarProps): import("react").JSX.Element;
19
+ export declare function ExamPreviewSidebar({ examName, duration, totalPoints, questions, questionsByPart, currentPart, currentPartIndex, totalParts, progress, loading, showPartsDetail, previewMode, onTogglePartsDetail, onGoToPart, }: ExamPreviewSidebarProps): import("react").JSX.Element;
@@ -6,10 +6,15 @@ import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/ca
6
6
  import { Progress } from '../../components/ui/progress';
7
7
  import { Loader2 } from 'lucide-react';
8
8
  import { useT } from '../../shared/lib/i18n';
9
+ import { ExamPreviewMode } from './exam-preview.mode';
9
10
  import { getQuestionNumber, } from './exam-preview.types';
10
- export function ExamPreviewSidebar({ examName, duration, totalPoints, questions, questionsByPart, currentPart, currentPartIndex, totalParts, progress, loading, showPartsDetail, onTogglePartsDetail, onGoToPart, }) {
11
+ export function ExamPreviewSidebar({ examName, duration, totalPoints, questions, questionsByPart, currentPart, currentPartIndex, totalParts, progress, loading, showPartsDetail, previewMode = ExamPreviewMode.STUDENT, onTogglePartsDetail, onGoToPart, }) {
11
12
  const t = useT();
12
- return (_jsx("div", { className: "w-full overflow-y-auto border-b border-gray-200 sm:w-80 sm:border-b-0 sm:border-r", children: _jsxs("div", { className: "space-y-4 p-4", children: [_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "text-base", children: t('admin.exams.preview.examInfo') }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-gray-600", children: t('admin.exams.preview.duration') }), _jsx("span", { className: "text-sm font-medium", children: t('admin.exams.view.minutes', { count: String(duration) }) })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-gray-600", children: t('admin.exams.preview.questionCount') }), _jsx("span", { className: "text-sm font-medium", children: loading
13
+ return (_jsx("div", { className: "w-full overflow-y-auto border-b border-gray-200 sm:w-80 sm:border-b-0 sm:border-r", children: _jsxs("div", { className: "space-y-4 p-4", children: [_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "text-base", children: t('admin.exams.preview.examInfo') }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-gray-600", children: t('admin.exams.preview.mode.label') }), _jsx(Badge, { variant: "outline", className: previewMode === ExamPreviewMode.ANSWERS
14
+ ? 'border-emerald-300 bg-emerald-50 text-emerald-700 font-semibold'
15
+ : 'border-blue-300 bg-blue-50 text-blue-700 font-semibold', children: previewMode === ExamPreviewMode.ANSWERS
16
+ ? t('admin.exams.preview.mode.answers')
17
+ : t('admin.exams.preview.mode.student') })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-gray-600", children: t('admin.exams.preview.duration') }), _jsx("span", { className: "text-sm font-medium", children: t('admin.exams.view.minutes', { count: String(duration) }) })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-gray-600", children: t('admin.exams.preview.questionCount') }), _jsx("span", { className: "text-sm font-medium", children: loading
13
18
  ? '...'
14
19
  : t('admin.exams.preview.questions', { count: String(questions.length) }) })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-gray-600", children: t('admin.exams.preview.totalPoints') }), _jsx("span", { className: "text-sm font-medium", children: t('admin.exams.preview.points', { count: String(totalPoints) }) })] })] })] }), !loading && totalParts > 0 && (_jsx(Card, { children: _jsx(CardContent, { className: "p-4", children: _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex justify-between text-sm", children: [_jsx("span", { children: t('admin.exams.preview.progress') }), _jsxs("span", { className: "font-medium", children: [currentPartIndex + 1, "/", totalParts] })] }), _jsx(Progress, { value: progress, className: "h-2" })] }) }) })), loading ? (_jsx("div", { className: "flex items-center justify-center py-4", children: _jsxs("div", { className: "flex items-center gap-2 text-gray-500", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), _jsx("span", { className: "text-sm", children: t('admin.exams.preview.loading') })] }) })) : (totalParts > 0 && (_jsxs("div", { className: "space-y-3", children: [_jsx("h4", { className: "text-sm font-medium text-gray-700", children: t('admin.exams.preview.questionNavigation') }), _jsx("div", { className: "flex flex-wrap gap-2", children: questionsByPart.map((part, index) => {
15
20
  const isCurrent = index === currentPartIndex;
@@ -0,0 +1,4 @@
1
+ export declare enum ExamPreviewMode {
2
+ STUDENT = "student",
3
+ ANSWERS = "answers"
4
+ }
@@ -0,0 +1,5 @@
1
+ export var ExamPreviewMode;
2
+ (function (ExamPreviewMode) {
3
+ ExamPreviewMode["STUDENT"] = "student";
4
+ ExamPreviewMode["ANSWERS"] = "answers";
5
+ })(ExamPreviewMode || (ExamPreviewMode = {}));
@@ -32,5 +32,6 @@ export { EXAM_DIFFICULTY, ExamViewDialog } from './ExamViewDialog';
32
32
  export type { ExamDifficulty, ExamViewDialogExam, ExamViewDialogProps } from './ExamViewDialog';
33
33
  export { ExamPreviewDialog } from './ExamPreviewDialog';
34
34
  export type { ExamPreviewDialogProps } from './ExamPreviewDialog';
35
+ export { ExamPreviewMode } from './exam-preview.mode';
35
36
  export type { PreviewQuestion, QuestionsByPart } from './exam-preview.types';
36
37
  export { QuestionStatus, clearQuestionStatuses, getQuestionStatus, initializeQuestionStatusesFromAPI, setQuestionStatus } from './exam-question-status';
@@ -16,4 +16,5 @@ export { UnsavedChangesDialog } from './UnsavedChangesDialog';
16
16
  export { CompletionDialog } from './CompletionDialog';
17
17
  export { EXAM_DIFFICULTY, ExamViewDialog } from './ExamViewDialog';
18
18
  export { ExamPreviewDialog } from './ExamPreviewDialog';
19
+ export { ExamPreviewMode } from './exam-preview.mode';
19
20
  export { QuestionStatus, clearQuestionStatuses, getQuestionStatus, initializeQuestionStatusesFromAPI, setQuestionStatus } from './exam-question-status';
@@ -8,9 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  * 2. Transforms data to renderer-specific props
9
9
  * 3. Renders the correct component for each question type
10
10
  */
11
- import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, } from './question-renderers';
11
+ import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversCrosswordPuzzleRenderer, } from './question-renderers';
12
12
  import { SpeakingQuestionRenderer } from './speaking/renderers';
13
- import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformTrueFalse, transformTrueFalseGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, } from '../utils/question-transformers';
13
+ import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformCrosswordPuzzle, } from '../utils/question-transformers';
14
14
  import { transformCrossOutWordGroup } from '../utils/cross-out-word-group.transformer';
15
15
  import { normalizeColoringAssignments } from '../utils/coloring-assignments';
16
16
  export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode = false, onExit, onPartComplete, onPartBack, speakingTheme, initialQuestionIndex, skipTeacherVideo, onTeacherIntroPlayed, currentQuestionIndex, }) {
@@ -112,6 +112,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
112
112
  const data = transformGridFill(questions);
113
113
  return (_jsx(MoversGridFillRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, grid: data.grid, rows: data.rows, columns: data.columns, cellAnswers: {}, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, disabledCells: data.disabledCells }));
114
114
  }
115
+ case 'CROSSWORD_PUZZLE': {
116
+ const data = transformCrosswordPuzzle(questions);
117
+ return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
118
+ }
115
119
  case 'IMAGE_OBJECT_MATCHING': {
116
120
  // Reuse coloring renderer — same content structure as READ_AND_COLOR_OBJECTS
117
121
  const data = transformColorObjects(questions);
@@ -137,6 +141,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
137
141
  const data = transformAnswerTheQuestion(questions);
138
142
  return (_jsx(MoversAnswerTheQuestionRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, audioUrl: part.audioUrl, title: data.title, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
139
143
  }
144
+ case 'ANSWER_THE_QUESTION_GROUP': {
145
+ const data = transformAnswerTheQuestionGroup(questions);
146
+ return (_jsx(MoversAnswerTheQuestionRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
147
+ }
140
148
  case 'TRUE_FALSE_GROUP': {
141
149
  const data = transformTrueFalseGroup(questions);
142
150
  return (_jsx(MoversTrueFalseRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import type { MoversCrosswordPuzzleRendererProps } from '../../types';
3
+ export declare function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCount, instruction, title, gridSize, clues, answers, correctAnswers, onAnswerChange, isReviewMode, caseSensitive, }: MoversCrosswordPuzzleRendererProps): React.JSX.Element;
@@ -0,0 +1,285 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState, useMemo, useRef, useEffect } from 'react';
4
+ import { Check, X } from 'lucide-react';
5
+ import CambridgeYlePartBanner from '../../../../themes/cambridge-yle/CambridgeYlePartBanner';
6
+ import CambridgeYleInstructionBanner from '../../../../themes/cambridge-yle/CambridgeYleInstructionBanner';
7
+ export function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCount, instruction, title, gridSize, clues, answers = {}, correctAnswers = {}, onAnswerChange, isReviewMode = false, caseSensitive = false, }) {
8
+ const rows = gridSize?.rows || 12;
9
+ const cols = gridSize?.cols || 12;
10
+ const acrossClues = useMemo(() => clues?.across || [], [clues?.across]);
11
+ const downClues = useMemo(() => clues?.down || [], [clues?.down]);
12
+ const [activeDirection, setActiveDirection] = useState('across');
13
+ const [activeClueNumber, setActiveClueNumber] = useState(() => {
14
+ return acrossClues[0]?.number || downClues[0]?.number || 1;
15
+ });
16
+ const [selectedCell, setSelectedCell] = useState(null);
17
+ const inputRefs = useRef(new Map());
18
+ const { cellMap, clueNumberMap, wordCellsMap } = useMemo(() => {
19
+ const cMap = new Map();
20
+ const numMap = new Map();
21
+ const wMap = new Map();
22
+ for (const c of acrossClues) {
23
+ numMap.set(`${c.startRow}-${c.startCol}`, c.number);
24
+ const cells = [];
25
+ for (let i = 0; i < c.length; i++) {
26
+ const key = `${c.startRow}-${c.startCol + i}`;
27
+ cells.push({ r: c.startRow, c: c.startCol + i });
28
+ const existing = cMap.get(key) || {};
29
+ cMap.set(key, { ...existing, across: c });
30
+ }
31
+ wMap.set(`across-${c.number}`, cells);
32
+ }
33
+ for (const c of downClues) {
34
+ if (!numMap.has(`${c.startRow}-${c.startCol}`)) {
35
+ numMap.set(`${c.startRow}-${c.startCol}`, c.number);
36
+ }
37
+ const cells = [];
38
+ for (let i = 0; i < c.length; i++) {
39
+ const key = `${c.startRow + i}-${c.startCol}`;
40
+ cells.push({ r: c.startRow + i, c: c.startCol });
41
+ const existing = cMap.get(key) || {};
42
+ cMap.set(key, { ...existing, down: c });
43
+ }
44
+ wMap.set(`down-${c.number}`, cells);
45
+ }
46
+ return { cellMap: cMap, clueNumberMap: numMap, wordCellsMap: wMap };
47
+ }, [acrossClues, downClues]);
48
+ useEffect(() => {
49
+ if (!selectedCell) {
50
+ const firstClue = acrossClues[0] || downClues[0];
51
+ if (firstClue) {
52
+ setSelectedCell({ r: firstClue.startRow, c: firstClue.startCol });
53
+ }
54
+ }
55
+ }, [acrossClues, downClues, selectedCell]);
56
+ const activeWordCells = useMemo(() => {
57
+ return wordCellsMap.get(`${activeDirection}-${activeClueNumber}`) || [];
58
+ }, [wordCellsMap, activeDirection, activeClueNumber]);
59
+ const handleSelectClue = (number, direction) => {
60
+ setActiveDirection(direction);
61
+ setActiveClueNumber(number);
62
+ const clueList = direction === 'across' ? acrossClues : downClues;
63
+ const clue = clueList.find((c) => c.number === number);
64
+ if (clue) {
65
+ setSelectedCell({ r: clue.startRow, c: clue.startCol });
66
+ const key = `${clue.startRow}-${clue.startCol}`;
67
+ inputRefs.current.get(key)?.focus();
68
+ }
69
+ };
70
+ const handleCellClick = (r, c) => {
71
+ const key = `${r}-${c}`;
72
+ const cellInfo = cellMap.get(key);
73
+ if (!cellInfo)
74
+ return;
75
+ if (selectedCell?.r === r && selectedCell?.c === c && cellInfo.across && cellInfo.down) {
76
+ const newDir = activeDirection === 'across' ? 'down' : 'across';
77
+ setActiveDirection(newDir);
78
+ const newClue = newDir === 'across' ? cellInfo.across : cellInfo.down;
79
+ if (newClue)
80
+ setActiveClueNumber(newClue.number);
81
+ }
82
+ else {
83
+ if (activeDirection === 'across' && cellInfo.across) {
84
+ setActiveClueNumber(cellInfo.across.number);
85
+ }
86
+ else if (activeDirection === 'down' && cellInfo.down) {
87
+ setActiveClueNumber(cellInfo.down.number);
88
+ }
89
+ else if (cellInfo.across) {
90
+ setActiveDirection('across');
91
+ setActiveClueNumber(cellInfo.across.number);
92
+ }
93
+ else if (cellInfo.down) {
94
+ setActiveDirection('down');
95
+ setActiveClueNumber(cellInfo.down.number);
96
+ }
97
+ setSelectedCell({ r, c });
98
+ }
99
+ };
100
+ const moveToNextCell = (r, c) => {
101
+ const cells = activeWordCells;
102
+ const currIdx = cells.findIndex((cell) => cell.r === r && cell.c === c);
103
+ if (currIdx >= 0 && currIdx < cells.length - 1) {
104
+ const next = cells[currIdx + 1];
105
+ setSelectedCell(next);
106
+ inputRefs.current.get(`${next.r}-${next.c}`)?.focus();
107
+ }
108
+ };
109
+ const moveToPrevCell = (r, c) => {
110
+ const cells = activeWordCells;
111
+ const currIdx = cells.findIndex((cell) => cell.r === r && cell.c === c);
112
+ if (currIdx > 0) {
113
+ const prev = cells[currIdx - 1];
114
+ setSelectedCell(prev);
115
+ inputRefs.current.get(`${prev.r}-${prev.c}`)?.focus();
116
+ }
117
+ };
118
+ const handleKeyDown = (e, r, c) => {
119
+ if (isReviewMode)
120
+ return;
121
+ if (e.key === 'Backspace') {
122
+ const key = `${r}-${c}`;
123
+ if (!answers[key]) {
124
+ e.preventDefault();
125
+ moveToPrevCell(r, c);
126
+ const cells = activeWordCells;
127
+ const currIdx = cells.findIndex((cell) => cell.r === r && cell.c === c);
128
+ if (currIdx > 0) {
129
+ const prev = cells[currIdx - 1];
130
+ onAnswerChange(`${prev.r}-${prev.c}`, '');
131
+ }
132
+ }
133
+ else {
134
+ onAnswerChange(key, '');
135
+ }
136
+ }
137
+ else if (e.key === 'ArrowRight') {
138
+ e.preventDefault();
139
+ if (activeDirection !== 'across') {
140
+ setActiveDirection('across');
141
+ const info = cellMap.get(`${r}-${c}`);
142
+ if (info?.across)
143
+ setActiveClueNumber(info.across.number);
144
+ }
145
+ else {
146
+ moveToNextCell(r, c);
147
+ }
148
+ }
149
+ else if (e.key === 'ArrowLeft') {
150
+ e.preventDefault();
151
+ if (activeDirection !== 'across') {
152
+ setActiveDirection('across');
153
+ const info = cellMap.get(`${r}-${c}`);
154
+ if (info?.across)
155
+ setActiveClueNumber(info.across.number);
156
+ }
157
+ else {
158
+ moveToPrevCell(r, c);
159
+ }
160
+ }
161
+ else if (e.key === 'ArrowDown') {
162
+ e.preventDefault();
163
+ if (activeDirection !== 'down') {
164
+ setActiveDirection('down');
165
+ const info = cellMap.get(`${r}-${c}`);
166
+ if (info?.down)
167
+ setActiveClueNumber(info.down.number);
168
+ }
169
+ else {
170
+ moveToNextCell(r, c);
171
+ }
172
+ }
173
+ else if (e.key === 'ArrowUp') {
174
+ e.preventDefault();
175
+ if (activeDirection !== 'down') {
176
+ setActiveDirection('down');
177
+ const info = cellMap.get(`${r}-${c}`);
178
+ if (info?.down)
179
+ setActiveClueNumber(info.down.number);
180
+ }
181
+ else {
182
+ moveToPrevCell(r, c);
183
+ }
184
+ }
185
+ };
186
+ const handleInputChange = (val, r, c) => {
187
+ if (isReviewMode)
188
+ return;
189
+ const cleanVal = val.slice(-1).toUpperCase();
190
+ const key = `${r}-${c}`;
191
+ onAnswerChange(key, cleanVal);
192
+ if (cleanVal) {
193
+ moveToNextCell(r, c);
194
+ }
195
+ };
196
+ const getCellReviewStatus = (r, c) => {
197
+ if (!isReviewMode)
198
+ return null;
199
+ const key = `${r}-${c}`;
200
+ const userLetter = (answers[key] || '').trim();
201
+ const cellInfo = cellMap.get(key);
202
+ if (!cellInfo)
203
+ return null;
204
+ let expectedLetter = '';
205
+ if (cellInfo.across && correctAnswers[`across-${cellInfo.across.number}`]) {
206
+ const word = correctAnswers[`across-${cellInfo.across.number}`];
207
+ const offset = c - cellInfo.across.startCol;
208
+ expectedLetter = word[offset] || '';
209
+ }
210
+ else if (cellInfo.down && correctAnswers[`down-${cellInfo.down.number}`]) {
211
+ const word = correctAnswers[`down-${cellInfo.down.number}`];
212
+ const offset = r - cellInfo.down.startRow;
213
+ expectedLetter = word[offset] || '';
214
+ }
215
+ if (!expectedLetter)
216
+ return null;
217
+ const isMatch = caseSensitive
218
+ ? userLetter === expectedLetter
219
+ : userLetter.toLowerCase() === expectedLetter.toLowerCase();
220
+ return { isCorrect: isMatch, expected: expectedLetter, user: userLetter };
221
+ };
222
+ return (_jsxs("div", { className: "flex h-full flex-col gap-4 overflow-y-auto p-3 sm:gap-5 sm:p-4 md:gap-6 md:p-6", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), title && (_jsx("h3", { className: "text-center text-lg font-bold text-gray-800 tracking-wide", children: title })), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-6 items-start", children: [_jsxs("div", { className: "lg:col-span-5 flex flex-col gap-4", children: [_jsxs("div", { className: "rounded-xl border-2 border-blue-200 bg-blue-50/40 p-4 shadow-sm", children: [_jsxs("h4", { className: "font-bold text-blue-900 mb-3 flex items-center justify-between", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-blue-600 text-white text-xs font-bold uppercase", children: "Across" }), _jsx("span", { children: "H\u00E0ng ngang" })] }), _jsxs("span", { className: "text-xs text-blue-600 font-medium", children: ["(", acrossClues.length, " c\u00E2u)"] })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-72 overflow-y-auto pr-1", children: acrossClues.map((c) => {
223
+ const isActive = activeDirection === 'across' && activeClueNumber === c.number;
224
+ const answerKey = `across-${c.number}`;
225
+ const expectedWord = correctAnswers[answerKey];
226
+ let userWord = '';
227
+ for (let i = 0; i < c.length; i++) {
228
+ userWord += (answers[`${c.startRow}-${c.startCol + i}`] || '').trim();
229
+ }
230
+ const isFilled = userWord.length === c.length;
231
+ return (_jsxs("button", { type: "button", onClick: () => handleSelectClue(c.number, 'across'), className: `flex items-start gap-2.5 p-2.5 rounded-lg border text-left transition ${isActive
232
+ ? 'border-blue-600 bg-blue-100/90 shadow-sm ring-1 ring-blue-500'
233
+ : 'border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50/60'}`, children: [_jsx("span", { className: `flex items-center justify-center h-6 w-6 rounded-md text-xs font-bold ${isActive ? 'bg-blue-600 text-white' : 'bg-blue-100 text-blue-800'}`, children: c.number }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: c.clue }), isReviewMode && expectedWord && (_jsxs("div", { className: "text-xs mt-1 flex items-center gap-2", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "font-bold text-green-700", children: expectedWord }), userWord && userWord !== expectedWord && (_jsx("span", { className: "line-through text-red-500 font-semibold", children: userWord }))] }))] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("span", { className: "text-xs text-gray-400 font-medium", children: ["(", c.length, ")"] }), isFilled && !isReviewMode && (_jsx("span", { className: "h-2 w-2 rounded-full bg-blue-500" }))] })] }, `across-${c.number}`));
234
+ }) })] }), _jsxs("div", { className: "rounded-xl border-2 border-purple-200 bg-purple-50/40 p-4 shadow-sm", children: [_jsxs("h4", { className: "font-bold text-purple-900 mb-3 flex items-center justify-between", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-purple-600 text-white text-xs font-bold uppercase", children: "Down" }), _jsx("span", { children: "H\u00E0ng d\u1ECDc" })] }), _jsxs("span", { className: "text-xs text-purple-600 font-medium", children: ["(", downClues.length, " c\u00E2u)"] })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-72 overflow-y-auto pr-1", children: downClues.map((c) => {
235
+ const isActive = activeDirection === 'down' && activeClueNumber === c.number;
236
+ const answerKey = `down-${c.number}`;
237
+ const expectedWord = correctAnswers[answerKey];
238
+ let userWord = '';
239
+ for (let i = 0; i < c.length; i++) {
240
+ userWord += (answers[`${c.startRow + i}-${c.startCol}`] || '').trim();
241
+ }
242
+ const isFilled = userWord.length === c.length;
243
+ return (_jsxs("button", { type: "button", onClick: () => handleSelectClue(c.number, 'down'), className: `flex items-start gap-2.5 p-2.5 rounded-lg border text-left transition ${isActive
244
+ ? 'border-purple-600 bg-purple-100/90 shadow-sm ring-1 ring-purple-500'
245
+ : 'border-gray-200 bg-white hover:border-purple-300 hover:bg-purple-50/60'}`, children: [_jsx("span", { className: `flex items-center justify-center h-6 w-6 rounded-md text-xs font-bold ${isActive ? 'bg-purple-600 text-white' : 'bg-purple-100 text-purple-800'}`, children: c.number }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: c.clue }), isReviewMode && expectedWord && (_jsxs("div", { className: "text-xs mt-1 flex items-center gap-2", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "font-bold text-green-700", children: expectedWord }), userWord && userWord !== expectedWord && (_jsx("span", { className: "line-through text-red-500 font-semibold", children: userWord }))] }))] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("span", { className: "text-xs text-gray-400 font-medium", children: ["(", c.length, ")"] }), isFilled && !isReviewMode && (_jsx("span", { className: "h-2 w-2 rounded-full bg-purple-500" }))] })] }, `down-${c.number}`));
246
+ }) })] })] }), _jsxs("div", { className: "lg:col-span-7 flex flex-col items-center gap-4 bg-gray-50/80 p-4 sm:p-6 rounded-2xl border-2 border-gray-200 shadow-sm", children: [!isReviewMode && (_jsxs("div", { className: "flex items-center gap-3 text-xs font-semibold text-gray-600 bg-white px-3 py-1.5 rounded-full border border-gray-200 shadow-sm", children: [_jsx("span", { children: "\u0110ang ch\u1ECDn:" }), _jsx("span", { className: "flex items-center gap-1 text-blue-700", children: activeDirection === 'across' ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "font-bold", children: ["Across ", activeClueNumber] }), _jsx("span", { className: "text-gray-400 font-normal", children: "(Ngang)" })] })) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "font-bold", children: ["Down ", activeClueNumber] }), _jsx("span", { className: "text-gray-400 font-normal", children: "(D\u1ECDc)" })] })) }), _jsx("span", { className: "text-gray-300", children: "|" }), _jsx("span", { className: "text-gray-500 font-normal", children: "Click \u0111\u00FAp \u00F4 giao nhau \u0111\u1EC3 \u0111\u1ED5i chi\u1EC1u" })] })), _jsx("div", { className: "overflow-x-auto max-w-full p-2", children: _jsx("div", { className: "inline-grid gap-1 sm:gap-1.5", style: {
247
+ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
248
+ }, children: Array.from({ length: rows }).map((_, rIdx) => {
249
+ const r = rIdx + 1;
250
+ return Array.from({ length: cols }).map((_, cIdx) => {
251
+ const c = cIdx + 1;
252
+ const key = `${r}-${c}`;
253
+ const cellInfo = cellMap.get(key);
254
+ const clueNum = clueNumberMap.get(key);
255
+ if (!cellInfo) {
256
+ return (_jsx("div", { className: "h-8 w-8 sm:h-10 sm:w-10 md:h-11 md:w-11 bg-transparent" }, key));
257
+ }
258
+ const isSelected = selectedCell?.r === r && selectedCell?.c === c;
259
+ const isWordActive = activeWordCells.some((cell) => cell.r === r && cell.c === c);
260
+ const reviewStatus = getCellReviewStatus(r, c);
261
+ const cellValue = answers[key] || '';
262
+ let cellStyle = 'border-gray-400 bg-white text-gray-800';
263
+ if (isReviewMode && reviewStatus) {
264
+ if (reviewStatus.isCorrect) {
265
+ cellStyle = 'border-green-500 bg-green-100 text-green-700 font-bold';
266
+ }
267
+ else {
268
+ cellStyle = 'border-red-500 bg-red-100 text-red-700 font-bold';
269
+ }
270
+ }
271
+ else if (isSelected) {
272
+ cellStyle = 'border-blue-600 bg-blue-100 ring-2 ring-blue-500 text-blue-900 font-bold';
273
+ }
274
+ else if (isWordActive) {
275
+ cellStyle = 'border-blue-400 bg-blue-50 text-blue-900';
276
+ }
277
+ return (_jsxs("div", { onClick: () => handleCellClick(r, c), className: `relative flex items-center justify-center rounded-lg border-2 font-bold shadow-sm h-8 w-8 sm:h-10 sm:w-10 md:h-11 md:w-11 text-base sm:text-lg transition-all select-none cursor-pointer ${cellStyle}`, children: [clueNum && (_jsx("span", { className: "absolute top-0.5 left-1 text-[9px] sm:text-[10px] font-bold text-gray-500 leading-none", children: clueNum })), !isReviewMode ? (_jsx("input", { ref: (el) => {
278
+ if (el)
279
+ inputRefs.current.set(key, el);
280
+ else
281
+ inputRefs.current.delete(key);
282
+ }, type: "text", maxLength: 1, value: cellValue, onChange: (e) => handleInputChange(e.target.value, r, c), onKeyDown: (e) => handleKeyDown(e, r, c), onFocus: () => handleCellClick(r, c), className: "w-full h-full text-center bg-transparent uppercase font-bold outline-none cursor-pointer" })) : (_jsxs("div", { className: "flex flex-col items-center justify-center", children: [_jsx("span", { children: cellValue || '—' }), reviewStatus && !reviewStatus.isCorrect && (_jsx("span", { className: "absolute -bottom-1 -right-1 text-[9px] bg-green-600 text-white font-bold px-1 rounded-full shadow", children: reviewStatus.expected }))] })), isReviewMode && reviewStatus && (_jsx("span", { className: `absolute -top-1 -right-1 rounded-full p-0.5 text-white ${reviewStatus.isCorrect ? 'bg-green-500' : 'bg-red-500'}`, children: reviewStatus.isCorrect ? (_jsx(Check, { className: "h-2.5 w-2.5" })) : (_jsx(X, { className: "h-2.5 w-2.5" })) }))] }, key));
283
+ });
284
+ }) }) })] })] })] }));
285
+ }
@@ -18,4 +18,5 @@ export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRender
18
18
  export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
19
19
  export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
20
20
  export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
21
+ export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
21
22
  export { ArticlesViewer } from './ArticlesViewer';
@@ -22,4 +22,5 @@ export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRender
22
22
  export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
23
23
  export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
24
24
  export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
25
+ export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
25
26
  export { ArticlesViewer } from './ArticlesViewer';
@@ -287,3 +287,33 @@ export interface MoversGridFillRendererProps {
287
287
  caseSensitive?: boolean;
288
288
  disabledCells?: string[];
289
289
  }
290
+ export interface CrosswordClueItem {
291
+ number: number;
292
+ clue: string;
293
+ startRow: number;
294
+ startCol: number;
295
+ length: number;
296
+ isExample?: boolean;
297
+ }
298
+ export interface MoversCrosswordPuzzleRendererProps {
299
+ partNumber: number;
300
+ partName?: string;
301
+ questionCount: number;
302
+ instruction: string;
303
+ title?: string;
304
+ gridSize: {
305
+ rows: number;
306
+ cols: number;
307
+ };
308
+ markedColumn?: number;
309
+ hiddenWord?: string;
310
+ clues: {
311
+ across: CrosswordClueItem[];
312
+ down: CrosswordClueItem[];
313
+ };
314
+ answers: Record<string, string>;
315
+ correctAnswers?: Record<string, string>;
316
+ onAnswerChange: (key: string, value: string) => void;
317
+ isReviewMode?: boolean;
318
+ caseSensitive?: boolean;
319
+ }