@tinyweb_dev/oe-exam-sdk 1.0.12 → 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 (53) 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 +5 -0
  6. package/dist/components/questions/_shared/components/compact/KidPassage.js +21 -0
  7. package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +3 -1
  8. package/dist/components/questions/_shared/components/compact/SpeakingReplyMic.d.ts +7 -0
  9. package/dist/components/questions/_shared/components/compact/SpeakingReplyMic.js +86 -0
  10. package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
  11. package/dist/components/questions/_shared/components/compact/index.js +4 -0
  12. package/dist/components/questions/_shared/components/compact/reviewItemCard.d.ts +3 -0
  13. package/dist/components/questions/_shared/components/compact/reviewItemCard.js +11 -0
  14. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +14 -5
  15. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -2
  16. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +2 -2
  17. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +19 -8
  18. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +2 -2
  19. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +36 -56
  20. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +2 -2
  21. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +53 -50
  22. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +2 -2
  23. package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +8 -2
  24. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +5 -3
  25. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +18 -15
  26. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +2 -2
  27. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +2 -2
  28. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +2 -2
  29. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.d.ts +4 -1
  30. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +4 -4
  31. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +17 -82
  32. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +2 -2
  33. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +2 -2
  34. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +18 -23
  35. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +2 -8
  36. package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +4 -11
  37. package/dist/components/questions/types/word-fill-structured-form/transform.js +4 -9
  38. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +4 -2
  39. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +2 -2
  40. package/dist/components/questions/viewer/QuestionViewer.d.ts +3 -1
  41. package/dist/components/questions/viewer/QuestionViewer.js +37 -19
  42. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +1 -1
  43. package/dist/shared/index.d.ts +2 -0
  44. package/dist/shared/index.js +1 -0
  45. package/dist/shared/lib/i18n/messages/en/common.d.ts +38 -0
  46. package/dist/shared/lib/i18n/messages/en/common.js +39 -0
  47. package/dist/shared/lib/i18n/messages/en.d.ts +38 -0
  48. package/dist/shared/lib/i18n/messages/vi/common.d.ts +38 -0
  49. package/dist/shared/lib/i18n/messages/vi/common.js +39 -0
  50. package/dist/shared/lib/i18n/messages/vi.d.ts +38 -0
  51. package/dist/shared/lib/utils/media-urls.d.ts +2 -0
  52. package/dist/shared/lib/utils/media-urls.js +23 -0
  53. 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
+ }
@@ -0,0 +1,5 @@
1
+ export declare function KidPassage({ html, className, isReviewMode, }: {
2
+ html: string;
3
+ className?: string;
4
+ isReviewMode?: boolean;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { BookOpen } from 'lucide-react';
5
+ import { useT } from '../../../../../shared/lib/i18n';
6
+ import { cn } from '../../../../../shared/lib/utils';
7
+ const COLLAPSE_CHAR_THRESHOLD = 480;
8
+ function plainTextLength(html) {
9
+ return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().length;
10
+ }
11
+ export function KidPassage({ html, className, isReviewMode = false, }) {
12
+ const t = useT();
13
+ const isLong = plainTextLength(html) > COLLAPSE_CHAR_THRESHOLD;
14
+ const [expanded, setExpanded] = useState(!isLong);
15
+ if (!html) {
16
+ return null;
17
+ }
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
+ ? t('question.viewer.collapsePassage')
20
+ : t('question.viewer.expandPassage') })) : null] }));
21
+ }
@@ -1,7 +1,9 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Check, CircleHelp, Star, X } from 'lucide-react';
4
+ import { useT } from '../../../../../shared/lib/i18n';
4
5
  import { cn } from '../../../../../shared/lib/utils';
5
6
  export function KidQuestionCard({ icon, hideIcon = false, title, titleClassName, points, isExample = false, review, headerExtra, children, className, contentClassName, }) {
6
- 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" }), "C\u00E2u v\u00ED d\u1EE5"] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [headerExtra, typeof points === 'number' && (_jsxs("span", { className: "text-xs text-gray-500", children: [points, " \u0111i\u1EC3m"] })), 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" }), "\u0110\u00FAng"] })), 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" }), "Sai"] }))] })] }), _jsx("div", { className: cn('space-y-3 p-3', contentClassName), children: children })] }));
7
+ const t = useT();
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 })] }));
7
9
  }
@@ -0,0 +1,7 @@
1
+ export declare function SpeakingReplyMic({ flattenIndex, audioKey, disabled, uploading, onRecorded, }: {
2
+ flattenIndex: number;
3
+ audioKey?: string | null;
4
+ disabled?: boolean;
5
+ uploading?: boolean;
6
+ onRecorded?: (flattenIndex: number, blob: Blob) => void;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { Mic, Square, Volume2 } from 'lucide-react';
5
+ import { usePresignedFileUrl } from '../../../../../shared/lib/hooks';
6
+ function QuestionAudio({ url }) {
7
+ const { previewUrl, isLoading } = usePresignedFileUrl(url);
8
+ const src = previewUrl ||
9
+ (url.startsWith('http') || url.startsWith('blob:') ? url : '');
10
+ if (!src && !isLoading) {
11
+ return null;
12
+ }
13
+ return (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-blue-100 bg-blue-50/60 px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 shrink-0 text-blue-500" }), isLoading ? (_jsx("div", { className: "h-8 flex-1 animate-pulse rounded-md bg-blue-100" })) : (_jsx("audio", { controls: true, src: src, className: "h-8 w-full", preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." }))] }));
14
+ }
15
+ export function SpeakingReplyMic({ flattenIndex, audioKey, disabled, uploading, onRecorded, }) {
16
+ const [status, setStatus] = useState('idle');
17
+ const [error, setError] = useState('');
18
+ const [blobUrl, setBlobUrl] = useState('');
19
+ const recorderRef = useRef(null);
20
+ const chunksRef = useRef([]);
21
+ const streamRef = useRef(null);
22
+ useEffect(() => {
23
+ return () => {
24
+ recorderRef.current?.stop();
25
+ streamRef.current?.getTracks().forEach((track) => track.stop());
26
+ if (blobUrl) {
27
+ URL.revokeObjectURL(blobUrl);
28
+ }
29
+ };
30
+ }, [blobUrl]);
31
+ const stopStream = () => {
32
+ streamRef.current?.getTracks().forEach((track) => track.stop());
33
+ streamRef.current = null;
34
+ };
35
+ const startRecording = async () => {
36
+ if (disabled || uploading || !onRecorded) {
37
+ return;
38
+ }
39
+ setError('');
40
+ try {
41
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
42
+ streamRef.current = stream;
43
+ chunksRef.current = [];
44
+ const recorder = new MediaRecorder(stream);
45
+ recorderRef.current = recorder;
46
+ recorder.ondataavailable = (event) => {
47
+ if (event.data.size > 0) {
48
+ chunksRef.current.push(event.data);
49
+ }
50
+ };
51
+ recorder.onstop = () => {
52
+ const blob = new Blob(chunksRef.current, { type: recorder.mimeType || 'audio/webm' });
53
+ setBlobUrl((current) => {
54
+ if (current) {
55
+ URL.revokeObjectURL(current);
56
+ }
57
+ return URL.createObjectURL(blob);
58
+ });
59
+ setStatus('recorded');
60
+ stopStream();
61
+ onRecorded(flattenIndex, blob);
62
+ };
63
+ recorder.start();
64
+ setStatus('recording');
65
+ }
66
+ catch {
67
+ setError('Không thể mở microphone. Hãy cho phép quyền ghi âm.');
68
+ stopStream();
69
+ }
70
+ };
71
+ const stopRecording = () => {
72
+ if (recorderRef.current && recorderRef.current.state !== 'inactive') {
73
+ recorderRef.current.stop();
74
+ }
75
+ };
76
+ const canRecord = Boolean(onRecorded) && !disabled && !uploading;
77
+ return (_jsxs("div", { className: "flex flex-col items-center gap-2 rounded-lg border border-dashed border-indigo-200 bg-indigo-50/60 px-4 py-3", children: [canRecord ? (_jsx("button", { type: "button", onClick: status === 'recording' ? stopRecording : startRecording, title: status === 'recording' ? 'Dừng ghi âm' : 'Nhấn micro để trả lời', className: `inline-flex h-12 w-12 items-center justify-center rounded-full text-white shadow-sm transition-colors ${status === 'recording'
78
+ ? 'animate-pulse bg-red-500 hover:bg-red-600'
79
+ : 'bg-indigo-600 hover:bg-indigo-700'}`, children: status === 'recording' ? _jsx(Square, { className: "h-4 w-4 fill-current" }) : _jsx(Mic, { className: "h-5 w-5" }) })) : (_jsx("div", { className: "inline-flex h-12 w-12 items-center justify-center rounded-full bg-slate-300 text-white", children: _jsx(Mic, { className: "h-5 w-5" }) })), _jsx("p", { className: "text-center text-xs text-indigo-700", children: uploading
80
+ ? 'Đang tải lên bản ghi...'
81
+ : status === 'recording'
82
+ ? 'Đang ghi âm... Nhấn để dừng'
83
+ : audioKey || status === 'recorded'
84
+ ? 'Đã ghi. Nhấn micro để ghi lại'
85
+ : 'Nhấn micro để trả lời bằng giọng nói' }), error && _jsx("p", { className: "text-center text-xs text-red-600", children: error }), audioKey ? (_jsx("div", { className: "w-full max-w-sm", children: _jsx(QuestionAudio, { url: audioKey }) })) : status === 'recorded' && blobUrl ? (_jsx("audio", { controls: true, src: blobUrl, className: "h-8 w-full max-w-sm", preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })) : null] }));
86
+ }
@@ -5,6 +5,10 @@ export { CompactCreatorHeader } from './CompactCreatorHeader';
5
5
  export { CompactFieldCard } from './CompactFieldCard';
6
6
  export { CompactExplanationToggle } from './CompactExplanationToggle';
7
7
  export { KidInstruction } from './KidInstruction';
8
+ export { KidPassage } from './KidPassage';
9
+ export { KidCollapsibleBlock, COLLAPSE_CHAR_THRESHOLD } from './KidCollapsibleBlock';
8
10
  export { KidPrompt } from './KidPrompt';
9
11
  export { KidQuestionCard } from './KidQuestionCard';
12
+ export { reviewItemTitle, reviewItemPoints } from './reviewItemCard';
10
13
  export { KidChoiceRow, optionLetter } from './KidChoiceRow';
14
+ export { SpeakingReplyMic } from './SpeakingReplyMic';
@@ -5,6 +5,10 @@ export { CompactCreatorHeader } from './CompactCreatorHeader';
5
5
  export { CompactFieldCard } from './CompactFieldCard';
6
6
  export { CompactExplanationToggle } from './CompactExplanationToggle';
7
7
  export { KidInstruction } from './KidInstruction';
8
+ export { KidPassage } from './KidPassage';
9
+ export { KidCollapsibleBlock, COLLAPSE_CHAR_THRESHOLD } from './KidCollapsibleBlock';
8
10
  export { KidPrompt } from './KidPrompt';
9
11
  export { KidQuestionCard } from './KidQuestionCard';
12
+ export { reviewItemTitle, reviewItemPoints } from './reviewItemCard';
10
13
  export { KidChoiceRow, optionLetter } from './KidChoiceRow';
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
+ }
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { BookOpen, CircleHelp, Lightbulb, Sparkles, Volume2 } from 'lucide-react';
3
+ import { CircleHelp, Lightbulb, Sparkles, Volume2 } from 'lucide-react';
4
+ import { useT } from '../../../../shared/lib/i18n';
4
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
5
6
  import { toAnswerTheQuestionGroupImageUrls } from '../../_shared/types/answer-the-question-group.type';
6
- import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
7
+ import { KidInstruction, KidPassage, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
7
8
  function isAbsoluteMediaUrl(value) {
8
9
  return (value.startsWith('http://')
9
10
  || value.startsWith('https://')
@@ -41,16 +42,24 @@ function MediaPreview({ src, kind = 'image', alt }) {
41
42
  } }) }));
42
43
  }
43
44
  export function AnswerTheQuestionGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
45
+ const t = useT();
44
46
  const items = questionData.items || [];
45
- return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.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: `Hình ảnh (${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: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
47
+ const instruction = questionData.instruction?.trim()
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, isReviewMode: isReviewMode })) : null, items.map((item, itemIndex) => {
46
50
  const userValue = userAnswers[itemIndex] || '';
47
51
  const expectedAnswers = Array.isArray(item.expectedAnswers)
48
52
  ? item.expectedAnswers.filter((answer) => answer.trim() !== '')
49
53
  : [];
50
- return (_jsxs(KidQuestionCard, { icon: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }), title: `Câu ${item.questionNumber || itemIndex + 1}`, 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: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), _jsx(KidPrompt, { children: item.question || 'Câu hỏi chưa được nhập' }), 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", { value: userValue, onChange: (event) => {
54
+ const questionNumber = item.questionNumber || itemIndex + 1;
55
+ const answerId = `atqg-answer-${questionNumber}`;
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
+ n: questionNumber,
58
+ m: imageIndex + 1,
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) => {
51
60
  const next = items.map((_, index) => userAnswers[index] || '');
52
61
  next[itemIndex] = event.target.value;
53
62
  onAnswerChange(next);
54
- }, placeholder: "Nh\u1EADp c\u00E2u tr\u1EA3 l\u1EDDi...", rows: 2, className: "w-full rounded-md border border-slate-200 px-3 py-1.5 text-sm text-slate-800 outline-none ring-blue-200 focus:border-blue-400 focus:ring-2" })), isReviewMode && expectedAnswers.length > 0 ? (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })) : isReviewMode && !userValue ? (_jsx("div", { className: "rounded-md border border-gray-200 bg-gray-50 px-3 py-1.5 text-sm text-gray-500", children: "Ch\u01B0a c\u00F3 \u0111\u00E1p \u00E1n" })) : null] }, `${item.questionNumber}-${itemIndex}`));
63
+ }, placeholder: t('question.viewer.placeholder'), rows: 4, className: "w-full rounded-md border border-slate-200 px-3 py-1.5 text-sm text-slate-800 outline-none ring-blue-200 focus:border-blue-400 focus:ring-2" })), isReviewMode && expectedAnswers.length > 0 ? (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: t('question.viewer.answerKey') }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })) : isReviewMode && !userValue ? (_jsx("div", { className: "rounded-md border border-gray-200 bg-gray-50 px-3 py-1.5 text-sm text-gray-500", children: t('question.viewer.noAnswer') })) : null] }, `${item.questionNumber}-${itemIndex}`));
55
64
  }), 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] }))] }));
56
65
  }
@@ -1,5 +1,5 @@
1
1
  import { fromAnswerTheQuestionGroupImageUrls, toAnswerTheQuestionGroupImageUrls, } from '../../_shared/types/answer-the-question-group.type';
2
- const DEFAULT_INSTRUCTION = 'Read the text again and answer the questions.';
2
+ const DEFAULT_INSTRUCTION = '';
3
3
  function isRecord(value) {
4
4
  return typeof value === 'object' && value !== null && !Array.isArray(value);
5
5
  }
@@ -70,7 +70,7 @@ export function mapQuestionToAnswerTheQuestionGroupData(question) {
70
70
  const audioUrl = asOptionalUrl(meta.audioUrl);
71
71
  const imageUrl = toAnswerTheQuestionGroupImageUrls(meta.imageUrl);
72
72
  return {
73
- instruction: asString(meta.instruction) || DEFAULT_INSTRUCTION,
73
+ instruction: asString(meta.instruction),
74
74
  passage: asString(meta.passage),
75
75
  ...(audioUrl ? { audioUrl } : {}),
76
76
  ...(imageUrl.length ? { imageUrl } : {}),
@@ -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);