@tinyweb_dev/oe-exam-sdk 1.0.12 → 1.0.13
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/questions/_shared/components/compact/KidPassage.d.ts +4 -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 +2 -0
- package/dist/components/questions/_shared/components/compact/index.js +2 -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/choose-then-answer-group/ChooseThenAnswerGroupClient.js +35 -55
- 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 +2 -75
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +15 -22
- 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/viewer/QuestionViewer.d.ts +3 -1
- package/dist/components/questions/viewer/QuestionViewer.js +36 -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
|
@@ -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, }) {
|
|
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 && '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('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,8 @@ 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';
|
|
8
9
|
export { KidPrompt } from './KidPrompt';
|
|
9
10
|
export { KidQuestionCard } from './KidQuestionCard';
|
|
10
11
|
export { KidChoiceRow, optionLetter } from './KidChoiceRow';
|
|
12
|
+
export { SpeakingReplyMic } from './SpeakingReplyMic';
|
|
@@ -5,6 +5,8 @@ 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';
|
|
8
9
|
export { KidPrompt } from './KidPrompt';
|
|
9
10
|
export { KidQuestionCard } from './KidQuestionCard';
|
|
10
11
|
export { KidChoiceRow, optionLetter } from './KidChoiceRow';
|
|
12
|
+
export { SpeakingReplyMic } from './SpeakingReplyMic';
|
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 } 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 }) : 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: t('question.viewer.questionN', { n: questionNumber }), points: item.points || 0, isExample: item.isExample, headerExtra: _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] }), children: [toAnswerTheQuestionGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: t('question.viewer.itemImageAlt', {
|
|
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/choose-then-answer-group/ChooseThenAnswerGroupClient.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Check, Lightbulb, X } from 'lucide-react';
|
|
4
|
+
import { useT } from '../../../../shared/lib/i18n';
|
|
4
5
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
6
|
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
7
|
function emptyAnswer() {
|
|
@@ -10,33 +11,13 @@ function formatHalf(points) {
|
|
|
10
11
|
const half = points * 0.5;
|
|
11
12
|
return Number.isInteger(half) ? String(half) : half.toFixed(1);
|
|
12
13
|
}
|
|
13
|
-
function badgeClass(params) {
|
|
14
|
-
if (params.isExample) {
|
|
15
|
-
return 'bg-amber-500';
|
|
16
|
-
}
|
|
17
|
-
if (!params.isReviewMode || !params.hasAttempt) {
|
|
18
|
-
return 'bg-indigo-600';
|
|
19
|
-
}
|
|
20
|
-
if (params.partialScore != null) {
|
|
21
|
-
if (params.partialScore >= params.points - 1e-9) {
|
|
22
|
-
return 'bg-emerald-500';
|
|
23
|
-
}
|
|
24
|
-
if (params.partialScore > 0) {
|
|
25
|
-
return 'bg-amber-500';
|
|
26
|
-
}
|
|
27
|
-
return 'bg-red-500';
|
|
28
|
-
}
|
|
29
|
-
if (params.mappingCorrect || params.hasText) {
|
|
30
|
-
return 'bg-amber-500';
|
|
31
|
-
}
|
|
32
|
-
return 'bg-red-500';
|
|
33
|
-
}
|
|
34
14
|
export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], itemResults, onAnswerChange, }) {
|
|
15
|
+
const t = useT();
|
|
35
16
|
const canEdit = Boolean(onAnswerChange) && !isReviewMode;
|
|
36
17
|
const items = questionData.items || [];
|
|
37
18
|
const hasAttempt = Boolean(itemResults?.length) ||
|
|
38
19
|
userAnswers.some((answer) => Boolean(answer?.optionId || answer?.text?.trim()));
|
|
39
|
-
return (_jsxs("div", { className: "space-y-
|
|
20
|
+
return (_jsxs("div", { className: "space-y-3", children: [(questionData.title || questionData.instruction) && (_jsxs(KidInstruction, { children: [questionData.title ? (_jsx("p", { className: "mb-1 text-[11px] font-semibold text-indigo-500", children: questionData.title })) : null, questionData.instruction] })), items.map((item, itemIndex) => {
|
|
40
21
|
const stored = userAnswers[itemIndex] || emptyAnswer();
|
|
41
22
|
const current = item.isExample
|
|
42
23
|
? { optionId: item.optionId, text: stored.text }
|
|
@@ -62,38 +43,37 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
|
|
|
62
43
|
text: next.text ?? current.text,
|
|
63
44
|
});
|
|
64
45
|
};
|
|
65
|
-
return (_jsx(KidQuestionCard, { title:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
: 'bg-red-50 text-red-700'), children: ["Vi\u1EBFt ", result.aiCorrect ? halfLabel : '0'] })) : null] })) : null, canEdit || hasStudentText || (isReviewMode && hasAttempt) ? (_jsxs("label", { className: "block space-y-1", children: [_jsx("span", { className: "text-xs font-medium text-slate-500", children: "C\u00E2u tr\u1EA3 l\u1EDDi c\u1EE7a b\u1EA1n" }), isReviewMode && !hasStudentText ? (_jsx("p", { className: "rounded-lg border border-dashed border-slate-200 bg-slate-50 px-3 py-2 text-sm italic text-slate-400", children: "Ch\u01B0a vi\u1EBFt" })) : (_jsx("textarea", { value: current.text, disabled: !canEdit || item.isExample, onChange: (event) => setAnswer({ text: event.target.value }), placeholder: "Vi\u1EBFt c\u00E2u tr\u1EA3 l\u1EDDi t\u1EA1i \u0111\u00E2y...", rows: 2, className: "min-h-0 w-full resize-none rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm leading-relaxed text-slate-800 outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 disabled:bg-slate-50" }))] })) : null, isReviewMode && sampleAnswers.length > 0 ? (_jsxs("p", { className: "text-xs text-slate-500", children: [_jsx("span", { className: "font-semibold", children: "M\u1EABu:" }), " ", sampleAnswers.join(' · ')] })) : null] })] }) }, `${item.questionNumber}-${itemIndex}`));
|
|
46
|
+
return (_jsx(KidQuestionCard, { title: t('question.viewer.questionN', { n: item.questionNumber || itemIndex + 1 }), points: points, isExample: item.isExample, children: _jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "min-w-0 space-y-2.5", children: [(parts[0] || parts.slice(1).join('{0}')) ? (_jsxs("p", { className: "text-sm font-semibold leading-7 text-slate-800", children: [_jsx("span", { children: parts[0] || '' }), _jsx("span", { className: "mx-1 inline-block min-w-[3rem] border-b-2 border-dashed border-slate-300 align-baseline" }), _jsx("span", { children: parts.slice(1).join('{0}') })] })) : null, _jsx("div", { className: "flex flex-wrap items-center gap-2.5", role: "group", "aria-label": t('question.viewer.questionN', {
|
|
47
|
+
n: item.questionNumber || itemIndex + 1,
|
|
48
|
+
}), children: item.options.map((option) => {
|
|
49
|
+
const selected = current.optionId === option.id;
|
|
50
|
+
const isKey = option.id === expectedId;
|
|
51
|
+
const pickedCorrect = showGradedReview && selected && isKey;
|
|
52
|
+
const pickedWrong = showGradedReview && selected && !isKey;
|
|
53
|
+
const keyOnly = showGradedReview && !selected && isKey;
|
|
54
|
+
const officialCorrect = showAnswerKey && isKey;
|
|
55
|
+
const optionId = `cta-${itemIndex}-opt-${option.id}`;
|
|
56
|
+
return (_jsxs("button", { id: optionId, name: `cta-${itemIndex}-option`, type: "button", disabled: !canEdit || item.isExample, onClick: () => setAnswer({ optionId: option.id }), className: cn('inline-flex min-w-[3.5rem] items-center justify-center gap-1 rounded-full border px-3 py-1.5 text-sm font-semibold transition-colors', !showReview &&
|
|
57
|
+
selected &&
|
|
58
|
+
'border-blue-500 bg-blue-500 text-white shadow-sm ring-2 ring-blue-100', !showReview &&
|
|
59
|
+
!selected &&
|
|
60
|
+
'border-gray-200 bg-white text-slate-800 hover:border-blue-300 hover:bg-blue-50', showAnswerKey &&
|
|
61
|
+
!isKey &&
|
|
62
|
+
'border-slate-200 bg-white text-slate-800', officialCorrect &&
|
|
63
|
+
'border-2 border-emerald-500 bg-white text-slate-800', pickedCorrect && 'border-emerald-600 bg-emerald-600 text-white', pickedWrong && 'border-red-500 bg-red-500 text-white', keyOnly && 'border-emerald-500 bg-white text-emerald-800', showGradedReview &&
|
|
64
|
+
!selected &&
|
|
65
|
+
!isKey &&
|
|
66
|
+
'border-slate-200 bg-slate-50 text-slate-400', canEdit &&
|
|
67
|
+
!item.isExample &&
|
|
68
|
+
'cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-200', (!canEdit || item.isExample) && 'cursor-default'), children: [option.text || option.id, pickedCorrect ? _jsx(Check, { className: "h-3 w-3" }) : null, pickedWrong ? _jsx(X, { className: "h-3 w-3" }) : null] }, option.id));
|
|
69
|
+
}) }), showReview && hasAttempt ? (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [_jsx("span", { className: cn('rounded-full px-2 py-0.5 text-[11px] font-semibold', mappingCorrect
|
|
70
|
+
? 'bg-emerald-50 text-emerald-700'
|
|
71
|
+
: 'bg-red-50 text-red-700'), children: t('question.viewer.chooseScore', {
|
|
72
|
+
score: mappingCorrect ? halfLabel : '0',
|
|
73
|
+
}) }), result?.aiCorrect !== undefined ? (_jsx("span", { className: cn('rounded-full px-2 py-0.5 text-[11px] font-semibold', result.aiCorrect
|
|
74
|
+
? 'bg-emerald-50 text-emerald-700'
|
|
75
|
+
: 'bg-red-50 text-red-700'), children: t('question.viewer.writeScore', {
|
|
76
|
+
score: result.aiCorrect ? halfLabel : '0',
|
|
77
|
+
}) })) : null] })) : null, canEdit || hasStudentText || (isReviewMode && hasAttempt) ? (_jsxs("label", { className: "block space-y-1", htmlFor: `cta-${itemIndex}-text`, children: [_jsx("span", { className: "text-xs font-medium text-slate-500", children: t('question.viewer.yourAnswer') }), isReviewMode && !hasStudentText ? (_jsx("p", { className: "rounded-lg border border-dashed border-slate-200 bg-slate-50 px-3 py-2 text-sm italic text-slate-400", children: t('question.viewer.notWritten') })) : (_jsx("textarea", { id: `cta-${itemIndex}-text`, name: `cta-${itemIndex}-text`, value: current.text, disabled: !canEdit || item.isExample, onChange: (event) => setAnswer({ text: event.target.value }), placeholder: t('question.viewer.writePlaceholder'), rows: 2, className: "min-h-0 w-full resize-none rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm leading-relaxed text-slate-800 outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 disabled:bg-slate-50" }))] })) : null, isReviewMode && sampleAnswers.length > 0 ? (_jsxs("p", { className: "text-xs text-slate-500", children: [_jsxs("span", { className: "font-semibold", children: [t('question.viewer.sample'), ":"] }), ' ', sampleAnswers.join(' · ')] })) : null] }) }) }, `${item.questionNumber}-${itemIndex}`));
|
|
98
78
|
}), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-amber-100 bg-amber-50 px-3 py-2 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
|
|
99
79
|
}
|
|
@@ -6,6 +6,9 @@ export interface SpeakingCueCardClientProps {
|
|
|
6
6
|
explanation?: string;
|
|
7
7
|
};
|
|
8
8
|
isReviewMode?: boolean;
|
|
9
|
+
audioKey?: string | null;
|
|
10
|
+
uploading?: boolean;
|
|
11
|
+
onRecorded?: (blob: Blob) => void;
|
|
9
12
|
}
|
|
10
13
|
export declare function mapQuestionToSpeakingCueCardData(question: Record<string, unknown> | null | undefined): SpeakingCueCardClientProps['questionData'];
|
|
11
|
-
export declare function SpeakingCueCardClient({ questionData, isReviewMode, }: SpeakingCueCardClientProps): import("react").JSX.Element;
|
|
14
|
+
export declare function SpeakingCueCardClient({ questionData, isReviewMode, audioKey, uploading, onRecorded, }: SpeakingCueCardClientProps): import("react").JSX.Element;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { CheckCircle2, Info, CreditCard } from 'lucide-react';
|
|
4
4
|
import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
|
|
5
|
-
import { KidQuestionCard } from '../../_shared/components/compact';
|
|
5
|
+
import { KidQuestionCard, SpeakingReplyMic } from '../../_shared/components/compact';
|
|
6
6
|
function asRecord(value) {
|
|
7
7
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
8
8
|
? value
|
|
@@ -22,7 +22,7 @@ export function mapQuestionToSpeakingCueCardData(question) {
|
|
|
22
22
|
explanation: typeof source.explanation === 'string' ? source.explanation : '',
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
export function SpeakingCueCardClient({ questionData, isReviewMode = false, }) {
|
|
25
|
+
export function SpeakingCueCardClient({ questionData, isReviewMode = false, audioKey = null, uploading = false, onRecorded, }) {
|
|
26
26
|
const answers = questionData.expectedAnswers || [];
|
|
27
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "Th\u1EBB g\u1EE3i \u00FD (Cue Card)", icon: _jsx(CreditCard, { className: "h-3.5 w-3.5" }), children: [_jsx("div", { className: "rounded-xl border border-purple-100 bg-purple-50/40 px-5 py-4", children: _jsx(RichTextHtml, { html: questionData.cueCardHtml, className: "text-sm text-slate-800", fallback: _jsx("p", { className: "text-sm text-gray-400", children: "Ch\u01B0a c\u00F3 n\u1ED9i dung cue card" }) }) }),
|
|
27
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "Th\u1EBB g\u1EE3i \u00FD (Cue Card)", icon: _jsx(CreditCard, { className: "h-3.5 w-3.5" }), children: [_jsx("div", { className: "rounded-xl border border-purple-100 bg-purple-50/40 px-5 py-4", children: _jsx(RichTextHtml, { html: questionData.cueCardHtml, className: "text-sm text-slate-800", fallback: _jsx("p", { className: "text-sm text-gray-400", children: "Ch\u01B0a c\u00F3 n\u1ED9i dung cue card" }) }) }), isReviewMode && !onRecorded ? (audioKey ? (_jsx(SpeakingReplyMic, { flattenIndex: 0, audioKey: audioKey, disabled: true })) : (_jsx("p", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 b\u1EA3n ghi." }))) : (_jsx(SpeakingReplyMic, { flattenIndex: 0, audioKey: audioKey, uploading: uploading, onRecorded: onRecorded ? (_index, blob) => onRecorded(blob) : undefined })), isReviewMode && answers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "\u0110\u00E1p \u00E1n m\u1EABu" })] }), _jsx("div", { className: "space-y-2", children: answers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, `${answer}-${index}`))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-amber-700 leading-relaxed whitespace-pre-wrap", children: questionData.explanation })] }))] })] }));
|
|
28
28
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
import { Lightbulb, Mic, Square, Volume2 } from 'lucide-react';
|
|
3
|
+
import { Lightbulb, Mic, Volume2 } from 'lucide-react';
|
|
5
4
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
|
-
import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
5
|
+
import { KidInstruction, KidPrompt, KidQuestionCard, SpeakingReplyMic, } from '../../_shared/components/compact';
|
|
7
6
|
function QuestionAudio({ url }) {
|
|
8
7
|
const { previewUrl, isLoading } = usePresignedFileUrl(url);
|
|
9
8
|
const src = previewUrl ||
|
|
@@ -34,78 +33,6 @@ function findGradeDetail(details, flattenIndex) {
|
|
|
34
33
|
item.itemId === `item-${flattenIndex}` ||
|
|
35
34
|
item.itemId === `item-${flattenIndex + 1}`));
|
|
36
35
|
}
|
|
37
|
-
function SpeakingReplyMic({ flattenIndex, audioKey, disabled, uploading, onRecorded, }) {
|
|
38
|
-
const [status, setStatus] = useState('idle');
|
|
39
|
-
const [error, setError] = useState('');
|
|
40
|
-
const [blobUrl, setBlobUrl] = useState('');
|
|
41
|
-
const recorderRef = useRef(null);
|
|
42
|
-
const chunksRef = useRef([]);
|
|
43
|
-
const streamRef = useRef(null);
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
return () => {
|
|
46
|
-
recorderRef.current?.stop();
|
|
47
|
-
streamRef.current?.getTracks().forEach((track) => track.stop());
|
|
48
|
-
if (blobUrl) {
|
|
49
|
-
URL.revokeObjectURL(blobUrl);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
}, [blobUrl]);
|
|
53
|
-
const stopStream = () => {
|
|
54
|
-
streamRef.current?.getTracks().forEach((track) => track.stop());
|
|
55
|
-
streamRef.current = null;
|
|
56
|
-
};
|
|
57
|
-
const startRecording = async () => {
|
|
58
|
-
if (disabled || uploading || !onRecorded) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
setError('');
|
|
62
|
-
try {
|
|
63
|
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
64
|
-
streamRef.current = stream;
|
|
65
|
-
chunksRef.current = [];
|
|
66
|
-
const recorder = new MediaRecorder(stream);
|
|
67
|
-
recorderRef.current = recorder;
|
|
68
|
-
recorder.ondataavailable = (event) => {
|
|
69
|
-
if (event.data.size > 0) {
|
|
70
|
-
chunksRef.current.push(event.data);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
recorder.onstop = () => {
|
|
74
|
-
const blob = new Blob(chunksRef.current, { type: recorder.mimeType || 'audio/webm' });
|
|
75
|
-
setBlobUrl((current) => {
|
|
76
|
-
if (current) {
|
|
77
|
-
URL.revokeObjectURL(current);
|
|
78
|
-
}
|
|
79
|
-
return URL.createObjectURL(blob);
|
|
80
|
-
});
|
|
81
|
-
setStatus('recorded');
|
|
82
|
-
stopStream();
|
|
83
|
-
onRecorded(flattenIndex, blob);
|
|
84
|
-
};
|
|
85
|
-
recorder.start();
|
|
86
|
-
setStatus('recording');
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
setError('Không thể mở microphone. Hãy cho phép quyền ghi âm.');
|
|
90
|
-
stopStream();
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
const stopRecording = () => {
|
|
94
|
-
if (recorderRef.current && recorderRef.current.state !== 'inactive') {
|
|
95
|
-
recorderRef.current.stop();
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
const canRecord = Boolean(onRecorded) && !disabled && !uploading;
|
|
99
|
-
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'
|
|
100
|
-
? 'animate-pulse bg-red-500 hover:bg-red-600'
|
|
101
|
-
: '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
|
|
102
|
-
? 'Đang tải lên bản ghi...'
|
|
103
|
-
: status === 'recording'
|
|
104
|
-
? 'Đang ghi âm... Nhấn để dừng'
|
|
105
|
-
: audioKey || status === 'recorded'
|
|
106
|
-
? 'Đã ghi. Nhấn micro để ghi lại'
|
|
107
|
-
: '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] }));
|
|
108
|
-
}
|
|
109
36
|
export function SpontaneousQaGroupClient({ questionData, isReviewMode = false, userAnswer, onItemRecorded, uploadingIndex = null, gradeDetails, disabled = false, }) {
|
|
110
37
|
const parts = questionData.parts || [];
|
|
111
38
|
const audioKeys = Array.isArray(userAnswer?.audioKeys) ? userAnswer.audioKeys : [];
|
package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js
CHANGED
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
4
4
|
import { Check, X, Library } from 'lucide-react';
|
|
5
|
+
import { useT } from '../../../../shared/lib/i18n';
|
|
5
6
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
7
|
+
import { toMediaUrlList } from '../../../../shared/lib/utils/media-urls';
|
|
6
8
|
import { cn } from '../../../../shared/lib/utils';
|
|
7
9
|
import { KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
8
10
|
import { detectWfsfBankMode, isOptionsWordEntry, normalizeSharedWordBank, } from './wfsf-word-bank.utils';
|
|
9
11
|
import { expandWfsfCorrectAlts, formatWfsfCorrectLabel, isWfsfBlankCorrect, primaryWfsfAnswers, } from '../../../../shared/lib/utils/wfsf-blank-match';
|
|
12
|
+
function wfsfFilledText(value) {
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return String(value[0] ?? '');
|
|
15
|
+
return value == null ? '' : String(value);
|
|
16
|
+
}
|
|
10
17
|
function normalizeWfsfBlankStyle(raw) {
|
|
11
18
|
const v = String(raw || '').toLowerCase();
|
|
12
19
|
if (v === 'letter' || v === 'box')
|
|
@@ -61,28 +68,14 @@ function ClientAudioItem({ url }) {
|
|
|
61
68
|
return null;
|
|
62
69
|
return (_jsx("div", { className: "w-full max-w-md", children: _jsx("audio", { controls: true, src: src, className: "w-full h-10 rounded-md" }) }));
|
|
63
70
|
}
|
|
64
|
-
const DEFAULT_WFSF_HEADER = 'Điền từ vào biểu mẫu có cấu trúc';
|
|
65
71
|
export function WordFillStructuredFormClient({ questionData, isReviewMode = false, userAnswer, onAnswerChange, autoFillCorrectAnswers = false, instruction, questionNumber, explanation, }) {
|
|
66
72
|
const { title = '', content = '', answers = {}, caseSensitive = false, blankStyle: rawBlankStyle, viewMode, wordBank = [], instruction: dataInstruction, } = questionData;
|
|
73
|
+
const t = useT();
|
|
67
74
|
const blankStyle = normalizeWfsfBlankStyle(rawBlankStyle);
|
|
68
75
|
const isLetterBlank = blankStyle === 'letter';
|
|
69
76
|
const isPhraseBlank = blankStyle === 'phrase' || blankStyle === 'translate';
|
|
70
|
-
const allImages = useMemo(() =>
|
|
71
|
-
|
|
72
|
-
return questionData.imageUrls.filter(Boolean);
|
|
73
|
-
}
|
|
74
|
-
if (questionData.imageUrl)
|
|
75
|
-
return [questionData.imageUrl];
|
|
76
|
-
return [];
|
|
77
|
-
}, [questionData.imageUrls, questionData.imageUrl]);
|
|
78
|
-
const allAudios = useMemo(() => {
|
|
79
|
-
if (Array.isArray(questionData.audioUrls) && questionData.audioUrls.length > 0) {
|
|
80
|
-
return questionData.audioUrls.filter(Boolean);
|
|
81
|
-
}
|
|
82
|
-
if (questionData.audioUrl)
|
|
83
|
-
return [questionData.audioUrl];
|
|
84
|
-
return [];
|
|
85
|
-
}, [questionData.audioUrls, questionData.audioUrl]);
|
|
77
|
+
const allImages = useMemo(() => toMediaUrlList(questionData.imageUrls?.length ? questionData.imageUrls : questionData.imageUrl), [questionData.imageUrls, questionData.imageUrl]);
|
|
78
|
+
const allAudios = useMemo(() => toMediaUrlList(questionData.audioUrls?.length ? questionData.audioUrls : questionData.audioUrl), [questionData.audioUrls, questionData.audioUrl]);
|
|
86
79
|
const [filledAnswers, setFilledAnswers] = useState(() => {
|
|
87
80
|
if (userAnswer)
|
|
88
81
|
return { ...userAnswer };
|
|
@@ -133,16 +126,16 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
133
126
|
return a.localeCompare(b);
|
|
134
127
|
});
|
|
135
128
|
}, [content]);
|
|
136
|
-
const allFilled = expectedBlankKeys.every(k => filledAnswers[k]
|
|
137
|
-
const isAllCorrect = isReviewMode && expectedBlankKeys.every((k) => isWfsfBlankCorrect(filledAnswers[k]
|
|
129
|
+
const allFilled = expectedBlankKeys.every((k) => wfsfFilledText(filledAnswers[k]).trim());
|
|
130
|
+
const isAllCorrect = isReviewMode && expectedBlankKeys.every((k) => isWfsfBlankCorrect(wfsfFilledText(filledAnswers[k]), answers[k], caseSensitive));
|
|
138
131
|
// Header = page rubric. Pearson preview maps draft.instruction → explanation.
|
|
139
132
|
const rawExplanation = String(explanation || questionData.explanation || '').trim();
|
|
140
133
|
const headerInstruction = String(instruction || dataInstruction || rawExplanation || '').trim();
|
|
141
134
|
// Header reads "Bài x: rubric" — the question type name is never shown.
|
|
142
135
|
const numberLabel = typeof questionNumber === 'number' && questionNumber > 0
|
|
143
|
-
?
|
|
136
|
+
? t('question.viewer.partN', { n: questionNumber })
|
|
144
137
|
: '';
|
|
145
|
-
const headerTitle = numberLabel ? (_jsxs(_Fragment, { children: [_jsxs("strong", { className: "font-bold", children: [numberLabel, headerInstruction ? ':' : ''] }), headerInstruction ? ` ${headerInstruction}` : ''] })) : (headerInstruction ||
|
|
138
|
+
const headerTitle = numberLabel ? (_jsxs(_Fragment, { children: [_jsxs("strong", { className: "font-bold", children: [numberLabel, headerInstruction ? ':' : ''] }), headerInstruction ? ` ${headerInstruction}` : ''] })) : (headerInstruction || t('question.viewer.wfsfDefaultTitle'));
|
|
146
139
|
// Do not re-show the same rubric under "Giải thích".
|
|
147
140
|
const displayExplanation = rawExplanation && rawExplanation !== headerInstruction ? rawExplanation : '';
|
|
148
141
|
const handleInputChange = useCallback((key, value) => {
|
|
@@ -159,7 +152,7 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
159
152
|
setExpandedBlank(null);
|
|
160
153
|
}, [isReviewMode, onAnswerChange]);
|
|
161
154
|
const renderBlank = useCallback((key, reactKey) => {
|
|
162
|
-
const filled = filledAnswers[key]
|
|
155
|
+
const filled = wfsfFilledText(filledAnswers[key]);
|
|
163
156
|
const isCorrectFill = isReviewMode && isWfsfBlankCorrect(filled, answers[key], caseSensitive);
|
|
164
157
|
const isWrongFill = isReviewMode && !!filled && !isCorrectFill;
|
|
165
158
|
const blankOptions = optionsByKey.get(key);
|
package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js
CHANGED
|
@@ -15,6 +15,7 @@ import { Textarea } from '../../../../components/ui/textarea';
|
|
|
15
15
|
import { Switch } from '../../../../components/ui/switch';
|
|
16
16
|
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
17
17
|
import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
|
|
18
|
+
import { toMediaUrlList } from '../../../../shared/lib/utils/media-urls';
|
|
18
19
|
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
19
20
|
import { ImagePreview } from '../../../../components/ui/image-preview';
|
|
20
21
|
import { WordFillStructuredFormClient } from './WordFillStructuredFormClient';
|
|
@@ -141,14 +142,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
|
|
|
141
142
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
142
143
|
const [blankStyle, setBlankStyle] = useState(initialData?.blankStyle ?? 'word');
|
|
143
144
|
// ── Multiple Images & Audios ────────────────────────────────────────────────
|
|
144
|
-
const [imageUrls, setImageUrls] = useState(() =>
|
|
145
|
-
if (Array.isArray(initialData?.imageUrls) && initialData.imageUrls.length > 0) {
|
|
146
|
-
return initialData.imageUrls.filter(Boolean);
|
|
147
|
-
}
|
|
148
|
-
if (initialData?.imageUrl)
|
|
149
|
-
return [initialData.imageUrl];
|
|
150
|
-
return [];
|
|
151
|
-
});
|
|
145
|
+
const [imageUrls, setImageUrls] = useState(() => toMediaUrlList(initialData?.imageUrls?.length ? initialData.imageUrls : initialData?.imageUrl));
|
|
152
146
|
const [audioUrls, setAudioUrls] = useState(() => {
|
|
153
147
|
if (Array.isArray(initialData?.audioUrls) && initialData.audioUrls.length > 0) {
|
|
154
148
|
return initialData.audioUrls.filter(Boolean);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toMediaUrlList } from '../../../../shared/lib/utils/media-urls';
|
|
1
2
|
import { normalizeWfsfAnswersMap } from '../../../../shared/lib/utils/wfsf-blank-match';
|
|
2
3
|
import { detectWfsfBankMode, normalizeOptionsWordBank, normalizeSharedWordBank, } from './wfsf-word-bank.utils';
|
|
3
4
|
function asRecord(value) {
|
|
@@ -79,19 +80,11 @@ export function mapQuestionToWfsfData(question) {
|
|
|
79
80
|
? normalizeOptionsWordBank(rawWordBank, blankKeys)
|
|
80
81
|
: undefined;
|
|
81
82
|
const groupPayload = asRecord(asRecord(source.questionGroup).payload);
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
? rawImageUrls.filter(Boolean)
|
|
85
|
-
: (apiContent.imageUrl || answer.imageUrl ? [String(apiContent.imageUrl || answer.imageUrl)] : []);
|
|
86
|
-
const rawAudioUrls = apiContent.audioUrls || answer.audioUrls;
|
|
87
|
-
const audioUrls = Array.isArray(rawAudioUrls)
|
|
88
|
-
? rawAudioUrls.filter(Boolean)
|
|
89
|
-
: (apiContent.audioUrl || answer.audioUrl || source.audio_url
|
|
90
|
-
? [String(apiContent.audioUrl || answer.audioUrl || source.audio_url)]
|
|
91
|
-
: []);
|
|
83
|
+
const imageUrls = toMediaUrlList(apiContent.imageUrls || answer.imageUrls || apiContent.imageUrl || answer.imageUrl);
|
|
84
|
+
const audioUrls = toMediaUrlList(apiContent.audioUrls || answer.audioUrls || apiContent.audioUrl || answer.audioUrl || source.audio_url);
|
|
92
85
|
return {
|
|
93
86
|
title: String(apiContent.title || answer.title || ''),
|
|
94
|
-
imageUrl: imageUrls[0] ||
|
|
87
|
+
imageUrl: imageUrls[0] || '',
|
|
95
88
|
imageUrls,
|
|
96
89
|
audioUrl: audioUrls[0] || String(apiContent.audioUrl || answer.audioUrl || source.audio_url || ''),
|
|
97
90
|
audioUrls,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toMediaUrlList } from '../../../../shared/lib/utils/media-urls';
|
|
1
2
|
import { compactWfsfAnswersMap } from '../../../../shared/lib/utils/wfsf-blank-match';
|
|
2
3
|
/**
|
|
3
4
|
* Transform WORD_FILL_STRUCTURED_FORM creator data → API format
|
|
@@ -19,17 +20,11 @@ export const transformWordFillStructuredForm = (question) => {
|
|
|
19
20
|
answerData.groupSubtitle ||
|
|
20
21
|
answerData.groupContent ||
|
|
21
22
|
answerData.groupImageUrl);
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
? rawImageUrls.filter(Boolean)
|
|
25
|
-
: (answerData.imageUrl ? [answerData.imageUrl] : []);
|
|
26
|
-
const rawAudioUrls = answerData.audioUrls;
|
|
27
|
-
const audioUrls = Array.isArray(rawAudioUrls)
|
|
28
|
-
? rawAudioUrls.filter(Boolean)
|
|
29
|
-
: (answerData.audioUrl ? [answerData.audioUrl] : []);
|
|
23
|
+
const imageUrls = toMediaUrlList(answerData.imageUrls ?? answerData.imageUrl);
|
|
24
|
+
const audioUrls = toMediaUrlList(answerData.audioUrls ?? answerData.audioUrl);
|
|
30
25
|
const apiContent = {
|
|
31
26
|
title: answerData.title || '',
|
|
32
|
-
imageUrl: imageUrls[0] ||
|
|
27
|
+
imageUrl: imageUrls[0] || '',
|
|
33
28
|
imageUrls,
|
|
34
29
|
audioUrl: audioUrls[0] || answerData.audioUrl || '',
|
|
35
30
|
audioUrls,
|
|
@@ -12,6 +12,8 @@ interface QuestionViewerProps {
|
|
|
12
12
|
partialScore?: number;
|
|
13
13
|
feedback?: string;
|
|
14
14
|
}>;
|
|
15
|
+
onItemRecorded?: (flattenIndex: number, blob: Blob) => void;
|
|
16
|
+
uploadingIndex?: number | null;
|
|
15
17
|
}
|
|
16
|
-
export declare function QuestionViewer({ question, onSubmitAnswer, isReviewMode, userAnswer, gradeDetails, }: QuestionViewerProps): import("react").JSX.Element;
|
|
18
|
+
export declare function QuestionViewer({ question, onSubmitAnswer, isReviewMode, userAnswer, gradeDetails, onItemRecorded, uploadingIndex, }: QuestionViewerProps): import("react").JSX.Element;
|
|
17
19
|
export {};
|
|
@@ -6,7 +6,7 @@ import { Input } from '../../../components/ui/input';
|
|
|
6
6
|
import { Check, Volume2, X } from 'lucide-react';
|
|
7
7
|
import { useRef, useState } from 'react';
|
|
8
8
|
import { toast } from 'sonner';
|
|
9
|
-
import { t } from '../../../shared/lib/i18n';
|
|
9
|
+
import { t, useT } from '../../../shared/lib/i18n';
|
|
10
10
|
import { ReadAndColorObjectsClient } from '../types/read-and-color-objects/ReadAndColorObjectsClient';
|
|
11
11
|
import { ArrangeLettersIntoWordsClient } from '../types/arrange-letters-into-words/ArrangeLettersIntoWordsClient';
|
|
12
12
|
import { WriteShortParagraphClient } from '../types/write-short-paragraph/WriteShortParagraphClient';
|
|
@@ -38,7 +38,14 @@ import { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
|
|
|
38
38
|
import { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
|
|
39
39
|
import { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
|
|
40
40
|
import { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
|
|
41
|
-
|
|
41
|
+
function readCueAudioKey(value) {
|
|
42
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
43
|
+
return null;
|
|
44
|
+
const key = value.audioKey;
|
|
45
|
+
return typeof key === 'string' && key.trim() !== '' ? key : null;
|
|
46
|
+
}
|
|
47
|
+
export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false, userAnswer, gradeDetails, onItemRecorded, uploadingIndex = null, }) {
|
|
48
|
+
const translate = useT();
|
|
42
49
|
const [selectedAnswer, setSelectedAnswer] = useState(userAnswer || null);
|
|
43
50
|
const [playCount, setPlayCount] = useState(0);
|
|
44
51
|
const audioRef = useRef(null);
|
|
@@ -214,6 +221,7 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
214
221
|
next[index] = value;
|
|
215
222
|
setFillBlankAnswers(next);
|
|
216
223
|
setSelectedAnswer(next);
|
|
224
|
+
onSubmitAnswer?.(next);
|
|
217
225
|
};
|
|
218
226
|
const isCorrect = (index, value) => {
|
|
219
227
|
const correctAnswer = blanks[index] ?? '';
|
|
@@ -517,11 +525,13 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
517
525
|
case 'ANSWER_THE_QUESTION_GROUP':
|
|
518
526
|
return (_jsx(AnswerTheQuestionGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
519
527
|
case 'SPEAKING_CUE_CARD':
|
|
520
|
-
return (_jsx(SpeakingCueCardClient, { questionData: mapQuestionToSpeakingCueCardData(question), isReviewMode: isReviewMode
|
|
528
|
+
return (_jsx(SpeakingCueCardClient, { questionData: mapQuestionToSpeakingCueCardData(question), isReviewMode: isReviewMode, audioKey: readCueAudioKey(userAnswer), uploading: uploadingIndex === 0, onRecorded: isReviewMode || !onItemRecorded
|
|
529
|
+
? undefined
|
|
530
|
+
: (blob) => onItemRecorded(0, blob) }));
|
|
521
531
|
case 'SPONTANEOUS_QA':
|
|
522
532
|
return (_jsx(SpontaneousQAClient, { questionData: mapQuestionToSpontaneousQAData(question), isReviewMode: isReviewMode }));
|
|
523
533
|
case 'SPONTANEOUS_QA_GROUP':
|
|
524
|
-
return (_jsx(SpontaneousQaGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, gradeDetails: gradeDetails, disabled: !onSubmitAnswer }));
|
|
534
|
+
return (_jsx(SpontaneousQaGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, gradeDetails: gradeDetails, onItemRecorded: isReviewMode ? undefined : onItemRecorded, uploadingIndex: uploadingIndex, disabled: !onSubmitAnswer && !onItemRecorded }));
|
|
525
535
|
case 'WORD_ORDER_AND_MATCH_GROUP':
|
|
526
536
|
case 'WORD_ORDER_AND_MATCH':
|
|
527
537
|
return (_jsx(WordOrderAndMatchGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
|
|
@@ -572,7 +582,7 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
572
582
|
case 'CHOOSE_THEN_ANSWER_GROUP':
|
|
573
583
|
return (_jsx(ChooseThenAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, gradeDetails: gradeDetails, onSubmitAnswer: onSubmitAnswer }));
|
|
574
584
|
default:
|
|
575
|
-
return
|
|
585
|
+
return _jsx("div", { children: t('question.viewer.unsupported', { type: question.type }) });
|
|
576
586
|
}
|
|
577
587
|
};
|
|
578
588
|
/**
|
|
@@ -588,25 +598,32 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
588
598
|
question.type === 'ANSWER_THE_QUESTION') {
|
|
589
599
|
return null;
|
|
590
600
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
return 'Câu hỏi';
|
|
595
|
-
}
|
|
601
|
+
const typeKey = `question.type.${question.type}`;
|
|
602
|
+
const typeLabel = translate(typeKey);
|
|
603
|
+
const typedTitle = typeLabel === typeKey ? translate('question.viewer.fallbackTitle') : typeLabel;
|
|
596
604
|
if (typeof question.content === 'string') {
|
|
597
|
-
return stripLightweightMarkdown(question.content) ||
|
|
605
|
+
return stripLightweightMarkdown(question.content) || typedTitle;
|
|
598
606
|
}
|
|
599
607
|
const c = question.content;
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
if (
|
|
603
|
-
return stripLightweightMarkdown(
|
|
608
|
+
const metaTitle = typeof c?.meta?.title === 'string' ? c.meta.title : '';
|
|
609
|
+
const explicitTitle = (typeof c?.title === 'string' && c.title) || metaTitle;
|
|
610
|
+
if (explicitTitle && explicitTitle.length <= 80 && !/\{\d+\}/.test(explicitTitle) && !/\*\*/.test(explicitTitle)) {
|
|
611
|
+
return stripLightweightMarkdown(explicitTitle) || typedTitle;
|
|
612
|
+
}
|
|
613
|
+
// Modern fill stems live entirely in the body; title is a short type label.
|
|
614
|
+
if (question.type === 'FILL_IN_BLANK' ||
|
|
615
|
+
question.type === 'WORD_FILL_PARAGRAPH') {
|
|
616
|
+
return typedTitle;
|
|
617
|
+
}
|
|
618
|
+
const raw = c?.question || c?.paragraph || '';
|
|
604
619
|
if (typeof raw === 'string' && (/\{\d+\}/.test(raw) || /\*\*/.test(raw))) {
|
|
605
|
-
return
|
|
620
|
+
return typedTitle;
|
|
606
621
|
}
|
|
607
|
-
|
|
622
|
+
const stripped = stripLightweightMarkdown(typeof raw === 'string' ? raw : '');
|
|
623
|
+
return stripped && stripped.length <= 80 ? stripped : typedTitle;
|
|
608
624
|
};
|
|
609
|
-
const
|
|
625
|
+
const isGroupQuestion = question.type.endsWith('_GROUP') || Boolean(question.isGroup);
|
|
626
|
+
const cardTitle = isGroupQuestion ? null : resolveCardTitle();
|
|
610
627
|
// WFSF and Crossword already have their own bordered shell — avoid double Card border.
|
|
611
628
|
if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
|
|
612
629
|
question.type === 'CROSSWORD_PUZZLE' ||
|
|
@@ -614,7 +631,7 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
614
631
|
question.type === 'ANSWER_THE_QUESTION') {
|
|
615
632
|
return (_jsx("div", { className: "space-y-4", children: renderQuestion() }));
|
|
616
633
|
}
|
|
617
|
-
return (_jsxs(Card, { children: [cardTitle ? (_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center justify-between", children: [_jsx("span", { className: "min-w-0 pr-2 text-base font-semibold leading-snug", children: cardTitle }),
|
|
634
|
+
return (_jsxs(Card, { children: [cardTitle ? (_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center justify-between", children: [_jsx("span", { className: "min-w-0 pr-2 text-base font-semibold leading-snug", children: cardTitle }), _jsx("span", { className: "shrink-0 text-sm font-normal text-gray-500", children: translate('question.viewer.points', { count: question.points }) })] }) })) : null, _jsx(CardContent, { className: cardTitle ? 'space-y-4' : 'space-y-4 pt-4', children: renderQuestion() })] }));
|
|
618
635
|
}
|
|
619
636
|
/** Drop lightweight markdown markers for plain header labels (not full token parse). */
|
|
620
637
|
function stripLightweightMarkdown(text) {
|
|
@@ -15,5 +15,5 @@ function readStudentAnswer(value) {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
export function SpontaneousQaGroupViewer({ question, isReviewMode = false, userAnswer, onItemRecorded, uploadingIndex, gradeDetails, disabled, }) {
|
|
18
|
-
return (_jsx(SpontaneousQaGroupClient, { questionData: mapQuestionToSpontaneousQaGroupData(question), isReviewMode: isReviewMode, userAnswer: readStudentAnswer(userAnswer), onItemRecorded: isReviewMode ? undefined : onItemRecorded, uploadingIndex: uploadingIndex, gradeDetails:
|
|
18
|
+
return (_jsx(SpontaneousQaGroupClient, { questionData: mapQuestionToSpontaneousQaGroupData(question), isReviewMode: isReviewMode, userAnswer: readStudentAnswer(userAnswer), onItemRecorded: isReviewMode ? undefined : onItemRecorded, uploadingIndex: uploadingIndex, gradeDetails: gradeDetails, disabled: disabled }));
|
|
19
19
|
}
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export * from './constants/QueryKeyConstant';
|
|
|
3
3
|
export * from './constants/exam-level.enum';
|
|
4
4
|
export * from './constants/routes';
|
|
5
5
|
export * from './lib/utils';
|
|
6
|
+
export { useLocaleStore } from './lib/stores/localeStore';
|
|
7
|
+
export type { Locale as ExamSdkLocale } from './lib/stores/localeStore';
|
|
6
8
|
export * from '../components/questions/_shared';
|
|
7
9
|
export * from './types';
|
|
8
10
|
export * from './lib/utils/question-transform';
|
package/dist/shared/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from './constants/QueryKeyConstant';
|
|
|
3
3
|
export * from './constants/exam-level.enum';
|
|
4
4
|
export * from './constants/routes';
|
|
5
5
|
export * from './lib/utils';
|
|
6
|
+
export { useLocaleStore } from './lib/stores/localeStore';
|
|
6
7
|
export * from '../components/questions/_shared';
|
|
7
8
|
export * from './types';
|
|
8
9
|
export * from './lib/utils/question-transform';
|
|
@@ -120,5 +120,43 @@ declare const common: {
|
|
|
120
120
|
readonly 'common.badge.source.createdByAdmin': "Created by admin";
|
|
121
121
|
readonly 'common.badge.source.createdByTeacher': "Created by teacher";
|
|
122
122
|
readonly 'common.badge.source.createdFromEBM': "Synced from EBM";
|
|
123
|
+
readonly 'question.viewer.fallbackTitle': "Question";
|
|
124
|
+
readonly 'question.viewer.points': "{count} pts";
|
|
125
|
+
readonly 'question.viewer.questionN': "Question {n}";
|
|
126
|
+
readonly 'question.viewer.partN': "Part {n}";
|
|
127
|
+
readonly 'question.viewer.example': "Example";
|
|
128
|
+
readonly 'question.viewer.correct': "Correct";
|
|
129
|
+
readonly 'question.viewer.wrong': "Incorrect";
|
|
130
|
+
readonly 'question.viewer.readingPassage': "Reading passage";
|
|
131
|
+
readonly 'question.viewer.placeholder': "Enter your answer…";
|
|
132
|
+
readonly 'question.viewer.yourAnswer': "Your answer";
|
|
133
|
+
readonly 'question.viewer.writePlaceholder': "Write your answer here…";
|
|
134
|
+
readonly 'question.viewer.answerKey': "Answer key";
|
|
135
|
+
readonly 'question.viewer.noAnswer': "No answer yet";
|
|
136
|
+
readonly 'question.viewer.notWritten': "Not written";
|
|
137
|
+
readonly 'question.viewer.sample': "Sample";
|
|
138
|
+
readonly 'question.viewer.chooseScore': "Choose {score}";
|
|
139
|
+
readonly 'question.viewer.writeScore': "Write {score}";
|
|
140
|
+
readonly 'question.viewer.expandPassage': "Show more";
|
|
141
|
+
readonly 'question.viewer.collapsePassage': "Show less";
|
|
142
|
+
readonly 'question.viewer.unsupported': "Unsupported question type: {type}";
|
|
143
|
+
readonly 'question.viewer.missingPrompt': "Question text is missing";
|
|
144
|
+
readonly 'question.viewer.imageAlt': "Image {n}";
|
|
145
|
+
readonly 'question.viewer.itemImageAlt': "Question {n} image {m}";
|
|
146
|
+
readonly 'question.viewer.atqgDefaultInstruction': "Read the text and answer the questions.";
|
|
147
|
+
readonly 'question.viewer.wfsfDefaultTitle': "Fill in the structured form";
|
|
148
|
+
readonly 'question.type.ANSWER_THE_QUESTION_GROUP': "Answer the questions";
|
|
149
|
+
readonly 'question.type.CHOOSE_THEN_ANSWER_GROUP': "Choose then answer";
|
|
150
|
+
readonly 'question.type.FILL_IN_BLANK': "Fill in the blank";
|
|
151
|
+
readonly 'question.type.FILL_IN_BLANK_GROUP': "Fill in the blanks";
|
|
152
|
+
readonly 'question.type.WORD_FILL_STRUCTURED_FORM': "Structured form";
|
|
153
|
+
readonly 'question.type.WORD_FILL_PARAGRAPH': "Fill the paragraph";
|
|
154
|
+
readonly 'question.type.MATCHING_WITH_LINES_GROUP': "Match the columns";
|
|
155
|
+
readonly 'question.type.MATCH_WORD_TO_PICTURE': "Match words to pictures";
|
|
156
|
+
readonly 'question.type.MATCH_WORD_TO_PICTURE_GROUP': "Match words to pictures";
|
|
157
|
+
readonly 'question.type.CROSSWORD_PUZZLE': "Crossword";
|
|
158
|
+
readonly 'question.type.FIND_WORDS_IN_MATRIX': "Find the words";
|
|
159
|
+
readonly 'question.type.CHOOSE_THE_CORRECT_ANSWER': "Choose the correct answer";
|
|
160
|
+
readonly 'question.type.CHOOSE_THE_CORRECT_ANSWER_GROUP': "Choose the correct answers";
|
|
123
161
|
};
|
|
124
162
|
export default common;
|
|
@@ -136,5 +136,44 @@ const common = {
|
|
|
136
136
|
'common.badge.source.createdByAdmin': 'Created by admin',
|
|
137
137
|
'common.badge.source.createdByTeacher': 'Created by teacher',
|
|
138
138
|
'common.badge.source.createdFromEBM': 'Synced from EBM',
|
|
139
|
+
// Question viewer chrome (homework / examine)
|
|
140
|
+
'question.viewer.fallbackTitle': 'Question',
|
|
141
|
+
'question.viewer.points': '{count} pts',
|
|
142
|
+
'question.viewer.questionN': 'Question {n}',
|
|
143
|
+
'question.viewer.partN': 'Part {n}',
|
|
144
|
+
'question.viewer.example': 'Example',
|
|
145
|
+
'question.viewer.correct': 'Correct',
|
|
146
|
+
'question.viewer.wrong': 'Incorrect',
|
|
147
|
+
'question.viewer.readingPassage': 'Reading passage',
|
|
148
|
+
'question.viewer.placeholder': 'Enter your answer…',
|
|
149
|
+
'question.viewer.yourAnswer': 'Your answer',
|
|
150
|
+
'question.viewer.writePlaceholder': 'Write your answer here…',
|
|
151
|
+
'question.viewer.answerKey': 'Answer key',
|
|
152
|
+
'question.viewer.noAnswer': 'No answer yet',
|
|
153
|
+
'question.viewer.notWritten': 'Not written',
|
|
154
|
+
'question.viewer.sample': 'Sample',
|
|
155
|
+
'question.viewer.chooseScore': 'Choose {score}',
|
|
156
|
+
'question.viewer.writeScore': 'Write {score}',
|
|
157
|
+
'question.viewer.expandPassage': 'Show more',
|
|
158
|
+
'question.viewer.collapsePassage': 'Show less',
|
|
159
|
+
'question.viewer.unsupported': 'Unsupported question type: {type}',
|
|
160
|
+
'question.viewer.missingPrompt': 'Question text is missing',
|
|
161
|
+
'question.viewer.imageAlt': 'Image {n}',
|
|
162
|
+
'question.viewer.itemImageAlt': 'Question {n} image {m}',
|
|
163
|
+
'question.viewer.atqgDefaultInstruction': 'Read the text and answer the questions.',
|
|
164
|
+
'question.viewer.wfsfDefaultTitle': 'Fill in the structured form',
|
|
165
|
+
'question.type.ANSWER_THE_QUESTION_GROUP': 'Answer the questions',
|
|
166
|
+
'question.type.CHOOSE_THEN_ANSWER_GROUP': 'Choose then answer',
|
|
167
|
+
'question.type.FILL_IN_BLANK': 'Fill in the blank',
|
|
168
|
+
'question.type.FILL_IN_BLANK_GROUP': 'Fill in the blanks',
|
|
169
|
+
'question.type.WORD_FILL_STRUCTURED_FORM': 'Structured form',
|
|
170
|
+
'question.type.WORD_FILL_PARAGRAPH': 'Fill the paragraph',
|
|
171
|
+
'question.type.MATCHING_WITH_LINES_GROUP': 'Match the columns',
|
|
172
|
+
'question.type.MATCH_WORD_TO_PICTURE': 'Match words to pictures',
|
|
173
|
+
'question.type.MATCH_WORD_TO_PICTURE_GROUP': 'Match words to pictures',
|
|
174
|
+
'question.type.CROSSWORD_PUZZLE': 'Crossword',
|
|
175
|
+
'question.type.FIND_WORDS_IN_MATRIX': 'Find the words',
|
|
176
|
+
'question.type.CHOOSE_THE_CORRECT_ANSWER': 'Choose the correct answer',
|
|
177
|
+
'question.type.CHOOSE_THE_CORRECT_ANSWER_GROUP': 'Choose the correct answers',
|
|
139
178
|
};
|
|
140
179
|
export default common;
|
|
@@ -2297,5 +2297,43 @@ declare const en: {
|
|
|
2297
2297
|
readonly 'common.badge.source.createdByAdmin': "Created by admin";
|
|
2298
2298
|
readonly 'common.badge.source.createdByTeacher': "Created by teacher";
|
|
2299
2299
|
readonly 'common.badge.source.createdFromEBM': "Synced from EBM";
|
|
2300
|
+
readonly 'question.viewer.fallbackTitle': "Question";
|
|
2301
|
+
readonly 'question.viewer.points': "{count} pts";
|
|
2302
|
+
readonly 'question.viewer.questionN': "Question {n}";
|
|
2303
|
+
readonly 'question.viewer.partN': "Part {n}";
|
|
2304
|
+
readonly 'question.viewer.example': "Example";
|
|
2305
|
+
readonly 'question.viewer.correct': "Correct";
|
|
2306
|
+
readonly 'question.viewer.wrong': "Incorrect";
|
|
2307
|
+
readonly 'question.viewer.readingPassage': "Reading passage";
|
|
2308
|
+
readonly 'question.viewer.placeholder': "Enter your answer…";
|
|
2309
|
+
readonly 'question.viewer.yourAnswer': "Your answer";
|
|
2310
|
+
readonly 'question.viewer.writePlaceholder': "Write your answer here…";
|
|
2311
|
+
readonly 'question.viewer.answerKey': "Answer key";
|
|
2312
|
+
readonly 'question.viewer.noAnswer': "No answer yet";
|
|
2313
|
+
readonly 'question.viewer.notWritten': "Not written";
|
|
2314
|
+
readonly 'question.viewer.sample': "Sample";
|
|
2315
|
+
readonly 'question.viewer.chooseScore': "Choose {score}";
|
|
2316
|
+
readonly 'question.viewer.writeScore': "Write {score}";
|
|
2317
|
+
readonly 'question.viewer.expandPassage': "Show more";
|
|
2318
|
+
readonly 'question.viewer.collapsePassage': "Show less";
|
|
2319
|
+
readonly 'question.viewer.unsupported': "Unsupported question type: {type}";
|
|
2320
|
+
readonly 'question.viewer.missingPrompt': "Question text is missing";
|
|
2321
|
+
readonly 'question.viewer.imageAlt': "Image {n}";
|
|
2322
|
+
readonly 'question.viewer.itemImageAlt': "Question {n} image {m}";
|
|
2323
|
+
readonly 'question.viewer.atqgDefaultInstruction': "Read the text and answer the questions.";
|
|
2324
|
+
readonly 'question.viewer.wfsfDefaultTitle': "Fill in the structured form";
|
|
2325
|
+
readonly 'question.type.ANSWER_THE_QUESTION_GROUP': "Answer the questions";
|
|
2326
|
+
readonly 'question.type.CHOOSE_THEN_ANSWER_GROUP': "Choose then answer";
|
|
2327
|
+
readonly 'question.type.FILL_IN_BLANK': "Fill in the blank";
|
|
2328
|
+
readonly 'question.type.FILL_IN_BLANK_GROUP': "Fill in the blanks";
|
|
2329
|
+
readonly 'question.type.WORD_FILL_STRUCTURED_FORM': "Structured form";
|
|
2330
|
+
readonly 'question.type.WORD_FILL_PARAGRAPH': "Fill the paragraph";
|
|
2331
|
+
readonly 'question.type.MATCHING_WITH_LINES_GROUP': "Match the columns";
|
|
2332
|
+
readonly 'question.type.MATCH_WORD_TO_PICTURE': "Match words to pictures";
|
|
2333
|
+
readonly 'question.type.MATCH_WORD_TO_PICTURE_GROUP': "Match words to pictures";
|
|
2334
|
+
readonly 'question.type.CROSSWORD_PUZZLE': "Crossword";
|
|
2335
|
+
readonly 'question.type.FIND_WORDS_IN_MATRIX': "Find the words";
|
|
2336
|
+
readonly 'question.type.CHOOSE_THE_CORRECT_ANSWER': "Choose the correct answer";
|
|
2337
|
+
readonly 'question.type.CHOOSE_THE_CORRECT_ANSWER_GROUP': "Choose the correct answers";
|
|
2300
2338
|
};
|
|
2301
2339
|
export default en;
|
|
@@ -120,5 +120,43 @@ declare const common: {
|
|
|
120
120
|
readonly 'common.badge.source.createdByAdmin': "Admin tạo";
|
|
121
121
|
readonly 'common.badge.source.createdByTeacher': "Giáo viên tạo";
|
|
122
122
|
readonly 'common.badge.source.createdFromEBM': "Đồng bộ EBM";
|
|
123
|
+
readonly 'question.viewer.fallbackTitle': "Câu hỏi";
|
|
124
|
+
readonly 'question.viewer.points': "{count} điểm";
|
|
125
|
+
readonly 'question.viewer.questionN': "Câu {n}";
|
|
126
|
+
readonly 'question.viewer.partN': "Bài {n}";
|
|
127
|
+
readonly 'question.viewer.example': "Câu ví dụ";
|
|
128
|
+
readonly 'question.viewer.correct': "Đúng";
|
|
129
|
+
readonly 'question.viewer.wrong': "Sai";
|
|
130
|
+
readonly 'question.viewer.readingPassage': "Bài đọc";
|
|
131
|
+
readonly 'question.viewer.placeholder': "Nhập câu trả lời...";
|
|
132
|
+
readonly 'question.viewer.yourAnswer': "Câu trả lời của bạn";
|
|
133
|
+
readonly 'question.viewer.writePlaceholder': "Viết câu trả lời tại đây...";
|
|
134
|
+
readonly 'question.viewer.answerKey': "Đáp án";
|
|
135
|
+
readonly 'question.viewer.noAnswer': "Chưa có đáp án";
|
|
136
|
+
readonly 'question.viewer.notWritten': "Chưa viết";
|
|
137
|
+
readonly 'question.viewer.sample': "Mẫu";
|
|
138
|
+
readonly 'question.viewer.chooseScore': "Chọn {score}";
|
|
139
|
+
readonly 'question.viewer.writeScore': "Viết {score}";
|
|
140
|
+
readonly 'question.viewer.expandPassage': "Xem thêm";
|
|
141
|
+
readonly 'question.viewer.collapsePassage': "Thu gọn";
|
|
142
|
+
readonly 'question.viewer.unsupported': "Loại câu hỏi không được hỗ trợ: {type}";
|
|
143
|
+
readonly 'question.viewer.missingPrompt': "Câu hỏi chưa được nhập";
|
|
144
|
+
readonly 'question.viewer.imageAlt': "Hình ảnh ({n})";
|
|
145
|
+
readonly 'question.viewer.itemImageAlt': "Hình ảnh câu {n} ({m})";
|
|
146
|
+
readonly 'question.viewer.atqgDefaultInstruction': "Đọc đoạn văn và trả lời câu hỏi.";
|
|
147
|
+
readonly 'question.viewer.wfsfDefaultTitle': "Điền từ vào biểu mẫu có cấu trúc";
|
|
148
|
+
readonly 'question.type.ANSWER_THE_QUESTION_GROUP': "Trả lời câu hỏi";
|
|
149
|
+
readonly 'question.type.CHOOSE_THEN_ANSWER_GROUP': "Chọn rồi trả lời";
|
|
150
|
+
readonly 'question.type.FILL_IN_BLANK': "Điền từ";
|
|
151
|
+
readonly 'question.type.FILL_IN_BLANK_GROUP': "Điền từ";
|
|
152
|
+
readonly 'question.type.WORD_FILL_STRUCTURED_FORM': "Điền từ vào biểu mẫu";
|
|
153
|
+
readonly 'question.type.WORD_FILL_PARAGRAPH': "Điền từ vào đoạn";
|
|
154
|
+
readonly 'question.type.MATCHING_WITH_LINES_GROUP': "Nối cột";
|
|
155
|
+
readonly 'question.type.MATCH_WORD_TO_PICTURE': "Nối từ với tranh";
|
|
156
|
+
readonly 'question.type.MATCH_WORD_TO_PICTURE_GROUP': "Nối từ với tranh";
|
|
157
|
+
readonly 'question.type.CROSSWORD_PUZZLE': "Ô chữ";
|
|
158
|
+
readonly 'question.type.FIND_WORDS_IN_MATRIX': "Tìm từ";
|
|
159
|
+
readonly 'question.type.CHOOSE_THE_CORRECT_ANSWER': "Chọn đáp án đúng";
|
|
160
|
+
readonly 'question.type.CHOOSE_THE_CORRECT_ANSWER_GROUP': "Chọn đáp án đúng";
|
|
123
161
|
};
|
|
124
162
|
export default common;
|
|
@@ -136,5 +136,44 @@ const common = {
|
|
|
136
136
|
'common.badge.source.createdByAdmin': 'Admin tạo',
|
|
137
137
|
'common.badge.source.createdByTeacher': 'Giáo viên tạo',
|
|
138
138
|
'common.badge.source.createdFromEBM': 'Đồng bộ EBM',
|
|
139
|
+
// Question viewer chrome (homework / examine)
|
|
140
|
+
'question.viewer.fallbackTitle': 'Câu hỏi',
|
|
141
|
+
'question.viewer.points': '{count} điểm',
|
|
142
|
+
'question.viewer.questionN': 'Câu {n}',
|
|
143
|
+
'question.viewer.partN': 'Bài {n}',
|
|
144
|
+
'question.viewer.example': 'Câu ví dụ',
|
|
145
|
+
'question.viewer.correct': 'Đúng',
|
|
146
|
+
'question.viewer.wrong': 'Sai',
|
|
147
|
+
'question.viewer.readingPassage': 'Bài đọc',
|
|
148
|
+
'question.viewer.placeholder': 'Nhập câu trả lời...',
|
|
149
|
+
'question.viewer.yourAnswer': 'Câu trả lời của bạn',
|
|
150
|
+
'question.viewer.writePlaceholder': 'Viết câu trả lời tại đây...',
|
|
151
|
+
'question.viewer.answerKey': 'Đáp án',
|
|
152
|
+
'question.viewer.noAnswer': 'Chưa có đáp án',
|
|
153
|
+
'question.viewer.notWritten': 'Chưa viết',
|
|
154
|
+
'question.viewer.sample': 'Mẫu',
|
|
155
|
+
'question.viewer.chooseScore': 'Chọn {score}',
|
|
156
|
+
'question.viewer.writeScore': 'Viết {score}',
|
|
157
|
+
'question.viewer.expandPassage': 'Xem thêm',
|
|
158
|
+
'question.viewer.collapsePassage': 'Thu gọn',
|
|
159
|
+
'question.viewer.unsupported': 'Loại câu hỏi không được hỗ trợ: {type}',
|
|
160
|
+
'question.viewer.missingPrompt': 'Câu hỏi chưa được nhập',
|
|
161
|
+
'question.viewer.imageAlt': 'Hình ảnh ({n})',
|
|
162
|
+
'question.viewer.itemImageAlt': 'Hình ảnh câu {n} ({m})',
|
|
163
|
+
'question.viewer.atqgDefaultInstruction': 'Đọc đoạn văn và trả lời câu hỏi.',
|
|
164
|
+
'question.viewer.wfsfDefaultTitle': 'Điền từ vào biểu mẫu có cấu trúc',
|
|
165
|
+
'question.type.ANSWER_THE_QUESTION_GROUP': 'Trả lời câu hỏi',
|
|
166
|
+
'question.type.CHOOSE_THEN_ANSWER_GROUP': 'Chọn rồi trả lời',
|
|
167
|
+
'question.type.FILL_IN_BLANK': 'Điền từ',
|
|
168
|
+
'question.type.FILL_IN_BLANK_GROUP': 'Điền từ',
|
|
169
|
+
'question.type.WORD_FILL_STRUCTURED_FORM': 'Điền từ vào biểu mẫu',
|
|
170
|
+
'question.type.WORD_FILL_PARAGRAPH': 'Điền từ vào đoạn',
|
|
171
|
+
'question.type.MATCHING_WITH_LINES_GROUP': 'Nối cột',
|
|
172
|
+
'question.type.MATCH_WORD_TO_PICTURE': 'Nối từ với tranh',
|
|
173
|
+
'question.type.MATCH_WORD_TO_PICTURE_GROUP': 'Nối từ với tranh',
|
|
174
|
+
'question.type.CROSSWORD_PUZZLE': 'Ô chữ',
|
|
175
|
+
'question.type.FIND_WORDS_IN_MATRIX': 'Tìm từ',
|
|
176
|
+
'question.type.CHOOSE_THE_CORRECT_ANSWER': 'Chọn đáp án đúng',
|
|
177
|
+
'question.type.CHOOSE_THE_CORRECT_ANSWER_GROUP': 'Chọn đáp án đúng',
|
|
139
178
|
};
|
|
140
179
|
export default common;
|
|
@@ -2297,5 +2297,43 @@ declare const vi: {
|
|
|
2297
2297
|
readonly 'common.badge.source.createdByAdmin': "Admin tạo";
|
|
2298
2298
|
readonly 'common.badge.source.createdByTeacher': "Giáo viên tạo";
|
|
2299
2299
|
readonly 'common.badge.source.createdFromEBM': "Đồng bộ EBM";
|
|
2300
|
+
readonly 'question.viewer.fallbackTitle': "Câu hỏi";
|
|
2301
|
+
readonly 'question.viewer.points': "{count} điểm";
|
|
2302
|
+
readonly 'question.viewer.questionN': "Câu {n}";
|
|
2303
|
+
readonly 'question.viewer.partN': "Bài {n}";
|
|
2304
|
+
readonly 'question.viewer.example': "Câu ví dụ";
|
|
2305
|
+
readonly 'question.viewer.correct': "Đúng";
|
|
2306
|
+
readonly 'question.viewer.wrong': "Sai";
|
|
2307
|
+
readonly 'question.viewer.readingPassage': "Bài đọc";
|
|
2308
|
+
readonly 'question.viewer.placeholder': "Nhập câu trả lời...";
|
|
2309
|
+
readonly 'question.viewer.yourAnswer': "Câu trả lời của bạn";
|
|
2310
|
+
readonly 'question.viewer.writePlaceholder': "Viết câu trả lời tại đây...";
|
|
2311
|
+
readonly 'question.viewer.answerKey': "Đáp án";
|
|
2312
|
+
readonly 'question.viewer.noAnswer': "Chưa có đáp án";
|
|
2313
|
+
readonly 'question.viewer.notWritten': "Chưa viết";
|
|
2314
|
+
readonly 'question.viewer.sample': "Mẫu";
|
|
2315
|
+
readonly 'question.viewer.chooseScore': "Chọn {score}";
|
|
2316
|
+
readonly 'question.viewer.writeScore': "Viết {score}";
|
|
2317
|
+
readonly 'question.viewer.expandPassage': "Xem thêm";
|
|
2318
|
+
readonly 'question.viewer.collapsePassage': "Thu gọn";
|
|
2319
|
+
readonly 'question.viewer.unsupported': "Loại câu hỏi không được hỗ trợ: {type}";
|
|
2320
|
+
readonly 'question.viewer.missingPrompt': "Câu hỏi chưa được nhập";
|
|
2321
|
+
readonly 'question.viewer.imageAlt': "Hình ảnh ({n})";
|
|
2322
|
+
readonly 'question.viewer.itemImageAlt': "Hình ảnh câu {n} ({m})";
|
|
2323
|
+
readonly 'question.viewer.atqgDefaultInstruction': "Đọc đoạn văn và trả lời câu hỏi.";
|
|
2324
|
+
readonly 'question.viewer.wfsfDefaultTitle': "Điền từ vào biểu mẫu có cấu trúc";
|
|
2325
|
+
readonly 'question.type.ANSWER_THE_QUESTION_GROUP': "Trả lời câu hỏi";
|
|
2326
|
+
readonly 'question.type.CHOOSE_THEN_ANSWER_GROUP': "Chọn rồi trả lời";
|
|
2327
|
+
readonly 'question.type.FILL_IN_BLANK': "Điền từ";
|
|
2328
|
+
readonly 'question.type.FILL_IN_BLANK_GROUP': "Điền từ";
|
|
2329
|
+
readonly 'question.type.WORD_FILL_STRUCTURED_FORM': "Điền từ vào biểu mẫu";
|
|
2330
|
+
readonly 'question.type.WORD_FILL_PARAGRAPH': "Điền từ vào đoạn";
|
|
2331
|
+
readonly 'question.type.MATCHING_WITH_LINES_GROUP': "Nối cột";
|
|
2332
|
+
readonly 'question.type.MATCH_WORD_TO_PICTURE': "Nối từ với tranh";
|
|
2333
|
+
readonly 'question.type.MATCH_WORD_TO_PICTURE_GROUP': "Nối từ với tranh";
|
|
2334
|
+
readonly 'question.type.CROSSWORD_PUZZLE': "Ô chữ";
|
|
2335
|
+
readonly 'question.type.FIND_WORDS_IN_MATRIX': "Tìm từ";
|
|
2336
|
+
readonly 'question.type.CHOOSE_THE_CORRECT_ANSWER': "Chọn đáp án đúng";
|
|
2337
|
+
readonly 'question.type.CHOOSE_THE_CORRECT_ANSWER_GROUP': "Chọn đáp án đúng";
|
|
2300
2338
|
};
|
|
2301
2339
|
export default vi;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Normalize creator / bank `imageUrl` that may be a string, string[], or Array#toString join. */
|
|
2
|
+
export function toMediaUrlList(value) {
|
|
3
|
+
if (Array.isArray(value)) {
|
|
4
|
+
return value.flatMap((item) => toMediaUrlList(item));
|
|
5
|
+
}
|
|
6
|
+
if (typeof value !== 'string') {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
if (!trimmed) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
if (trimmed.includes(',') && /https?:\/\//i.test(trimmed)) {
|
|
14
|
+
const parts = trimmed
|
|
15
|
+
.split(',')
|
|
16
|
+
.map((part) => part.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
if (parts.length > 1 && parts.every((part) => /^https?:\/\//i.test(part) || !part.includes(' '))) {
|
|
19
|
+
return parts;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return [trimmed];
|
|
23
|
+
}
|