@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.
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.js +3 -1
- package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +3 -1
- package/dist/components/questions/_shared/components/compact/KidCollapsibleBlock.d.ts +8 -0
- package/dist/components/questions/_shared/components/compact/KidCollapsibleBlock.js +16 -0
- package/dist/components/questions/_shared/components/compact/KidPassage.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidPassage.js +21 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +3 -1
- package/dist/components/questions/_shared/components/compact/SpeakingReplyMic.d.ts +7 -0
- package/dist/components/questions/_shared/components/compact/SpeakingReplyMic.js +86 -0
- package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
- package/dist/components/questions/_shared/components/compact/index.js +4 -0
- package/dist/components/questions/_shared/components/compact/reviewItemCard.d.ts +3 -0
- package/dist/components/questions/_shared/components/compact/reviewItemCard.js +11 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +14 -5
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -2
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +2 -2
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +19 -8
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +2 -2
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +36 -56
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +2 -2
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +53 -50
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +2 -2
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +8 -2
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +5 -3
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +18 -15
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +2 -2
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +2 -2
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +2 -2
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.d.ts +4 -1
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +4 -4
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +17 -82
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +2 -2
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +2 -2
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +18 -23
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +2 -8
- package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +4 -11
- package/dist/components/questions/types/word-fill-structured-form/transform.js +4 -9
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +4 -2
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +2 -2
- package/dist/components/questions/viewer/QuestionViewer.d.ts +3 -1
- package/dist/components/questions/viewer/QuestionViewer.js +37 -19
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +1 -1
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/lib/i18n/messages/en/common.d.ts +38 -0
- package/dist/shared/lib/i18n/messages/en/common.js +39 -0
- package/dist/shared/lib/i18n/messages/en.d.ts +38 -0
- package/dist/shared/lib/i18n/messages/vi/common.d.ts +38 -0
- package/dist/shared/lib/i18n/messages/vi/common.js +39 -0
- package/dist/shared/lib/i18n/messages/vi.d.ts +38 -0
- package/dist/shared/lib/utils/media-urls.d.ts +2 -0
- package/dist/shared/lib/utils/media-urls.js +23 -0
- 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(
|
|
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
|
}
|
package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js
CHANGED
|
@@ -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(
|
|
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,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
|
-
|
|
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
|
+
}
|
package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 = '
|
|
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)
|
|
73
|
+
instruction: asString(meta.instruction),
|
|
74
74
|
passage: asString(meta.passage),
|
|
75
75
|
...(audioUrl ? { audioUrl } : {}),
|
|
76
76
|
...(imageUrl.length ? { imageUrl } : {}),
|
package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js
CHANGED
|
@@ -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}
|
|
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() !== ''
|
package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js
CHANGED
|
@@ -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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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}
|
|
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);
|