@tinyweb_dev/oe-exam-sdk 1.0.13 → 1.0.14

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 (33) hide show
  1. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.js +3 -1
  2. package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +3 -1
  3. package/dist/components/questions/_shared/components/compact/KidCollapsibleBlock.d.ts +8 -0
  4. package/dist/components/questions/_shared/components/compact/KidCollapsibleBlock.js +16 -0
  5. package/dist/components/questions/_shared/components/compact/KidPassage.d.ts +2 -1
  6. package/dist/components/questions/_shared/components/compact/KidPassage.js +2 -2
  7. package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +1 -1
  8. package/dist/components/questions/_shared/components/compact/index.d.ts +2 -0
  9. package/dist/components/questions/_shared/components/compact/index.js +2 -0
  10. package/dist/components/questions/_shared/components/compact/reviewItemCard.d.ts +3 -0
  11. package/dist/components/questions/_shared/components/compact/reviewItemCard.js +11 -0
  12. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +3 -3
  13. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +2 -2
  14. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +19 -8
  15. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +2 -2
  16. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +2 -2
  17. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +2 -2
  18. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +53 -50
  19. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +2 -2
  20. package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +8 -2
  21. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +5 -3
  22. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +18 -15
  23. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +2 -2
  24. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +2 -2
  25. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +2 -2
  26. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +16 -8
  27. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +2 -2
  28. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +2 -2
  29. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +3 -1
  30. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +4 -2
  31. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +2 -2
  32. package/dist/components/questions/viewer/QuestionViewer.js +2 -1
  33. package/package.json +1 -1
@@ -6,6 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
7
7
  import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
8
8
  import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
9
+ import { COLLAPSE_CHAR_THRESHOLD, KidCollapsibleBlock, } from '../../../../questions/_shared/components/compact';
9
10
  // Single Question Component (1 Line / Full Width Row)
10
11
  function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctAnswerTextMap, id, }) {
11
12
  const isDisabled = isReviewMode || question.isExample;
@@ -47,5 +48,6 @@ export function MoversTrueFalseRenderer({ partNumber, partName, questionCount, i
47
48
  // Separate example questions from regular questions
48
49
  const exampleQuestions = questions.filter((q) => q.isExample);
49
50
  const regularQuestions = questions.filter((q) => !q.isExample);
50
- return (_jsxs("div", { className: "custom-scrollbar flex h-full flex-col gap-4 overflow-y-auto sm:gap-5 pb-6", children: [_jsxs("div", { className: "flex flex-col gap-3.5 p-3 sm:px-6 sm:pt-2", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), imageUrl && (_jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration", className: "max-h-[300px] rounded-xl object-contain shadow-md" }) })), groupContent && (_jsx("div", { className: "rounded-xl border border-amber-200 bg-amber-50/60 p-4 text-[15.5px] leading-relaxed text-slate-800 shadow-sm", dangerouslySetInnerHTML: { __html: groupContent } })), exampleQuestions.length > 0 && (_jsx("div", { className: "mt-1 flex flex-col gap-3", children: exampleQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswerTextMap: correctAnswerTextMap }, question.id))) }))] }), exampleQuestions.length > 0 && regularQuestions.length > 0 && (_jsx("div", { className: "px-3 sm:px-6", children: _jsx("hr", { className: "border-t border-dashed border-slate-200" }) })), _jsx("div", { className: "flex flex-col gap-3 px-3 sm:px-6", children: regularQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswerTextMap: correctAnswerTextMap }, question.id))) })] }));
51
+ return (_jsxs("div", { className: "custom-scrollbar flex h-full flex-col gap-4 overflow-y-auto sm:gap-5 pb-6", children: [_jsxs("div", { className: "flex flex-col gap-3.5 p-3 sm:px-6 sm:pt-2", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), imageUrl && (_jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration", className: "max-h-[300px] rounded-xl object-contain shadow-md" }) })), groupContent && (_jsx(KidCollapsibleBlock, { collapse: isReviewMode &&
52
+ groupContent.replace(/<[^>]+>/g, ' ').length > COLLAPSE_CHAR_THRESHOLD, children: _jsx("div", { className: "rounded-xl border border-amber-200 bg-amber-50/60 p-4 text-[15.5px] leading-relaxed text-slate-800 shadow-sm", dangerouslySetInnerHTML: { __html: groupContent } }) })), exampleQuestions.length > 0 && (_jsx("div", { className: "mt-1 flex flex-col gap-3", children: exampleQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswerTextMap: correctAnswerTextMap }, question.id))) }))] }), exampleQuestions.length > 0 && regularQuestions.length > 0 && (_jsx("div", { className: "px-3 sm:px-6", children: _jsx("hr", { className: "border-t border-dashed border-slate-200" }) })), _jsx("div", { className: "flex flex-col gap-3 px-3 sm:px-6", children: regularQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswerTextMap: correctAnswerTextMap }, question.id))) })] }));
51
53
  }
@@ -6,6 +6,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
7
7
  import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
8
8
  import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
9
+ import { COLLAPSE_CHAR_THRESHOLD, KidCollapsibleBlock, } from '../../../../questions/_shared/components/compact';
9
10
  export function MoversWriteSentencesRenderer({ partNumber, partName, questionCount, instruction, imageUrl, title, groupContent, questions, answers, onAnswerChange, isReviewMode = false, }) {
10
- return (_jsxs("div", { className: `grid h-full grid-cols-1 gap-4 sm:gap-6 ${!isReviewMode ? 'lg:grid-cols-2' : ''}`, children: [_jsxs("div", { className: "flex flex-col gap-4 overflow-y-auto p-3 sm:pb-6 sm:pr-6 sm:pt-6", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), imageUrl && (_jsx("div", { className: "flex items-center justify-center", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Illustration", className: "max-h-[300px] rounded-lg object-contain shadow-md" }) })), groupContent && (_jsx("div", { className: "rounded-lg bg-amber-50 p-4 text-sm leading-relaxed text-gray-700", children: groupContent }))] }), _jsxs("div", { className: "flex flex-col gap-4 overflow-y-auto p-3 sm:gap-5 sm:pb-6 sm:pl-6 sm:pt-6", children: [title && (_jsx("h2", { className: "text-center text-2xl font-bold uppercase text-coral-500", children: title })), questions.map((question) => (_jsxs("div", { id: `question-${question.id}`, className: "flex items-start gap-3", children: [_jsx("span", { className: "flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-sm font-bold text-white", children: question.questionNumber }), _jsx("textarea", { value: answers[question.id] || '', onChange: (e) => onAnswerChange(question.id, e.target.value), disabled: isReviewMode, placeholder: "Write your sentence...", rows: 2, className: "flex-1 resize-none rounded-lg border-2 border-gray-300 bg-white px-3 py-2 text-base outline-none transition-colors focus:border-blue-500 disabled:opacity-60" })] }, question.id)))] })] }));
11
+ return (_jsxs("div", { className: `grid h-full grid-cols-1 gap-4 sm:gap-6 ${!isReviewMode ? 'lg:grid-cols-2' : ''}`, children: [_jsxs("div", { className: "flex flex-col gap-4 overflow-y-auto p-3 sm:pb-6 sm:pr-6 sm:pt-6", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), imageUrl && (_jsx("div", { className: "flex items-center justify-center", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Illustration", className: "max-h-[300px] rounded-lg object-contain shadow-md" }) })), groupContent && (_jsx(KidCollapsibleBlock, { collapse: isReviewMode &&
12
+ groupContent.replace(/<[^>]+>/g, ' ').length > COLLAPSE_CHAR_THRESHOLD, children: _jsx("div", { className: "rounded-lg bg-amber-50 p-4 text-sm leading-relaxed text-gray-700", children: groupContent }) }))] }), _jsxs("div", { className: "flex flex-col gap-4 overflow-y-auto p-3 sm:gap-5 sm:pb-6 sm:pl-6 sm:pt-6", children: [title && (_jsx("h2", { className: "text-center text-2xl font-bold uppercase text-coral-500", children: title })), questions.map((question) => (_jsxs("div", { id: `question-${question.id}`, className: "flex items-start gap-3", children: [_jsx("span", { className: "flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-sm font-bold text-white", children: question.questionNumber }), _jsx("textarea", { value: answers[question.id] || '', onChange: (e) => onAnswerChange(question.id, e.target.value), disabled: isReviewMode, placeholder: "Write your sentence...", rows: 2, className: "flex-1 resize-none rounded-lg border-2 border-gray-300 bg-white px-3 py-2 text-base outline-none transition-colors focus:border-blue-500 disabled:opacity-60" })] }, question.id)))] })] }));
11
13
  }
@@ -0,0 +1,8 @@
1
+ import { type ReactNode } from 'react';
2
+ export declare const COLLAPSE_CHAR_THRESHOLD = 480;
3
+ export declare function KidCollapsibleBlock({ collapse, children, className, previewClassName, }: {
4
+ collapse: boolean;
5
+ children: ReactNode;
6
+ className?: string;
7
+ previewClassName?: string;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { useT } from '../../../../../shared/lib/i18n';
5
+ import { cn } from '../../../../../shared/lib/utils';
6
+ export const COLLAPSE_CHAR_THRESHOLD = 480;
7
+ export function KidCollapsibleBlock({ collapse, children, className, previewClassName = 'max-h-40 overflow-hidden', }) {
8
+ const t = useT();
9
+ const [expanded, setExpanded] = useState(!collapse);
10
+ if (!collapse) {
11
+ return _jsx("div", { className: className, children: children });
12
+ }
13
+ return (_jsxs("div", { className: className, children: [_jsx("div", { className: cn(!expanded && previewClassName), children: children }), _jsx("button", { type: "button", onClick: () => setExpanded((value) => !value), className: "mt-3 text-sm font-semibold text-indigo-600 hover:text-indigo-800", children: expanded
14
+ ? t('question.viewer.collapsePassage')
15
+ : t('question.viewer.expandPassage') })] }));
16
+ }
@@ -1,4 +1,5 @@
1
- export declare function KidPassage({ html, className, }: {
1
+ export declare function KidPassage({ html, className, isReviewMode, }: {
2
2
  html: string;
3
3
  className?: string;
4
+ isReviewMode?: boolean;
4
5
  }): import("react").JSX.Element;
@@ -8,14 +8,14 @@ const COLLAPSE_CHAR_THRESHOLD = 480;
8
8
  function plainTextLength(html) {
9
9
  return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().length;
10
10
  }
11
- export function KidPassage({ html, className, }) {
11
+ export function KidPassage({ html, className, isReviewMode = false, }) {
12
12
  const t = useT();
13
13
  const isLong = plainTextLength(html) > COLLAPSE_CHAR_THRESHOLD;
14
14
  const [expanded, setExpanded] = useState(!isLong);
15
15
  if (!html) {
16
16
  return null;
17
17
  }
18
- return (_jsxs("div", { className: cn('rounded-xl border border-gray-200 bg-white p-5 shadow-xs', expanded && isLong && 'md:sticky md:top-24 md:z-10', className), 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: t('question.viewer.readingPassage') })] }), _jsx("div", { className: cn('prose prose-sm max-w-none leading-relaxed text-gray-800', !expanded && 'max-h-40 overflow-hidden'), dangerouslySetInnerHTML: { __html: html } }), isLong ? (_jsx("button", { type: "button", onClick: () => setExpanded((value) => !value), className: "mt-3 text-sm font-semibold text-indigo-600 hover:text-indigo-800", children: expanded
18
+ return (_jsxs("div", { className: cn('rounded-xl border border-gray-200 bg-white p-5 shadow-xs', expanded && isLong && !isReviewMode && 'md:sticky md:top-24 md:z-10', className), 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: t('question.viewer.readingPassage') })] }), _jsx("div", { className: cn('prose prose-sm max-w-none leading-relaxed text-gray-800', !expanded && 'max-h-40 overflow-hidden'), dangerouslySetInnerHTML: { __html: html } }), isLong ? (_jsx("button", { type: "button", onClick: () => setExpanded((value) => !value), className: "mt-3 text-sm font-semibold text-indigo-600 hover:text-indigo-800", children: expanded
19
19
  ? t('question.viewer.collapsePassage')
20
20
  : t('question.viewer.expandPassage') })) : null] }));
21
21
  }
@@ -5,5 +5,5 @@ import { useT } from '../../../../../shared/lib/i18n';
5
5
  import { cn } from '../../../../../shared/lib/utils';
6
6
  export function KidQuestionCard({ icon, hideIcon = false, title, titleClassName, points, isExample = false, review, headerExtra, children, className, contentClassName, }) {
7
7
  const t = useT();
8
- return (_jsxs("div", { className: cn('overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm', className), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [!hideIcon && (_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: icon ?? _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) })), _jsx("span", { className: cn('text-sm font-medium text-gray-700', titleClassName ?? 'truncate'), children: title }), isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), t('question.viewer.example')] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [headerExtra, typeof points === 'number' && (_jsx("span", { className: "text-xs text-gray-500", children: t('question.viewer.points', { count: points }) })), review === 'correct' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-semibold text-green-700", children: [_jsx(Check, { className: "h-3 w-3" }), t('question.viewer.correct')] })), review === 'wrong' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-700", children: [_jsx(X, { className: "h-3 w-3" }), t('question.viewer.wrong')] }))] })] }), _jsx("div", { className: cn('space-y-3 p-3', contentClassName), children: children })] }));
8
+ return (_jsxs("div", { className: cn('min-w-0 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm', className), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [!hideIcon && (_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: icon ?? _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) })), _jsx("span", { className: cn('text-sm font-medium text-gray-700', titleClassName ?? 'truncate'), children: title }), isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), t('question.viewer.example')] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [headerExtra, typeof points === 'number' && (_jsx("span", { className: "text-xs text-gray-500", children: t('question.viewer.points', { count: points }) })), review === 'correct' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-semibold text-green-700", children: [_jsx(Check, { className: "h-3 w-3" }), t('question.viewer.correct')] })), review === 'wrong' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-700", children: [_jsx(X, { className: "h-3 w-3" }), t('question.viewer.wrong')] }))] })] }), _jsx("div", { className: cn('space-y-3 p-3', contentClassName), children: children })] }));
9
9
  }
@@ -6,7 +6,9 @@ export { CompactFieldCard } from './CompactFieldCard';
6
6
  export { CompactExplanationToggle } from './CompactExplanationToggle';
7
7
  export { KidInstruction } from './KidInstruction';
8
8
  export { KidPassage } from './KidPassage';
9
+ export { KidCollapsibleBlock, COLLAPSE_CHAR_THRESHOLD } from './KidCollapsibleBlock';
9
10
  export { KidPrompt } from './KidPrompt';
10
11
  export { KidQuestionCard } from './KidQuestionCard';
12
+ export { reviewItemTitle, reviewItemPoints } from './reviewItemCard';
11
13
  export { KidChoiceRow, optionLetter } from './KidChoiceRow';
12
14
  export { SpeakingReplyMic } from './SpeakingReplyMic';
@@ -6,7 +6,9 @@ export { CompactFieldCard } from './CompactFieldCard';
6
6
  export { CompactExplanationToggle } from './CompactExplanationToggle';
7
7
  export { KidInstruction } from './KidInstruction';
8
8
  export { KidPassage } from './KidPassage';
9
+ export { KidCollapsibleBlock, COLLAPSE_CHAR_THRESHOLD } from './KidCollapsibleBlock';
9
10
  export { KidPrompt } from './KidPrompt';
10
11
  export { KidQuestionCard } from './KidQuestionCard';
12
+ export { reviewItemTitle, reviewItemPoints } from './reviewItemCard';
11
13
  export { KidChoiceRow, optionLetter } from './KidChoiceRow';
12
14
  export { SpeakingReplyMic } from './SpeakingReplyMic';
@@ -0,0 +1,3 @@
1
+ /** Review chrome: item index only — homework already shows Exercise N. */
2
+ export declare function reviewItemTitle(n: number, isReviewMode: boolean, takingLabel: string): string;
3
+ export declare function reviewItemPoints(isReviewMode: boolean, points?: number): number | undefined;
@@ -0,0 +1,11 @@
1
+ /** Review chrome: item index only — homework already shows Exercise N. */
2
+ export function reviewItemTitle(n, isReviewMode, takingLabel) {
3
+ if (isReviewMode)
4
+ return String(n);
5
+ return takingLabel;
6
+ }
7
+ export function reviewItemPoints(isReviewMode, points) {
8
+ if (isReviewMode)
9
+ return undefined;
10
+ return points;
11
+ }
@@ -4,7 +4,7 @@ import { CircleHelp, Lightbulb, Sparkles, Volume2 } from 'lucide-react';
4
4
  import { useT } from '../../../../shared/lib/i18n';
5
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
6
  import { toAnswerTheQuestionGroupImageUrls } from '../../_shared/types/answer-the-question-group.type';
7
- import { KidInstruction, KidPassage, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
7
+ import { KidInstruction, KidPassage, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
8
8
  function isAbsoluteMediaUrl(value) {
9
9
  return (value.startsWith('http://')
10
10
  || value.startsWith('https://')
@@ -46,14 +46,14 @@ export function AnswerTheQuestionGroupClient({ questionData, isReviewMode = fals
46
46
  const items = questionData.items || [];
47
47
  const instruction = questionData.instruction?.trim()
48
48
  || t('question.viewer.atqgDefaultInstruction');
49
- return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: instruction }), questionData.audioUrl && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: t('question.viewer.imageAlt', { n: imageIndex + 1 }) }, `group-image-${imageIndex}`))) })), questionData.passage ? _jsx(KidPassage, { html: questionData.passage }) : null, items.map((item, itemIndex) => {
49
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: instruction }), questionData.audioUrl && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: t('question.viewer.imageAlt', { n: imageIndex + 1 }) }, `group-image-${imageIndex}`))) })), questionData.passage ? (_jsx(KidPassage, { html: questionData.passage, isReviewMode: isReviewMode })) : null, items.map((item, itemIndex) => {
50
50
  const userValue = userAnswers[itemIndex] || '';
51
51
  const expectedAnswers = Array.isArray(item.expectedAnswers)
52
52
  ? item.expectedAnswers.filter((answer) => answer.trim() !== '')
53
53
  : [];
54
54
  const questionNumber = item.questionNumber || itemIndex + 1;
55
55
  const answerId = `atqg-answer-${questionNumber}`;
56
- return (_jsxs(KidQuestionCard, { icon: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }), title: t('question.viewer.questionN', { n: questionNumber }), points: item.points || 0, isExample: item.isExample, headerExtra: _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"] }), children: [toAnswerTheQuestionGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: t('question.viewer.itemImageAlt', {
56
+ return (_jsxs(KidQuestionCard, { icon: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }), title: reviewItemTitle(questionNumber, isReviewMode, t('question.viewer.questionN', { n: questionNumber })), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, headerExtra: _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"] }), children: [toAnswerTheQuestionGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: t('question.viewer.itemImageAlt', {
57
57
  n: questionNumber,
58
58
  m: imageIndex + 1,
59
59
  }) }, `${item.questionNumber}-${imageIndex}`))) })), _jsx(KidPrompt, { children: _jsx("label", { htmlFor: answerId, children: item.question || t('question.viewer.missingPrompt') }) }), isReviewMode || item.isExample || !onAnswerChange ? (userValue ? (_jsx("div", { className: "rounded-md border border-gray-200 bg-gray-50 px-3 py-1.5 text-sm text-gray-700", children: userValue })) : null) : (_jsx("textarea", { id: answerId, name: answerId, value: userValue, onChange: (event) => {
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useState, useEffect, useRef } from 'react';
4
4
  import { Check, X } from 'lucide-react';
5
- import { KidQuestionCard } from '../../_shared/components/compact';
5
+ import { KidQuestionCard, reviewItemTitle } from '../../_shared/components/compact';
6
6
  // Fisher-Yates shuffle algorithm
7
7
  function shuffleArray(array) {
8
8
  const shuffled = [...array];
@@ -128,7 +128,7 @@ export function ArrangeLettersIntoWordsClient({ questionData, onSubmit, isReview
128
128
  const available = availableLetters[subQuestion.id] || [];
129
129
  const arranged = arrangedLetters[subQuestion.id] || [];
130
130
  const imagePreviewUrl = getImagePreviewUrl(subQuestion);
131
- return (_jsxs(KidQuestionCard, { title: `Câu ${sqIndex + 1}`, review: isReviewMode
131
+ return (_jsxs(KidQuestionCard, { title: reviewItemTitle(sqIndex + 1, isReviewMode, `Câu ${sqIndex + 1}`), review: isReviewMode
132
132
  ? isCorrect
133
133
  ? 'correct'
134
134
  : userAnswerValue.trim() !== ''
@@ -7,7 +7,7 @@ import { cn } from '../../../../shared/lib/utils';
7
7
  import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
8
8
  import { isRichTextEmpty, richTextToPlainText } from '../../../../shared/lib/rich-text';
9
9
  import { normalizeChooseAnswerCorrectAnswer, normalizeChooseAnswerOptions, optionToString } from './normalize';
10
- import { KidChoiceRow, KidPrompt, KidQuestionCard, optionLetter } from '../../_shared/components/compact';
10
+ import { COLLAPSE_CHAR_THRESHOLD, KidChoiceRow, KidCollapsibleBlock, KidPrompt, KidQuestionCard, optionLetter, reviewItemPoints } from '../../_shared/components/compact';
11
11
  // Helper component to resolve presigned URL for each image option
12
12
  function OptionImage({ src, alt, className }) {
13
13
  const { previewUrl, isLoading } = usePresignedFileUrl(src);
@@ -105,13 +105,24 @@ export function ChooseTheCorrectAnswerClient({ questionData, onSubmit, isReviewM
105
105
  : typeof questionData.glossary === 'string' && questionData.glossary.trim()
106
106
  ? [questionData.glossary]
107
107
  : [];
108
- return (_jsxs("div", { className: "space-y-4", children: [passageList.length > 0 && (_jsxs("div", { className: "rounded-xl border border-indigo-100 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-3 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: "space-y-3", children: passageList.map((p, i) => (_jsx("div", { className: "prose prose-sm max-w-none text-gray-800 leading-relaxed", dangerouslySetInnerHTML: { __html: p } }, i))) })] })), glossaryList.length > 0 && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50/60 p-4 shadow-xs", children: [_jsx("div", { className: "mb-2 text-xs font-bold uppercase tracking-wider text-amber-800", children: "T\u1EEB v\u1EF1ng (Glossary)" }), _jsx("div", { className: "space-y-1.5 text-xs text-amber-900 leading-relaxed", children: glossaryList.map((g, i) => (_jsx("div", { dangerouslySetInnerHTML: { __html: g } }, i))) })] })), questionData.isGroup && (questionData.groupTitle || questionData.groupSubtitle || questionData.groupContent || questionData.groupImageUrl || hasArticles) && (_jsxs("div", { className: "relative overflow-hidden rounded-2xl border border-indigo-100 bg-white shadow-sm ring-1 ring-black/5 transition-all", children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-br from-indigo-50/80 via-white to-blue-50/30 opacity-70 pointer-events-none" }), _jsx("div", { className: "absolute left-0 top-0 bottom-0 w-1.5 bg-gradient-to-b from-indigo-500 to-blue-600 rounded-l-2xl" }), _jsxs("div", { className: "relative p-6 sm:p-7", children: [_jsxs("div", { className: "flex items-start gap-4 mb-5 border-b border-indigo-50/60 pb-4", children: [_jsx("div", { className: "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-100 to-blue-100 shadow-inner ring-1 ring-indigo-200/50", children: _jsx(BookOpen, { className: "h-5 w-5 text-indigo-600" }) }), _jsxs("div", { className: "flex-1 pt-1", children: [_jsx("div", { className: "flex items-center gap-2 mb-1", children: questionData.groupNumber && (_jsxs("span", { className: "inline-flex items-center rounded-md bg-indigo-50 px-2 py-1 text-xs font-semibold text-indigo-700 ring-1 ring-inset ring-indigo-700/10", children: ["B\u00E0i \u0111\u1ECDc ", questionData.groupNumber] })) }), questionData.groupTitle && (_jsx("h4", { className: "text-xl font-bold text-slate-800 leading-tight tracking-tight", children: questionData.groupTitle })), questionData.groupSubtitle && (_jsx("p", { className: "mt-1 text-sm text-slate-500 leading-snug", children: questionData.groupSubtitle }))] })] }), hasArticles && (_jsx("div", { className: "flex flex-col gap-4", children: questionData.articles.map((article, i) => (_jsxs("div", { className: "group relative overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm transition-all hover:shadow-md hover:border-indigo-200", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-slate-100 bg-gradient-to-r from-indigo-50/60 to-blue-50/40 px-4 py-2.5", children: [_jsx("div", { className: "flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-blue-600 text-xs font-bold text-white shadow-sm", children: String.fromCharCode(65 + i) }), _jsx("h5", { className: "flex-1 truncate text-sm font-bold text-slate-800", children: article.name || `Bài viết ${i + 1}` })] }), _jsxs("div", { className: "flex flex-col gap-3 p-4 sm:flex-row sm:items-start", children: [article.imageUrl && (_jsx("div", { className: "flex flex-shrink-0 items-center justify-center rounded-md bg-slate-50 p-2 sm:w-40", children: _jsx(OptionImage, { src: article.imageUrl, alt: article.name || `Article ${i + 1}`, className: "max-h-40 w-auto rounded-md object-contain" }) })), article.content && (_jsx("div", { className: "prose prose-sm prose-slate max-w-none flex-1 text-[14px] leading-relaxed text-slate-700", children: article.content.split('\n').map((line, j) => (_jsx("p", { className: "mb-2 last:mb-0", children: line }, j))) }))] })] }, i))) })), !hasArticles && (_jsxs("div", { className: "space-y-6 text-slate-700", children: [questionData.groupContent && (_jsx("div", { className: "prose prose-slate prose-p:leading-relaxed prose-p:mb-3 max-w-none text-[15px]", dangerouslySetInnerHTML: {
109
- __html: questionData.groupContent
110
- .replace(/\{(\d+)\}/g, (_m, num) => `<span style="display:inline-block;min-width:100px;padding:1px 5px;margin:0 5px;background:#eef2ff;border-bottom:2px solid #6366f1;color:#4f46e5;font-weight:700;font-size:0.85em;text-align:center;vertical-align:baseline;letter-spacing:2px">———(${Number(num) + 1})———</span>`)
111
- .split('\n')
112
- .map((line) => `<p class="last:mb-0">${line}</p>`)
113
- .join(''),
114
- } })), questionData.groupImageUrl && (_jsxs("div", { className: "mt-5 relative overflow-hidden rounded-xl border border-slate-200/60 bg-slate-50 shadow-sm transition-all hover:shadow-md", children: [_jsx("div", { className: "flex items-center justify-between border-b border-slate-200/60 bg-slate-100/50 px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("span", { className: "ml-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: "T\u00E0i li\u1EC7u \u0111\u00EDnh k\u00E8m" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4 min-h-[160px]", children: _jsx(OptionImage, { src: questionData.groupImageUrl, alt: "Group illustration", className: "max-h-64 w-auto rounded-lg object-contain" }) })] }))] }))] })] })), _jsxs(KidQuestionCard, { title: isMultipleAnswers ? 'Chọn các đáp án đúng' : 'Chọn đáp án đúng', points: questionData.points, review: isReviewMode && hasAnswered ? (isAnswerCorrect ? 'correct' : 'wrong') : null, children: [(questionData.askerName || questionData.answererName) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-gray-50 p-2.5 text-xs text-gray-600", children: [_jsx(Users, { className: "h-4 w-4 text-gray-400" }), questionData.askerName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi h\u1ECFi:" }), " ", questionData.askerName] })), questionData.askerName && questionData.answererName && _jsx("span", { children: "\u2022" }), questionData.answererName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi:" }), " ", questionData.answererName] }))] })), showQuestion && (_jsx(KidPrompt, { children: isRichTextEmpty(questionData.question) ? (_jsx("h3", { className: "text-sm font-semibold text-gray-800", children: "C\u00E2u h\u1ECFi ch\u01B0a \u0111\u01B0\u1EE3c nh\u1EADp" })) : (_jsx(RichTextHtml, { html: questionData.question, className: "text-sm font-semibold text-gray-800" })) })), showImage && (imagePreviewUrl || cleanImageUrl) && (_jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2 w-2 rounded-full bg-red-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-green-400" }), _jsx("span", { className: "ml-2 text-xs font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4", children: isLoading ? (_jsx("div", { className: "flex h-48 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })) : (
108
+ const groupStemPlain = [
109
+ String(questionData.groupContent || ''),
110
+ ...(hasArticles
111
+ ? questionData.articles.map((article) => article.content || '')
112
+ : []),
113
+ ]
114
+ .join(' ')
115
+ .replace(/<[^>]+>/g, ' ');
116
+ const collapseGroupStem = isReviewMode && groupStemPlain.length > COLLAPSE_CHAR_THRESHOLD;
117
+ return (_jsxs("div", { className: "space-y-4", children: [passageList.length > 0 && (_jsxs("div", { className: "rounded-xl border border-indigo-100 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-3 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(KidCollapsibleBlock, { collapse: isReviewMode &&
118
+ passageList.join(' ').replace(/<[^>]+>/g, ' ').length >
119
+ COLLAPSE_CHAR_THRESHOLD, children: _jsx("div", { className: "space-y-3", children: passageList.map((p, i) => (_jsx("div", { className: "prose prose-sm max-w-none text-gray-800 leading-relaxed", dangerouslySetInnerHTML: { __html: p } }, i))) }) })] })), glossaryList.length > 0 && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50/60 p-4 shadow-xs", children: [_jsx("div", { className: "mb-2 text-xs font-bold uppercase tracking-wider text-amber-800", children: "T\u1EEB v\u1EF1ng (Glossary)" }), _jsx("div", { className: "space-y-1.5 text-xs text-amber-900 leading-relaxed", children: glossaryList.map((g, i) => (_jsx("div", { dangerouslySetInnerHTML: { __html: g } }, i))) })] })), questionData.isGroup && (questionData.groupTitle || questionData.groupSubtitle || questionData.groupContent || questionData.groupImageUrl || hasArticles) && (_jsxs("div", { className: "relative overflow-hidden rounded-2xl border border-indigo-100 bg-white shadow-sm ring-1 ring-black/5 transition-all", children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-br from-indigo-50/80 via-white to-blue-50/30 opacity-70 pointer-events-none" }), _jsx("div", { className: "absolute left-0 top-0 bottom-0 w-1.5 bg-gradient-to-b from-indigo-500 to-blue-600 rounded-l-2xl" }), _jsxs("div", { className: "relative p-6 sm:p-7", children: [_jsxs("div", { className: "flex items-start gap-4 mb-5 border-b border-indigo-50/60 pb-4", children: [_jsx("div", { className: "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-100 to-blue-100 shadow-inner ring-1 ring-indigo-200/50", children: _jsx(BookOpen, { className: "h-5 w-5 text-indigo-600" }) }), _jsxs("div", { className: "flex-1 pt-1", children: [_jsx("div", { className: "flex items-center gap-2 mb-1", children: questionData.groupNumber && (_jsxs("span", { className: "inline-flex items-center rounded-md bg-indigo-50 px-2 py-1 text-xs font-semibold text-indigo-700 ring-1 ring-inset ring-indigo-700/10", children: ["B\u00E0i \u0111\u1ECDc ", questionData.groupNumber] })) }), questionData.groupTitle && (_jsx("h4", { className: "text-xl font-bold text-slate-800 leading-tight tracking-tight", children: questionData.groupTitle })), questionData.groupSubtitle && (_jsx("p", { className: "mt-1 text-sm text-slate-500 leading-snug", children: questionData.groupSubtitle }))] })] }), _jsxs(KidCollapsibleBlock, { collapse: collapseGroupStem, children: [hasArticles && (_jsx("div", { className: "flex flex-col gap-4", children: questionData.articles.map((article, i) => (_jsxs("div", { className: "group relative overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm transition-all hover:shadow-md hover:border-indigo-200", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-slate-100 bg-gradient-to-r from-indigo-50/60 to-blue-50/40 px-4 py-2.5", children: [_jsx("div", { className: "flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-blue-600 text-xs font-bold text-white shadow-sm", children: String.fromCharCode(65 + i) }), _jsx("h5", { className: "flex-1 truncate text-sm font-bold text-slate-800", children: article.name || `Bài viết ${i + 1}` })] }), _jsxs("div", { className: "flex flex-col gap-3 p-4 sm:flex-row sm:items-start", children: [article.imageUrl && (_jsx("div", { className: "flex flex-shrink-0 items-center justify-center rounded-md bg-slate-50 p-2 sm:w-40", children: _jsx(OptionImage, { src: article.imageUrl, alt: article.name || `Article ${i + 1}`, className: "max-h-40 w-auto rounded-md object-contain" }) })), article.content && (_jsx("div", { className: "prose prose-sm prose-slate max-w-none flex-1 text-[14px] leading-relaxed text-slate-700", children: article.content.split('\n').map((line, j) => (_jsx("p", { className: "mb-2 last:mb-0", children: line }, j))) }))] })] }, i))) })), !hasArticles && (_jsxs("div", { className: "space-y-6 text-slate-700", children: [questionData.groupContent && (_jsx("div", { className: "prose prose-slate prose-p:leading-relaxed prose-p:mb-3 max-w-none text-[15px]", dangerouslySetInnerHTML: {
120
+ __html: questionData.groupContent
121
+ .replace(/\{(\d+)\}/g, (_m, num) => `<span style="display:inline-block;min-width:100px;padding:1px 5px;margin:0 5px;background:#eef2ff;border-bottom:2px solid #6366f1;color:#4f46e5;font-weight:700;font-size:0.85em;text-align:center;vertical-align:baseline;letter-spacing:2px">———(${Number(num) + 1})———</span>`)
122
+ .split('\n')
123
+ .map((line) => `<p class="last:mb-0">${line}</p>`)
124
+ .join(''),
125
+ } })), questionData.groupImageUrl && (_jsxs("div", { className: "mt-5 relative overflow-hidden rounded-xl border border-slate-200/60 bg-slate-50 shadow-sm transition-all hover:shadow-md", children: [_jsx("div", { className: "flex items-center justify-between border-b border-slate-200/60 bg-slate-100/50 px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("span", { className: "ml-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: "T\u00E0i li\u1EC7u \u0111\u00EDnh k\u00E8m" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4 min-h-[160px]", children: _jsx(OptionImage, { src: questionData.groupImageUrl, alt: "Group illustration", className: "max-h-64 w-auto rounded-lg object-contain" }) })] }))] }))] })] })] })), _jsxs(KidQuestionCard, { title: isMultipleAnswers ? 'Chọn các đáp án đúng' : 'Chọn đáp án đúng', points: reviewItemPoints(isReviewMode, questionData.points), review: isReviewMode && hasAnswered ? (isAnswerCorrect ? 'correct' : 'wrong') : null, children: [(questionData.askerName || questionData.answererName) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-gray-50 p-2.5 text-xs text-gray-600", children: [_jsx(Users, { className: "h-4 w-4 text-gray-400" }), questionData.askerName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi h\u1ECFi:" }), " ", questionData.askerName] })), questionData.askerName && questionData.answererName && _jsx("span", { children: "\u2022" }), questionData.answererName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi:" }), " ", questionData.answererName] }))] })), showQuestion && (_jsx(KidPrompt, { children: isRichTextEmpty(questionData.question) ? (_jsx("h3", { className: "text-sm font-semibold text-gray-800", children: "C\u00E2u h\u1ECFi ch\u01B0a \u0111\u01B0\u1EE3c nh\u1EADp" })) : (_jsx(RichTextHtml, { html: questionData.question, className: "text-sm font-semibold text-gray-800" })) })), showImage && (imagePreviewUrl || cleanImageUrl) && (_jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2 w-2 rounded-full bg-red-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-green-400" }), _jsx("span", { className: "ml-2 text-xs font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4", children: isLoading ? (_jsx("div", { className: "flex h-48 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })) : (
115
126
  /* eslint-disable-next-line @next/next/no-img-element */
116
127
  _jsx("img", { src: imagePreviewUrl || cleanImageUrl, alt: "Question illustration", className: "max-h-56 w-auto object-contain rounded-md", onError: (e) => {
117
128
  const target = e.target;
@@ -5,7 +5,7 @@ import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
5
5
  import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
6
6
  import { isRichTextEmpty } from '../../../../shared/lib/rich-text';
7
7
  import { getChooseAnswerGroupOptionImageSrc, toChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
8
- import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter } from '../../_shared/components/compact';
8
+ import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
9
9
  function ClientImageItem({ url, alt }) {
10
10
  const { previewUrl, isLoading } = usePresignedFileUrl(url);
11
11
  const src = previewUrl ||
@@ -38,7 +38,7 @@ export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode =
38
38
  return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), toChooseAnswerGroupImageUrls(questionData.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `group-audio-${audioIndex}`))), toChooseAnswerGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh nhóm (${imageIndex + 1})` }, `group-image-${imageIndex}`))) })), questionData.passage && (_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: "overflow-x-auto", children: _jsx(RichTextHtml, { html: questionData.passage, className: "text-gray-800 leading-relaxed" }) })] })), items.map((item, itemIndex) => {
39
39
  const selectedId = userAnswers[itemIndex];
40
40
  const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
41
- return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, children: [!isRichTextEmpty(item.question) && (_jsx(KidPrompt, { children: _jsx(RichTextHtml, { html: item.question, className: "text-sm font-semibold leading-relaxed text-gray-800" }) })), toChooseAnswerGroupImageUrls(item.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `${item.questionNumber}-audio-${audioIndex}`))), toChooseAnswerGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), _jsx("div", { className: "space-y-1.5", children: item.options.map((option, optionIndex) => {
41
+ return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, children: [!isRichTextEmpty(item.question) && (_jsx(KidPrompt, { children: _jsx(RichTextHtml, { html: item.question, className: "text-sm font-semibold leading-relaxed text-gray-800" }) })), toChooseAnswerGroupImageUrls(item.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `${item.questionNumber}-audio-${audioIndex}`))), toChooseAnswerGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), _jsx("div", { className: "space-y-1.5", children: item.options.map((option, optionIndex) => {
42
42
  const isCorrect = option.id === item.correctAnswer;
43
43
  const isSelected = option.id === selectedId;
44
44
  const optionImageSrc = getChooseAnswerGroupOptionImageSrc(option);
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Check, Lightbulb, X } from 'lucide-react';
4
4
  import { useT } from '../../../../shared/lib/i18n';
5
5
  import { cn } from '../../../../shared/lib/utils';
6
- import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
6
+ import { KidInstruction, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
7
7
  function emptyAnswer() {
8
8
  return { optionId: '', text: '' };
9
9
  }
@@ -43,7 +43,7 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
43
43
  text: next.text ?? current.text,
44
44
  });
45
45
  };
46
- return (_jsx(KidQuestionCard, { title: t('question.viewer.questionN', { n: item.questionNumber || itemIndex + 1 }), points: points, isExample: item.isExample, children: _jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "min-w-0 space-y-2.5", children: [(parts[0] || parts.slice(1).join('{0}')) ? (_jsxs("p", { className: "text-sm font-semibold leading-7 text-slate-800", children: [_jsx("span", { children: parts[0] || '' }), _jsx("span", { className: "mx-1 inline-block min-w-[3rem] border-b-2 border-dashed border-slate-300 align-baseline" }), _jsx("span", { children: parts.slice(1).join('{0}') })] })) : null, _jsx("div", { className: "flex flex-wrap items-center gap-2.5", role: "group", "aria-label": t('question.viewer.questionN', {
46
+ return (_jsx(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, t('question.viewer.questionN', { n: item.questionNumber || itemIndex + 1 })), points: reviewItemPoints(isReviewMode, points), isExample: item.isExample, children: _jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "min-w-0 space-y-2.5", children: [(parts[0] || parts.slice(1).join('{0}')) ? (_jsxs("p", { className: "text-sm font-semibold leading-7 text-slate-800", children: [_jsx("span", { children: parts[0] || '' }), _jsx("span", { className: "mx-1 inline-block min-w-[3rem] border-b-2 border-dashed border-slate-300 align-baseline" }), _jsx("span", { children: parts.slice(1).join('{0}') })] })) : null, _jsx("div", { className: "flex flex-wrap items-center gap-2.5", role: "group", "aria-label": t('question.viewer.questionN', {
47
47
  n: item.questionNumber || itemIndex + 1,
48
48
  }), children: item.options.map((option) => {
49
49
  const selected = current.optionId === option.id;
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Lightbulb } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
5
  import { tokenizeCrossOutQuestion } from './tokenize';
6
- import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
6
+ import { KidInstruction, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
7
7
  function normalizeWord(word) {
8
8
  return word.replace(/[^\p{L}\p{N}'-]/gu, '').toLowerCase();
9
9
  }
@@ -48,7 +48,7 @@ export function CrossOutWordGroupClient({ questionData, isReviewMode = false, us
48
48
  const selectedWord = userValue?.crossOut || '';
49
49
  const replaceWith = userValue?.replaceWith || '';
50
50
  const correct = item.correctAnswer;
51
- return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, headerExtra: isReviewMode ? (_jsx("span", { className: cn('rounded-full px-2 py-0.5 text-xs font-semibold', isReplace
51
+ return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, headerExtra: isReviewMode ? (_jsx("span", { className: cn('rounded-full px-2 py-0.5 text-xs font-semibold', isReplace
52
52
  ? 'bg-violet-100 text-violet-700'
53
53
  : 'bg-sky-100 text-sky-700'), children: isReplace ? 'Gạch + thay' : 'Chỉ gạch' })) : null, children: [isReplace ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-semibold text-slate-600", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: !canSelect, onChange: (event) => emit(itemIndex, {
54
54
  type: item.type,
@@ -32,7 +32,9 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
32
32
  }
33
33
  return { cellMap: cMap, clueNumberMap: numMap };
34
34
  }, [acrossClues, downClues]);
35
- return (_jsxs("div", { className: "flex flex-col items-stretch gap-3 text-left", children: [_jsxs("p", { className: "w-full text-left text-sm font-semibold text-gray-800", children: [_jsxs("span", { className: "font-bold", children: [displayNumber, "."] }), instructionText ? ` ${instructionText}` : ''] }), _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-lg border border-blue-200 bg-blue-50/50 p-4", children: [_jsxs("h4", { className: "font-bold text-blue-900 mb-2.5 flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-blue-600 text-white text-xs font-semibold", children: "Across" }), _jsx("span", { children: "H\u00E0ng ngang" })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-60 overflow-y-auto pr-1", children: acrossClues.map((c) => {
35
+ return (_jsxs("div", { className: "flex w-full min-w-0 flex-col items-stretch gap-3 text-left", children: [_jsxs("p", { className: "w-full text-left text-sm font-semibold text-gray-800", children: [!isReviewMode ? (_jsxs("span", { className: "font-bold", children: [displayNumber, "."] })) : null, instructionText
36
+ ? `${!isReviewMode ? ' ' : ''}${instructionText}`
37
+ : ''] }), _jsxs("div", { className: "flex w-full min-w-0 flex-col gap-4", children: [_jsxs("div", { className: "grid min-w-0 grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs("div", { className: "rounded-lg border border-blue-200 bg-blue-50/50 p-4", children: [_jsxs("h4", { className: "font-bold text-blue-900 mb-2.5 flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-blue-600 text-white text-xs font-semibold", children: "Across" }), _jsx("span", { children: "H\u00E0ng ngang" })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-60 overflow-y-auto pr-1", children: acrossClues.map((c) => {
36
38
  const answer = correctAnswer?.answers?.[`across-${c.number}`];
37
39
  const userAns = userAnswers[`across-${c.number}`];
38
40
  return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-blue-100", children: [_jsxs("span", { className: "font-bold text-blue-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [_jsxs("span", { className: "text-gray-800", children: [c.clue, c.isExample && (_jsx("span", { className: "ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-bold text-amber-700", children: "Example" }))] }), !isReviewMode && onAnswerChange && !c.isExample ? (_jsx("input", { type: "text", value: userAns || '', maxLength: c.length, onChange: (event) => onAnswerChange(`across-${c.number}`, event.target.value.toUpperCase()), placeholder: `${c.length} chữ`, className: "mt-1.5 w-full rounded border border-blue-200 px-2 py-1 text-sm font-semibold uppercase tracking-wider outline-none focus:border-blue-500" })) : null, isReviewMode && answer && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: answer }), userAns && userAns !== answer && (_jsx("span", { className: "ml-2 line-through text-red-500 font-semibold", children: userAns }))] }))] }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", c.length, " ch\u1EEF)"] })] }, `across-${c.number}`));
@@ -40,57 +42,58 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
40
42
  const answer = correctAnswer?.answers?.[`down-${c.number}`];
41
43
  const userAns = userAnswers[`down-${c.number}`];
42
44
  return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-purple-100", children: [_jsxs("span", { className: "font-bold text-purple-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [_jsxs("span", { className: "text-gray-800", children: [c.clue, c.isExample && (_jsx("span", { className: "ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-bold text-amber-700", children: "Example" }))] }), !isReviewMode && onAnswerChange && !c.isExample ? (_jsx("input", { type: "text", value: userAns || '', maxLength: c.length, onChange: (event) => onAnswerChange(`down-${c.number}`, event.target.value.toUpperCase()), placeholder: `${c.length} chữ`, className: "mt-1.5 w-full rounded border border-purple-200 px-2 py-1 text-sm font-semibold uppercase tracking-wider outline-none focus:border-purple-500" })) : null, isReviewMode && answer && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: answer }), userAns && userAns !== answer && (_jsx("span", { className: "ml-2 line-through text-red-500 font-semibold", children: userAns }))] }))] }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", c.length, " ch\u1EEF)"] })] }, `down-${c.number}`));
43
- }) })] })] }), _jsx("div", { className: "lg:col-span-7 flex justify-center overflow-x-auto p-4 bg-gray-50 rounded-xl border border-gray-200", children: _jsx("div", { className: "inline-grid gap-1", style: {
44
- gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
45
- }, children: Array.from({ length: rows }).map((_, rIdx) => {
46
- const r = rIdx + 1;
47
- return Array.from({ length: cols }).map((_, cIdx) => {
48
- const c = cIdx + 1;
49
- const key = `${r}-${c}`;
50
- const cellInfo = cellMap.get(key);
51
- const clueNum = clueNumberMap.get(key);
52
- if (!cellInfo) {
53
- return (_jsx("div", { className: "h-8 w-8 sm:h-9 sm:w-9 md:h-10 md:w-10 bg-transparent" }, key));
54
- }
55
- const isMarked = content?.markedColumn && c === content.markedColumn;
56
- const isExample = Boolean(cellInfo.across?.isExample || cellInfo.down?.isExample);
57
- // If in review mode or cell is an example, display correct answer letter
58
- let previewLetter = '';
59
- if (isReviewMode || isExample) {
60
- if (cellInfo.across && correctAnswer?.answers) {
61
- const word = correctAnswer.answers[`across-${cellInfo.across.number}`] || '';
62
- const offset = c - cellInfo.across.startCol;
63
- previewLetter = word[offset] || '';
45
+ }) })] })] }), _jsx("div", { className: "w-full min-w-0 overflow-x-auto rounded-xl border border-gray-200 bg-gray-50", children: _jsx("div", { className: "flex w-max min-w-full justify-center p-4", children: _jsx("div", { className: "inline-grid shrink-0 gap-1 [--cw-cell:2rem] sm:[--cw-cell:2.25rem] md:[--cw-cell:2.5rem]", style: {
46
+ gridTemplateColumns: `repeat(${cols}, var(--cw-cell))`,
47
+ gridAutoRows: 'var(--cw-cell)',
48
+ }, children: Array.from({ length: rows }).map((_, rIdx) => {
49
+ const r = rIdx + 1;
50
+ return Array.from({ length: cols }).map((_, cIdx) => {
51
+ const c = cIdx + 1;
52
+ const key = `${r}-${c}`;
53
+ const cellInfo = cellMap.get(key);
54
+ const clueNum = clueNumberMap.get(key);
55
+ if (!cellInfo) {
56
+ return (_jsx("div", { className: "size-[var(--cw-cell)] bg-transparent" }, key));
64
57
  }
65
- else if (cellInfo.down && correctAnswer?.answers) {
66
- const word = correctAnswer.answers[`down-${cellInfo.down.number}`] || '';
67
- const offset = r - cellInfo.down.startRow;
68
- previewLetter = word[offset] || '';
58
+ const isMarked = content?.markedColumn && c === content.markedColumn;
59
+ const isExample = Boolean(cellInfo.across?.isExample || cellInfo.down?.isExample);
60
+ // If in review mode or cell is an example, display correct answer letter
61
+ let previewLetter = '';
62
+ if (isReviewMode || isExample) {
63
+ if (cellInfo.across && correctAnswer?.answers) {
64
+ const word = correctAnswer.answers[`across-${cellInfo.across.number}`] || '';
65
+ const offset = c - cellInfo.across.startCol;
66
+ previewLetter = word[offset] || '';
67
+ }
68
+ else if (cellInfo.down && correctAnswer?.answers) {
69
+ const word = correctAnswer.answers[`down-${cellInfo.down.number}`] || '';
70
+ const offset = r - cellInfo.down.startRow;
71
+ previewLetter = word[offset] || '';
72
+ }
69
73
  }
70
- }
71
- else {
72
- if (cellInfo.across) {
73
- const word = userAnswers[`across-${cellInfo.across.number}`] || '';
74
- previewLetter = word[c - cellInfo.across.startCol] || '';
74
+ else {
75
+ if (cellInfo.across) {
76
+ const word = userAnswers[`across-${cellInfo.across.number}`] || '';
77
+ previewLetter = word[c - cellInfo.across.startCol] || '';
78
+ }
79
+ if (!previewLetter && cellInfo.down) {
80
+ const word = userAnswers[`down-${cellInfo.down.number}`] || '';
81
+ previewLetter = word[r - cellInfo.down.startRow] || '';
82
+ }
83
+ if (!previewLetter) {
84
+ previewLetter = userAnswers[key] || '';
85
+ }
75
86
  }
76
- if (!previewLetter && cellInfo.down) {
77
- const word = userAnswers[`down-${cellInfo.down.number}`] || '';
78
- previewLetter = word[r - cellInfo.down.startRow] || '';
87
+ const cellBg = isMarked ? 'bg-gray-400 border-gray-400 shadow-sm ring-1 ring-gray-400/50' : 'bg-gray-100 border-gray-300';
88
+ let textColor = isExample
89
+ ? 'text-orange-600 font-bold italic'
90
+ : isMarked
91
+ ? 'text-gray-950 font-bold'
92
+ : 'text-gray-800 font-bold';
93
+ if (isReviewMode && previewLetter && !isExample) {
94
+ textColor = 'text-green-700 font-extrabold';
79
95
  }
80
- if (!previewLetter) {
81
- previewLetter = userAnswers[key] || '';
82
- }
83
- }
84
- const cellBg = isMarked ? 'bg-gray-400 border-gray-400 shadow-sm ring-1 ring-gray-400/50' : 'bg-gray-100 border-gray-300';
85
- let textColor = isExample
86
- ? 'text-orange-600 font-bold italic'
87
- : isMarked
88
- ? 'text-gray-950 font-bold'
89
- : 'text-gray-800 font-bold';
90
- if (isReviewMode && previewLetter && !isExample) {
91
- textColor = 'text-green-700 font-extrabold';
92
- }
93
- return (_jsxs("div", { className: `relative flex items-center justify-center rounded border-2 font-bold shadow-sm h-8 w-8 sm:h-9 sm:w-9 md:h-10 md:w-10 text-sm sm:text-base md:text-lg ${cellBg} ${textColor}`, children: [clueNum && (_jsx("span", { className: "absolute top-0.5 left-1 text-[9px] font-bold text-gray-500 leading-none", children: clueNum })), _jsx("span", { children: previewLetter })] }, key));
94
- });
95
- }) }) })] }), (content?.type === 'HIDDEN_WORD' || content?.markedColumn) && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx("label", { className: "block text-sm font-semibold text-amber-900 mb-1", children: "What's the hidden word?" }), !isReviewMode && onAnswerChange ? (_jsx("input", { type: "text", value: userAnswers['hidden-word'] || '', onChange: (event) => onAnswerChange('hidden-word', event.target.value.toUpperCase()), placeholder: "Hidden word", className: "w-full rounded border border-amber-300 px-3 py-1.5 text-sm font-semibold uppercase tracking-wider outline-none focus:border-amber-500" })) : (_jsxs("div", { className: "text-sm", children: [userAnswers['hidden-word'] && (_jsx("span", { className: "font-semibold text-gray-800", children: userAnswers['hidden-word'] })), isReviewMode && correctAnswer?.answers?.['hidden-word'] && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: correctAnswer.answers['hidden-word'] })] }))] }))] }))] }));
96
+ return (_jsxs("div", { className: `relative flex size-[var(--cw-cell)] items-center justify-center rounded border-2 font-bold shadow-sm text-sm sm:text-base md:text-lg ${cellBg} ${textColor}`, children: [clueNum && (_jsx("span", { className: "absolute top-0.5 left-1 text-[9px] font-bold text-gray-500 leading-none", children: clueNum })), _jsx("span", { children: previewLetter })] }, key));
97
+ });
98
+ }) }) }) })] }), (content?.type === 'HIDDEN_WORD' || content?.markedColumn) && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx("label", { className: "block text-sm font-semibold text-amber-900 mb-1", children: "What's the hidden word?" }), !isReviewMode && onAnswerChange ? (_jsx("input", { type: "text", value: userAnswers['hidden-word'] || '', onChange: (event) => onAnswerChange('hidden-word', event.target.value.toUpperCase()), placeholder: "Hidden word", className: "w-full rounded border border-amber-300 px-3 py-1.5 text-sm font-semibold uppercase tracking-wider outline-none focus:border-amber-500" })) : (_jsxs("div", { className: "text-sm", children: [userAnswers['hidden-word'] && (_jsx("span", { className: "font-semibold text-gray-800", children: userAnswers['hidden-word'] })), isReviewMode && correctAnswer?.answers?.['hidden-word'] && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: correctAnswer.answers['hidden-word'] })] }))] }))] }))] }));
96
99
  }
@@ -9,7 +9,7 @@ import { extractPlaceholderIndices, formatBlankAnswersDisplay, getPrimaryBlankAn
9
9
  import { parseFillBlankSegments } from '../../../../shared/lib/utils/fill-in-blank';
10
10
  import { cn } from '../../../../shared/lib/utils';
11
11
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
12
- import { KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
12
+ import { KidPrompt, KidQuestionCard, reviewItemTitle } from '../../_shared/components/compact';
13
13
  // Sub-component: Resolve presigned URL for group image
14
14
  function GroupImage({ src, alt, className }) {
15
15
  const { previewUrl, isLoading } = usePresignedFileUrl(src);
@@ -228,7 +228,7 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
228
228
  const userInput = answers[index] || '';
229
229
  const isCorrect = isReviewMode && isAnswerCorrect(index, userInput);
230
230
  const hasAnswered = userInput.trim() !== '';
231
- return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: `Câu ${index + 1}: Điền vào chỗ trống`, review: isReviewMode && hasAnswered ? (isCorrect ? 'correct' : 'wrong') : null, children: [index === 0 && showAudio && cleanAudioUrl && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-indigo-100 bg-gradient-to-r from-indigo-50 to-blue-50 p-3", children: [_jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-indigo-100", children: _jsx(Volume2, { className: "h-4 w-4 text-indigo-600" }) }), _jsx("audio", { controls: true, className: "h-9 w-full", src: audioPreviewUrl || cleanAudioUrl, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })] })), _jsx(KidPrompt, { children: renderLegacyBlank(blank.text, blank.id, index) }), index === 0 && renderQuestionImage(), index === 0 && renderHints(), isReviewMode && (_jsx("div", { className: cn('rounded-xl border p-3 transition-colors', isCorrect
231
+ return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: reviewItemTitle(index + 1, isReviewMode, `Câu ${index + 1}: Điền vào chỗ trống`), review: isReviewMode && hasAnswered ? (isCorrect ? 'correct' : 'wrong') : null, children: [index === 0 && showAudio && cleanAudioUrl && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-indigo-100 bg-gradient-to-r from-indigo-50 to-blue-50 p-3", children: [_jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-indigo-100", children: _jsx(Volume2, { className: "h-4 w-4 text-indigo-600" }) }), _jsx("audio", { controls: true, className: "h-9 w-full", src: audioPreviewUrl || cleanAudioUrl, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })] })), _jsx(KidPrompt, { children: renderLegacyBlank(blank.text, blank.id, index) }), index === 0 && renderQuestionImage(), index === 0 && renderHints(), isReviewMode && (_jsx("div", { className: cn('rounded-xl border p-3 transition-colors', isCorrect
232
232
  ? 'border-emerald-200 bg-emerald-50/50'
233
233
  : hasAnswered
234
234
  ? 'border-red-200 bg-red-50/50'
@@ -1,11 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Fragment } from 'react';
2
+ import { Fragment, useState } from 'react';
3
+ import { useT } from '../../../../shared/lib/i18n';
3
4
  import { acceptedAnswerList } from './blank-utils';
4
5
  function AnswerOrList({ answers }) {
6
+ const t = useT();
7
+ const [expanded, setExpanded] = useState(false);
5
8
  if (answers.length === 0) {
6
9
  return _jsx("span", { className: "font-semibold text-green-900", children: "(tr\u1ED1ng)" });
7
10
  }
8
- return (_jsx(_Fragment, { children: answers.map((answer, index) => (_jsxs(Fragment, { children: [index > 0 && (_jsx("span", { className: "mx-1 text-xs font-normal italic text-green-600", children: "ho\u1EB7c" })), _jsx("span", { className: "font-semibold text-green-900", children: answer })] }, `${answer}-${index}`))) }));
11
+ const visible = expanded ? answers : answers.slice(0, 1);
12
+ return (_jsxs(_Fragment, { children: [visible.map((answer, index) => (_jsxs(Fragment, { children: [index > 0 && (_jsx("span", { className: "mx-1 text-xs font-normal italic text-green-600", children: "ho\u1EB7c" })), _jsx("span", { className: "font-semibold text-green-900", children: answer })] }, `${answer}-${index}`))), answers.length > 1 ? (_jsx("button", { type: "button", onClick: () => setExpanded((value) => !value), className: "ml-1 text-xs font-semibold text-indigo-600 hover:text-indigo-800", children: expanded
13
+ ? t('question.viewer.collapsePassage')
14
+ : t('question.viewer.expandPassage') })) : null] }));
9
15
  }
10
16
  export function FillInBlankAnswerKey({ answersByBlank, blankIndices, }) {
11
17
  const rows = blankIndices.map((blankIndex, order) => ({
@@ -3,7 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
3
3
  import { BookOpen, Lightbulb, PenLine, Volume2 } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
- import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
6
+ import { COLLAPSE_CHAR_THRESHOLD, KidCollapsibleBlock, KidInstruction, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle, } from '../../_shared/components/compact';
7
7
  import { FillInBlankAnswerKey } from './FillInBlankAnswerKey';
8
8
  import { blankSlotCount, extractPlaceholderIndices, isBlankMatch, padStudentAnswers, resolveBlankOptions, splitStem, splitStemTextDecorations, toFillInBlankImageUrls, } from './blank-utils';
9
9
  const STEM_MARK_STYLE = {
@@ -66,7 +66,9 @@ export function FillInBlankGroupClient({ questionData, isReviewMode = false, use
66
66
  next[itemIndex] = { answers: current };
67
67
  onAnswerChange(next);
68
68
  };
69
- return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), questionData.passage && (_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" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), questionData.audioUrl && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toFillInBlankImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: toFillInBlankImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, kind: "image", alt: `Group image ${imageIndex + 1}` }, `group-image-${imageIndex}`))) })), groupHints.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: groupHints.map((hint) => (_jsx("span", { className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: hint }, hint))) })), _jsx("div", { className: "grid grid-cols-1 gap-3", children: items.map((item, itemIndex) => {
69
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), questionData.passage && (_jsx(KidCollapsibleBlock, { collapse: isReviewMode &&
70
+ questionData.passage.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().length >
71
+ COLLAPSE_CHAR_THRESHOLD, children: _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" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] }) })), questionData.audioUrl && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toFillInBlankImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: toFillInBlankImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, kind: "image", alt: `Group image ${imageIndex + 1}` }, `group-image-${imageIndex}`))) })), groupHints.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: groupHints.map((hint) => (_jsx("span", { className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: hint }, hint))) })), _jsx("div", { className: "grid grid-cols-1 gap-3", children: items.map((item, itemIndex) => {
70
72
  const blankCount = blankSlotCount(item.question || '{0}');
71
73
  const placeholderIndices = extractPlaceholderIndices(item.question || '{0}');
72
74
  const userValue = padStudentAnswers(userAnswers[itemIndex], blankCount);
@@ -74,7 +76,7 @@ export function FillInBlankGroupClient({ questionData, isReviewMode = false, use
74
76
  const showItemAudio = Boolean(item.audioUrl) && !questionData.audioUrl;
75
77
  const hasAnyAnswer = userValue.some((value) => value.trim().length > 0);
76
78
  const allBlanksCorrect = placeholderIndices.every((index) => isBlankMatch(userValue[index] || '', item.answers[index] || []));
77
- return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnyAnswer ? (allBlanksCorrect ? 'correct' : 'wrong') : null, children: [toFillInBlankImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap items-center gap-2", children: toFillInBlankImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, kind: "image", alt: `Item ${item.questionNumber} image ${imageIndex + 1}` }, `${item.questionNumber}-${imageIndex}`))) })), showItemAudio && item.audioUrl && (_jsx(MediaPreview, { src: item.audioUrl, kind: "audio", alt: `Item ${item.questionNumber} audio` })), _jsx(KidPrompt, { children: _jsx("div", { className: "min-w-0 text-sm leading-relaxed text-slate-800", children: stem.map((part, partIndex) => {
79
+ return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnyAnswer ? (allBlanksCorrect ? 'correct' : 'wrong') : null, children: [toFillInBlankImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap items-center gap-2", children: toFillInBlankImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, kind: "image", alt: `Item ${item.questionNumber} image ${imageIndex + 1}` }, `${item.questionNumber}-${imageIndex}`))) })), showItemAudio && item.audioUrl && (_jsx(MediaPreview, { src: item.audioUrl, kind: "audio", alt: `Item ${item.questionNumber} audio` })), _jsx(KidPrompt, { children: _jsx("div", { className: "min-w-0 text-sm leading-relaxed text-slate-800", children: stem.map((part, partIndex) => {
78
80
  if (part.type === 'text') {
79
81
  return (_jsx("span", { children: _jsx(StemText, { value: part.value }) }, partIndex));
80
82
  }
@@ -193,19 +193,22 @@ export function FindWordsInMatrixClient({ content, correctAnswer, showSolution =
193
193
  : 'bấm ô cuối để hoàn thành từ.'] })) : (_jsx(_Fragment, { children: "Ch\u1ECDn m\u1ED9t t\u1EEB trong danh s\u00E1ch, r\u1ED3i b\u1EA5m t\u1EEBng ch\u1EEF tr\u00EAn l\u01B0\u1EDBi (ho\u1EB7c \u00F4 \u0111\u1EA7u r\u1ED3i \u00F4 cu\u1ED1i)." })) })) : null, isCategorize ? (_jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3", children: categories.map((cat) => {
194
194
  const wordsInCat = wordBank.filter((word) => word.categoryId === cat.id);
195
195
  return (_jsxs(Card, { className: "border bg-gray-50/70 p-3.5", children: [_jsxs("div", { className: "mb-2.5 flex items-center justify-between border-b pb-1.5 text-sm font-bold", children: [_jsx("span", { children: cat.name }), _jsx(Badge, { variant: "outline", className: "text-xs", children: wordsInCat.length })] }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: wordsInCat.map((word, index) => renderWordBadge(word, index)) })] }, cat.id));
196
- }) })) : (_jsxs(Card, { className: "border-2 border-dashed bg-gray-50 p-4", children: [_jsxs("div", { className: "mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500", children: ["Word Bank (", wordBank.length, " words)"] }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => renderWordBadge(word, index)) })] })), _jsx("div", { className: "flex justify-center overflow-x-auto pb-2", children: _jsx("div", { className: "inline-grid gap-1 rounded-xl border border-gray-200 bg-white p-3 shadow-sm", style: { gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }, children: Array.from({ length: rows }).map((_, rIdx) => {
197
- const r = rIdx + 1;
198
- return Array.from({ length: cols }).map((_, cIdx) => {
199
- const c = cIdx + 1;
200
- const char = grid[rIdx]?.[cIdx] || '';
201
- const highlight = cellColorMap[`${r}-${c}`];
202
- const colorConfig = highlight ? WORD_COLORS[highlight.colorIdx] : null;
203
- const isTapped = tappedPath.some((cell) => cell.row === r && cell.col === c);
204
- return (_jsx("button", { type: "button", disabled: !canSelect, onClick: () => handleCellClick(r, c), className: `flex h-9 w-9 items-center justify-center rounded-md border text-sm font-bold sm:h-11 sm:w-11 sm:text-base ${colorConfig
205
- ? `${colorConfig.bg} shadow-sm`
206
- : isTapped
207
- ? 'border-blue-500 bg-blue-50 text-blue-800'
208
- : 'border-gray-100 bg-gray-50/50 text-gray-700'}`, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})`, children: char || '·' }, `${r}-${c}`));
209
- });
210
- }) }) })] }));
196
+ }) })) : (_jsxs(Card, { className: "border-2 border-dashed bg-gray-50 p-4", children: [_jsxs("div", { className: "mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500", children: ["Word Bank (", wordBank.length, " words)"] }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => renderWordBadge(word, index)) })] })), _jsx("div", { className: "w-full min-w-0 overflow-x-auto pb-2", children: _jsx("div", { className: "flex w-max min-w-full justify-center", children: _jsx("div", { className: "inline-grid shrink-0 gap-1 rounded-xl border border-gray-200 bg-white p-3 shadow-sm [--fw-cell:2.25rem] sm:[--fw-cell:2.75rem]", style: {
197
+ gridTemplateColumns: `repeat(${cols}, var(--fw-cell))`,
198
+ gridAutoRows: 'var(--fw-cell)',
199
+ }, children: Array.from({ length: rows }).map((_, rIdx) => {
200
+ const r = rIdx + 1;
201
+ return Array.from({ length: cols }).map((_, cIdx) => {
202
+ const c = cIdx + 1;
203
+ const char = grid[rIdx]?.[cIdx] || '';
204
+ const highlight = cellColorMap[`${r}-${c}`];
205
+ const colorConfig = highlight ? WORD_COLORS[highlight.colorIdx] : null;
206
+ const isTapped = tappedPath.some((cell) => cell.row === r && cell.col === c);
207
+ return (_jsx("button", { type: "button", disabled: !canSelect, onClick: () => handleCellClick(r, c), className: `flex size-[var(--fw-cell)] items-center justify-center rounded-md border text-sm font-bold sm:text-base ${colorConfig
208
+ ? `${colorConfig.bg} shadow-sm`
209
+ : isTapped
210
+ ? 'border-blue-500 bg-blue-50 text-blue-800'
211
+ : 'border-gray-100 bg-gray-50/50 text-gray-700'}`, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})`, children: char || '·' }, `${r}-${c}`));
212
+ });
213
+ }) }) }) })] }));
211
214
  }
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Lightbulb } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
5
  import { assembleMatchColumnsSentence, getMatchColumnsChoiceColumns, getMatchColumnsStemColumn, } from './map-match-columns-to-make-sentences-data';
6
- import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter, } from '../../_shared/components/compact';
6
+ import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter, reviewItemPoints, reviewItemTitle, } from '../../_shared/components/compact';
7
7
  function emptyAnswer(stemFragmentId) {
8
8
  return { fragmentIds: stemFragmentId ? [stemFragmentId] : [], sentence: '' };
9
9
  }
@@ -59,7 +59,7 @@ export function MatchColumnsToMakeSentencesClient({ questionData, isReviewMode =
59
59
  || '—';
60
60
  const isCorrect = isReviewMode &&
61
61
  current.sentence.trim().toLowerCase() === item.sentence.trim().toLowerCase();
62
- return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: [_jsx(KidPrompt, { children: stemText }), choiceColumns.map((column, choiceIndex) => {
62
+ return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: [_jsx(KidPrompt, { children: stemText }), choiceColumns.map((column, choiceIndex) => {
63
63
  const selectedId = current.fragmentIds[choiceIndex + 1] || '';
64
64
  const correctId = item.fragmentIds[choiceIndex + 1] || '';
65
65
  return (_jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: "text-xs font-medium uppercase tracking-wider text-gray-500", children: column.label || column.id }), column.fragments.map((fragment, fragmentIndex) => {
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Lightbulb } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
- import { KidChoiceRow, KidInstruction, KidQuestionCard, optionLetter, } from '../../_shared/components/compact';
6
+ import { KidChoiceRow, KidInstruction, KidQuestionCard, optionLetter, reviewItemPoints, reviewItemTitle, } from '../../_shared/components/compact';
7
7
  function normalizeWord(value) {
8
8
  return value.trim().toLowerCase();
9
9
  }
@@ -53,7 +53,7 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
53
53
  const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
54
54
  const isCorrect = normalizeWord(selectedWord) === normalizeWord(item.correctAnswer) &&
55
55
  Boolean(item.correctAnswer);
56
- return (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start", children: [_jsx(ClientImageItem, { url: item.imageUrl, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}` }), _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), wordBank.map((word, wordIndex) => {
56
+ return (_jsx(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start", children: [_jsx(ClientImageItem, { url: item.imageUrl, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}` }), _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), wordBank.map((word, wordIndex) => {
57
57
  const isSelected = normalizeWord(word) === normalizeWord(selectedWord);
58
58
  const isUsedByOther = usedWords.has(normalizeWord(word)) && !isSelected;
59
59
  const isWordCorrect = normalizeWord(word) === normalizeWord(item.correctAnswer);
@@ -4,7 +4,7 @@ import { BookOpen, Check, ChevronDown, Lightbulb, Volume2, X } from 'lucide-reac
4
4
  import { cn } from '../../../../shared/lib/utils';
5
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
6
  import { toMatchWordToPictureImageUrls, toMatchWordToPictureStorageKey, } from '../../_shared/types/match-word-to-picture-group.type';
7
- import { KidInstruction, KidPrompt, KidQuestionCard, } from '../../_shared/components/compact';
7
+ import { KidInstruction, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle, } from '../../_shared/components/compact';
8
8
  function normalizeWord(value) {
9
9
  return value.trim().toLowerCase();
10
10
  }
@@ -76,7 +76,7 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
76
76
  const isCorrect = normalizeWord(selectedWord) === normalizeWord(item.correctAnswer) &&
77
77
  Boolean(item.correctAnswer);
78
78
  const itemImages = toMatchWordToPictureImageUrls(item.imageUrl);
79
- return (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start", children: [itemImages.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-2", children: itemImages.map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}${imageIndex > 0 ? ` (${imageIndex + 1})` : ''}` }, `${item.questionNumber}-${imageIndex}`))) })) : null, _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), item.isExample ? (_jsx("span", { className: "text-sm italic text-gray-800 underline decoration-gray-400", children: item.correctAnswer })) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(WordDropdown, { value: selectedWord, wordBank: wordBank, usedWords: usedWords, disabled: !canSelect, onChange: (value) => handleSelect(itemIndex, value) }), isReviewMode && (isCorrect
79
+ return (_jsx(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start", children: [itemImages.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-2", children: itemImages.map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}${imageIndex > 0 ? ` (${imageIndex + 1})` : ''}` }, `${item.questionNumber}-${imageIndex}`))) })) : null, _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), item.isExample ? (_jsx("span", { className: "text-sm italic text-gray-800 underline decoration-gray-400", children: item.correctAnswer })) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(WordDropdown, { value: selectedWord, wordBank: wordBank, usedWords: usedWords, disabled: !canSelect, onChange: (value) => handleSelect(itemIndex, value) }), isReviewMode && (isCorrect
80
80
  ? _jsx(Check, { className: "h-5 w-5 text-emerald-600" })
81
81
  : _jsx(X, { className: "h-5 w-5 text-red-500" }))] })), isReviewMode && !item.isExample && !isCorrect && (_jsxs("p", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: item.correctAnswer || '—' })] }))] })] }) }, `${item.questionNumber}-${itemIndex}`));
82
82
  }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Lightbulb, Mic, Volume2 } from 'lucide-react';
3
+ import { Mic, Volume2 } from 'lucide-react';
4
4
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
5
5
  import { KidInstruction, KidPrompt, KidQuestionCard, SpeakingReplyMic, } from '../../_shared/components/compact';
6
6
  function QuestionAudio({ url }) {
@@ -44,12 +44,20 @@ export function SpontaneousQaGroupClient({ questionData, isReviewMode = false, u
44
44
  const detail = findGradeDetail(gradeDetails, currentIndex);
45
45
  const grade = readGradeUser(detail);
46
46
  const showMic = !isReviewMode && !question.isExample;
47
- return (_jsxs(KidQuestionCard, { title: `Câu ${question.questionNumber || questionIndex + 1}`, points: detail?.partialScore != null ? undefined : (question.points || 0), isExample: question.isExample, headerExtra: detail?.partialScore != null ? (_jsxs("span", { className: "text-xs text-gray-500", children: [detail.partialScore, " / ", question.points || 0, " \u0111i\u1EC3m"] })) : undefined, children: [_jsx(KidPrompt, { children: question.questionText || 'Câu hỏi chưa được nhập' }), question.audioUrl ? _jsx(QuestionAudio, { url: question.audioUrl }) : null, showMic && (_jsx(SpeakingReplyMic, { flattenIndex: currentIndex, audioKey: typeof audioKey === 'string' ? audioKey : null, disabled: disabled, uploading: uploadingIndex === currentIndex, onRecorded: onItemRecorded })), detail && !question.isExample && (_jsxs("div", { className: "space-y-1 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-700", children: [grade.transcript && (_jsxs("p", { children: [_jsx("span", { className: "font-semibold", children: "Transcript:" }), " ", grade.transcript] })), (grade.contentScore != null || grade.pronunciationScore != null || grade.fluencyScore != null) && (_jsx("p", { children: [
48
- grade.contentScore != null ? `Content ${Math.round(grade.contentScore)}` : null,
49
- grade.pronunciationScore != null ? `Pronunciation ${Math.round(grade.pronunciationScore)}` : null,
50
- grade.fluencyScore != null ? `Fluency ${Math.round(grade.fluencyScore)}` : null,
51
- ].filter(Boolean).join(' · ') })), grade.feedback && _jsx("p", { children: grade.feedback })] })), isReviewMode && question.expectedAnswers.filter((item) => item.trim() !== '').length > 0 && (_jsxs("div", { className: "rounded-lg border border-amber-100 bg-amber-50 px-3 py-2", children: [_jsxs("div", { className: "mb-1 flex items-center gap-1 text-xs font-semibold text-amber-700", children: [_jsx(Lightbulb, { className: "h-3 w-3" }), "\u0110\u00E1p \u00E1n m\u1EABu"] }), _jsx("ul", { className: "list-disc space-y-1 pl-4 text-xs text-amber-800", children: question.expectedAnswers
52
- .filter((item) => item.trim() !== '')
53
- .map((item) => (_jsx("li", { children: item }, item))) })] }))] }, `${part.partNumber}-${question.questionNumber}-${questionIndex}`));
47
+ const itemNumber = question.questionNumber || questionIndex + 1;
48
+ const scoreLabel = detail?.partialScore != null
49
+ ? `${detail.partialScore} / ${question.points || 0}`
50
+ : null;
51
+ if (isReviewMode) {
52
+ return (_jsxs("div", { className: "flex items-start gap-3 rounded-lg border border-slate-200 bg-white px-3 py-2.5", children: [_jsx("span", { className: "mt-0.5 w-6 shrink-0 text-sm font-extrabold text-sky-800", children: itemNumber }), _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-medium leading-snug text-slate-800", children: question.questionText || 'Câu hỏi chưa được nhập' }), question.audioUrl ? _jsx(QuestionAudio, { url: question.audioUrl }) : null, detail && !question.isExample ? (_jsx("p", { className: "line-clamp-2 text-xs text-slate-600", children: [
53
+ grade.feedback,
54
+ grade.contentScore != null
55
+ ? `Content ${Math.round(grade.contentScore)}`
56
+ : null,
57
+ ]
58
+ .filter(Boolean)
59
+ .join(' · ') })) : null] }), scoreLabel ? (_jsx("span", { className: "shrink-0 text-xs font-semibold text-slate-500", children: scoreLabel })) : null] }, `${part.partNumber}-${question.questionNumber}-${questionIndex}`));
60
+ }
61
+ return (_jsxs(KidQuestionCard, { title: `Câu ${itemNumber}`, points: detail?.partialScore != null ? undefined : (question.points || 0), isExample: question.isExample, headerExtra: detail?.partialScore != null ? (_jsxs("span", { className: "text-xs text-gray-500", children: [detail.partialScore, " / ", question.points || 0, " \u0111i\u1EC3m"] })) : undefined, children: [_jsx(KidPrompt, { children: question.questionText || 'Câu hỏi chưa được nhập' }), question.audioUrl ? _jsx(QuestionAudio, { url: question.audioUrl }) : null, showMic && (_jsx(SpeakingReplyMic, { flattenIndex: currentIndex, audioKey: typeof audioKey === 'string' ? audioKey : null, disabled: disabled, uploading: uploadingIndex === currentIndex, onRecorded: onItemRecorded }))] }, `${part.partNumber}-${question.questionNumber}-${questionIndex}`));
54
62
  })] }, `part-${part.partNumber}-${partIndex}`)))] }));
55
63
  }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Lightbulb } from 'lucide-react';
4
- import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
4
+ import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
5
5
  function emptyAnswer() {
6
6
  return { isTrue: null, correction: '' };
7
7
  }
@@ -25,7 +25,7 @@ export function TrueFalseCorrectGroupClient({ questionData, isReviewMode = false
25
25
  const showStudentCorrection = userValue?.isTrue === false || (isReviewMode && !correctIsTrue);
26
26
  const hasAnswered = userValue?.isTrue === true || userValue?.isTrue === false;
27
27
  const isAnswerCorrect = userValue?.isTrue === correctIsTrue;
28
- return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnswered
28
+ return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnswered
29
29
  ? (isAnswerCorrect ? 'correct' : 'wrong')
30
30
  : null, children: [_jsx(KidPrompt, { children: item.question || 'Câu hỏi chưa được nhập' }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(KidChoiceRow, { letter: "T", selected: userValue?.isTrue === true, isReviewMode: isReviewMode, isCorrect: isReviewMode && correctIsTrue, disabled: !canSelect, onClick: canSelect
31
31
  ? () => emit(itemIndex, {
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Lightbulb } from 'lucide-react';
4
- import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
4
+ import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
5
5
  export function TrueFalseGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
6
6
  const items = questionData.items || [];
7
7
  const handleSelect = (itemIndex, value) => {
@@ -19,7 +19,7 @@ export function TrueFalseGroupClient({ questionData, isReviewMode = false, userA
19
19
  const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
20
20
  const hasAnswered = userValue !== undefined;
21
21
  const isAnswerCorrect = userValue === correctAnswer;
22
- return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnswered
22
+ return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnswered
23
23
  ? (isAnswerCorrect ? 'correct' : 'wrong')
24
24
  : null, children: [_jsx(KidPrompt, { children: item.question || 'Câu hỏi chưa được nhập' }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(KidChoiceRow, { letter: "T", selected: userValue === true, isReviewMode: isReviewMode, isCorrect: isReviewMode && correctAnswer === true, disabled: !canSelect, onClick: canSelect ? () => handleSelect(itemIndex, true) : undefined, children: "\u0110\u00FAng" }), _jsx(KidChoiceRow, { letter: "F", selected: userValue === false, isReviewMode: isReviewMode, isCorrect: isReviewMode && correctAnswer === false, disabled: !canSelect, onClick: canSelect ? () => handleSelect(itemIndex, false) : undefined, children: "Sai" })] })] }, `${item.questionNumber}-${itemIndex}`));
25
25
  }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
@@ -132,7 +132,9 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
132
132
  const rawExplanation = String(explanation || questionData.explanation || '').trim();
133
133
  const headerInstruction = String(instruction || dataInstruction || rawExplanation || '').trim();
134
134
  // Header reads "Bài x: rubric" — the question type name is never shown.
135
- const numberLabel = typeof questionNumber === 'number' && questionNumber > 0
135
+ const numberLabel = !isReviewMode &&
136
+ typeof questionNumber === 'number' &&
137
+ questionNumber > 0
136
138
  ? t('question.viewer.partN', { n: questionNumber })
137
139
  : '';
138
140
  const headerTitle = numberLabel ? (_jsxs(_Fragment, { children: [_jsxs("strong", { className: "font-bold", children: [numberLabel, headerInstruction ? ':' : ''] }), headerInstruction ? ` ${headerInstruction}` : ''] })) : (headerInstruction || t('question.viewer.wfsfDefaultTitle'));
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect, useCallback } from 'react';
4
4
  import { X, RotateCcw } from 'lucide-react';
5
5
  import { cn } from '../../../../shared/lib/utils';
6
- import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
6
+ import { KidInstruction, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
7
7
  export function WordOrderAndMatchGroupClient({ questionData, isReviewMode = false, userAnswers, onAnswerChange, }) {
8
8
  const { instruction, matchOptions = ['A', 'B', 'C', 'D', 'E', 'F'], items = [] } = questionData;
9
9
  // Initialize interactive state for each item
@@ -152,7 +152,9 @@ export function WordOrderAndMatchGroupClient({ questionData, isReviewMode = fals
152
152
  const isPhraseCorrect = normalizeStr(userPhrase) === normalizeStr(item.orderedPhrase || '');
153
153
  const isLabelCorrect = (userLabel || '').trim().toUpperCase() === (item.matchLabel || '').trim().toUpperCase();
154
154
  const isItemAllCorrect = isPhraseCorrect && isLabelCorrect;
155
- return (_jsx(KidQuestionCard, { title: isExample ? 'Ví dụ (Example)' : `Câu ${item.questionNumber || itemIdx + 1}`, points: !isExample && typeof item.points === 'number' ? item.points : undefined, isExample: isExample, review: isReviewMode && !isExample ? (isItemAllCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "grid grid-cols-1 gap-4 lg:grid-cols-12", children: [_jsxs("div", { className: "space-y-3 lg:col-span-8 xl:col-span-9", children: [_jsx("div", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wider", children: "1. S\u1EAFp x\u1EBFp c\u00E1c t\u1EEB th\u00E0nh c\u00E2u ho\u00E0n ch\u1EC9nh:" }), _jsxs("div", { className: cn('min-h-[52px] rounded-xl border-2 border-dashed p-2.5 transition-colors flex flex-wrap items-center gap-1.5', state.placedIndices.length > 0
155
+ return (_jsx(KidQuestionCard, { title: isExample
156
+ ? 'Ví dụ (Example)'
157
+ : reviewItemTitle(item.questionNumber || itemIdx + 1, isReviewMode, `Câu ${item.questionNumber || itemIdx + 1}`), points: reviewItemPoints(isReviewMode, !isExample && typeof item.points === 'number' ? item.points : undefined), isExample: isExample, review: isReviewMode && !isExample ? (isItemAllCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "grid grid-cols-1 gap-4 lg:grid-cols-12", children: [_jsxs("div", { className: "space-y-3 lg:col-span-8 xl:col-span-9", children: [_jsx("div", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wider", children: "1. S\u1EAFp x\u1EBFp c\u00E1c t\u1EEB th\u00E0nh c\u00E2u ho\u00E0n ch\u1EC9nh:" }), _jsxs("div", { className: cn('min-h-[52px] rounded-xl border-2 border-dashed p-2.5 transition-colors flex flex-wrap items-center gap-1.5', state.placedIndices.length > 0
156
158
  ? 'border-blue-300 bg-blue-50/40'
157
159
  : 'border-gray-300 bg-gray-50/60'), children: [state.placedIndices.length === 0 ? (_jsx("span", { className: "text-xs italic text-gray-400 select-none", children: "Nh\u1EA5n v\u00E0o c\u00E1c t\u1EEB b\u00EAn d\u01B0\u1EDBi \u0111\u1EC3 gh\u00E9p c\u00E2u..." })) : (state.placedIndices.map((wordIdx, orderIdx) => {
158
160
  const word = item.scrambledWords[wordIdx];
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Lightbulb } from 'lucide-react';
4
4
  import { WordOrderingClient } from '../word-ordering/WordOrderingClient';
5
- import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
5
+ import { KidInstruction, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
6
6
  export function WordOrderingGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
7
7
  const items = questionData.items || [];
8
8
  const handleItemSubmit = (itemIndex, ordered) => {
@@ -17,7 +17,7 @@ export function WordOrderingGroupClient({ questionData, isReviewMode = false, us
17
17
  });
18
18
  onAnswerChange(next);
19
19
  };
20
- return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), items.map((item, itemIndex) => (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, children: _jsx(WordOrderingClient, { questionData: {
20
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), items.map((item, itemIndex) => (_jsx(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, children: _jsx(WordOrderingClient, { questionData: {
21
21
  words: item.words,
22
22
  answer: item.answer,
23
23
  isExample: item.isExample,
@@ -631,7 +631,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
631
631
  question.type === 'ANSWER_THE_QUESTION') {
632
632
  return (_jsx("div", { className: "space-y-4", children: renderQuestion() }));
633
633
  }
634
- return (_jsxs(Card, { children: [cardTitle ? (_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center justify-between", children: [_jsx("span", { className: "min-w-0 pr-2 text-base font-semibold leading-snug", children: cardTitle }), _jsx("span", { className: "shrink-0 text-sm font-normal text-gray-500", children: translate('question.viewer.points', { count: question.points }) })] }) })) : null, _jsx(CardContent, { className: cardTitle ? 'space-y-4' : 'space-y-4 pt-4', children: renderQuestion() })] }));
634
+ const showViewerHeader = Boolean(cardTitle) && !isReviewMode;
635
+ return (_jsxs(Card, { children: [showViewerHeader ? (_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center justify-between", children: [_jsx("span", { className: "min-w-0 pr-2 text-base font-semibold leading-snug", children: cardTitle }), _jsx("span", { className: "shrink-0 text-sm font-normal text-gray-500", children: translate('question.viewer.points', { count: question.points }) })] }) })) : null, _jsx(CardContent, { className: showViewerHeader ? 'space-y-4' : 'space-y-4 pt-4', children: renderQuestion() })] }));
635
636
  }
636
637
  /** Drop lightweight markdown markers for plain header labels (not full token parse). */
637
638
  function stripLightweightMarkdown(text) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyweb_dev/oe-exam-sdk",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Reusable OceanEdu question components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",