@tinyweb_dev/oe-exam-sdk 1.0.11 → 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.
Files changed (32) hide show
  1. package/dist/components/questions/_shared/components/compact/KidPassage.d.ts +4 -0
  2. package/dist/components/questions/_shared/components/compact/KidPassage.js +21 -0
  3. package/dist/components/questions/_shared/components/compact/KidQuestionCard.d.ts +5 -2
  4. package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +4 -2
  5. package/dist/components/questions/_shared/components/compact/SpeakingReplyMic.d.ts +7 -0
  6. package/dist/components/questions/_shared/components/compact/SpeakingReplyMic.js +86 -0
  7. package/dist/components/questions/_shared/components/compact/index.d.ts +2 -0
  8. package/dist/components/questions/_shared/components/compact/index.js +2 -0
  9. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +14 -5
  10. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -2
  11. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +35 -55
  12. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.d.ts +4 -1
  13. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +4 -4
  14. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +2 -75
  15. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +32 -35
  16. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +2 -8
  17. package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +4 -11
  18. package/dist/components/questions/types/word-fill-structured-form/transform.js +4 -9
  19. package/dist/components/questions/viewer/QuestionViewer.d.ts +3 -1
  20. package/dist/components/questions/viewer/QuestionViewer.js +36 -19
  21. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +1 -1
  22. package/dist/shared/index.d.ts +2 -0
  23. package/dist/shared/index.js +1 -0
  24. package/dist/shared/lib/i18n/messages/en/common.d.ts +38 -0
  25. package/dist/shared/lib/i18n/messages/en/common.js +39 -0
  26. package/dist/shared/lib/i18n/messages/en.d.ts +38 -0
  27. package/dist/shared/lib/i18n/messages/vi/common.d.ts +38 -0
  28. package/dist/shared/lib/i18n/messages/vi/common.js +39 -0
  29. package/dist/shared/lib/i18n/messages/vi.d.ts +38 -0
  30. package/dist/shared/lib/utils/media-urls.d.ts +2 -0
  31. package/dist/shared/lib/utils/media-urls.js +23 -0
  32. package/package.json +1 -1
@@ -1,12 +1,19 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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
- import { Check, X, FileText, Library } from 'lucide-react';
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
- import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
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
- if (Array.isArray(questionData.imageUrls) && questionData.imageUrls.length > 0) {
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,14 +126,18 @@ 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]?.trim());
137
- const isAllCorrect = isReviewMode && expectedBlankKeys.every((k) => isWfsfBlankCorrect(filledAnswers[k] || '', answers[k], caseSensitive));
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
- const headerLabel = headerInstruction || DEFAULT_WFSF_HEADER;
134
+ // Header reads "Bài x: rubric" — the question type name is never shown.
135
+ const numberLabel = typeof questionNumber === 'number' && questionNumber > 0
136
+ ? t('question.viewer.partN', { n: questionNumber })
137
+ : '';
138
+ const headerTitle = numberLabel ? (_jsxs(_Fragment, { children: [_jsxs("strong", { className: "font-bold", children: [numberLabel, headerInstruction ? ':' : ''] }), headerInstruction ? ` ${headerInstruction}` : ''] })) : (headerInstruction || t('question.viewer.wfsfDefaultTitle'));
142
139
  // Do not re-show the same rubric under "Giải thích".
143
- const displayExplanation = rawExplanation && rawExplanation !== headerLabel ? rawExplanation : '';
140
+ const displayExplanation = rawExplanation && rawExplanation !== headerInstruction ? rawExplanation : '';
144
141
  const handleInputChange = useCallback((key, value) => {
145
142
  if (isReviewMode)
146
143
  return;
@@ -155,7 +152,7 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
155
152
  setExpandedBlank(null);
156
153
  }, [isReviewMode, onAnswerChange]);
157
154
  const renderBlank = useCallback((key, reactKey) => {
158
- const filled = filledAnswers[key] || '';
155
+ const filled = wfsfFilledText(filledAnswers[key]);
159
156
  const isCorrectFill = isReviewMode && isWfsfBlankCorrect(filled, answers[key], caseSensitive);
160
157
  const isWrongFill = isReviewMode && !!filled && !isCorrectFill;
161
158
  const blankOptions = optionsByKey.get(key);
@@ -178,8 +175,8 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
178
175
  : undefined, size: isLetterBlank ? undefined : isPhraseBlank ? 28 : 10, className: cn('inline-block bg-slate-50/40 text-center text-sm font-semibold outline-none transition-all align-baseline hover:bg-slate-100/60 focus:bg-teal-50/60 focus:ring-0', isLetterBlank
179
176
  ? 'h-7 w-7 rounded-md border px-0 uppercase'
180
177
  : isPhraseBlank
181
- ? 'min-w-[16rem] max-w-[36rem] w-[28ch] rounded-md border px-3 py-1.5'
182
- : 'min-w-[4.5rem] max-w-[8rem] w-[10ch] rounded-md border px-3 py-1.5', isReviewMode
178
+ ? 'min-w-[16rem] max-w-[36rem] w-[28ch] rounded-none border-0 border-b-2 bg-transparent px-1 py-0.5'
179
+ : 'min-w-[4.5rem] max-w-[8rem] w-[10ch] rounded-none border-0 border-b-2 bg-transparent px-1 py-0.5', isReviewMode
183
180
  ? isCorrectFill
184
181
  ? 'border-green-400 bg-green-50 text-green-800'
185
182
  : isWrongFill
@@ -278,12 +275,12 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
278
275
  .map((node, index) => renderNode(node, `root-${index}`))
279
276
  .filter((node) => node !== null);
280
277
  }, [content, renderBlank]);
281
- return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: headerInstruction }), _jsxs(KidQuestionCard, { icon: _jsx(FileText, { className: "h-3.5 w-3.5" }), title: headerInstruction ? DEFAULT_WFSF_HEADER : headerLabel, headerExtra: typeof questionNumber === 'number' && questionNumber > 0 ? (_jsxs("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-teal-700", children: ["B\u00E0i ", questionNumber] })) : undefined, review: isReviewMode && allFilled ? (isAllCorrect ? 'correct' : 'wrong') : null, children: [title && (_jsx(KidPrompt, { children: _jsx("h2", { className: "text-sm font-semibold text-gray-800", children: title }) })), allAudios.length > 0 && (_jsx("div", { className: "flex flex-col items-center gap-2", children: allAudios.map((audio, idx) => (_jsx(ClientAudioItem, { url: audio }, idx))) })), allImages.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: allImages.map((img, idx) => (_jsx(ClientImageItem, { url: img, alt: title ? `${title} (${idx + 1})` : undefined }, idx))) })), bankMode === 'WITH_WORD_BANK' && sharedWords.length > 0 && (_jsxs("div", { className: "rounded-lg border border-purple-200 bg-gradient-to-r from-purple-50 to-pink-50 p-3", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [_jsx(Library, { className: "h-4 w-4 text-purple-600" }), _jsx("span", { className: "text-xs font-semibold uppercase tracking-wider text-purple-700", children: "Word Bank" })] }), _jsx("div", { className: "flex flex-wrap gap-2", children: sharedWords.map((item, index) => {
282
- const used = usedSharedWords.has(item.word.trim().toLowerCase());
283
- return (_jsxs("span", { className: cn('rounded-full border px-3 py-1 text-sm font-medium', item.isExample
284
- ? 'border-amber-300 bg-amber-100 text-amber-800'
285
- : used
286
- ? 'border-purple-200 bg-white/60 text-purple-400 line-through'
287
- : 'border-purple-300 bg-white text-purple-800'), children: [item.word, item.isExample ? ' (ex)' : ''] }, `${item.word}-${index}`));
288
- }) })] })), _jsx("div", { className: "structured-form-content space-y-2 rounded-lg border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-5 text-base leading-loose text-gray-800", children: renderedContent }), isReviewMode && !isAllCorrect && (_jsxs("div", { className: "rounded-lg border border-green-200 bg-green-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-green-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: expectedBlankKeys.map((key) => (_jsxs("span", { className: "rounded-lg border border-green-300 bg-green-100 px-3 py-1.5 text-sm font-semibold text-green-800", children: [`{${key}}`, ": ", formatWfsfCorrectLabel(answers[key])] }, key))) })] })), isReviewMode && displayExplanation && (_jsxs("div", { className: "rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-blue-600", children: "Gi\u1EA3i th\u00EDch" }), _jsx("p", { className: "mt-1 text-sm text-blue-800", children: displayExplanation })] }))] })] }));
278
+ return (_jsx("div", { className: "space-y-4", children: _jsxs(KidQuestionCard, { hideIcon: true, title: headerTitle, titleClassName: "whitespace-normal break-words text-base font-normal text-gray-800", review: isReviewMode && allFilled ? (isAllCorrect ? 'correct' : 'wrong') : null, children: [title && (_jsx(KidPrompt, { children: _jsx("h2", { className: "text-sm font-semibold text-gray-800", children: title }) })), allAudios.length > 0 && (_jsx("div", { className: "flex flex-col items-center gap-2", children: allAudios.map((audio, idx) => (_jsx(ClientAudioItem, { url: audio }, idx))) })), allImages.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: allImages.map((img, idx) => (_jsx(ClientImageItem, { url: img, alt: title ? `${title} (${idx + 1})` : undefined }, idx))) })), bankMode === 'WITH_WORD_BANK' && sharedWords.length > 0 && (_jsxs("div", { className: "rounded-lg border border-purple-200 bg-gradient-to-r from-purple-50 to-pink-50 p-3", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [_jsx(Library, { className: "h-4 w-4 text-purple-600" }), _jsx("span", { className: "text-xs font-semibold uppercase tracking-wider text-purple-700", children: "Word Bank" })] }), _jsx("div", { className: "flex flex-wrap gap-2", children: sharedWords.map((item, index) => {
279
+ const used = usedSharedWords.has(item.word.trim().toLowerCase());
280
+ return (_jsxs("span", { className: cn('rounded-full border px-3 py-1 text-sm font-medium', item.isExample
281
+ ? 'border-amber-300 bg-amber-100 text-amber-800'
282
+ : used
283
+ ? 'border-purple-200 bg-white/60 text-purple-400 line-through'
284
+ : 'border-purple-300 bg-white text-purple-800'), children: [item.word, item.isExample ? ' (ex)' : ''] }, `${item.word}-${index}`));
285
+ }) })] })), _jsx("div", { className: "structured-form-content space-y-2 rounded-lg border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-5 text-base leading-loose text-gray-800", children: renderedContent }), isReviewMode && !isAllCorrect && (_jsxs("div", { className: "rounded-lg border border-green-200 bg-green-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-green-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: expectedBlankKeys.map((key) => (_jsxs("span", { className: "rounded-lg border border-green-300 bg-green-100 px-3 py-1.5 text-sm font-semibold text-green-800", children: [`{${key}}`, ": ", formatWfsfCorrectLabel(answers[key])] }, key))) })] })), isReviewMode && displayExplanation && (_jsxs("div", { className: "rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-blue-600", children: "Gi\u1EA3i th\u00EDch" }), _jsx("p", { className: "mt-1 text-sm text-blue-800", children: displayExplanation })] }))] }) }));
289
286
  }
@@ -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 rawImageUrls = apiContent.imageUrls || answer.imageUrls;
83
- const imageUrls = Array.isArray(rawImageUrls)
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] || String(apiContent.imageUrl || answer.imageUrl || ''),
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 rawImageUrls = answerData.imageUrls;
23
- const imageUrls = Array.isArray(rawImageUrls)
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] || answerData.imageUrl || '',
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
- export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false, userAnswer, gradeDetails, }) {
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 _jsxs("div", { children: ["Lo\u1EA1i c\u00E2u h\u1ECFi kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3: ", question.type] });
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
- // Modern fill stems live entirely in the body; title is a short label.
592
- if (question.type === 'FILL_IN_BLANK' ||
593
- question.type === 'WORD_FILL_PARAGRAPH') {
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) || 'Câu hỏi';
605
+ return stripLightweightMarkdown(question.content) || typedTitle;
598
606
  }
599
607
  const c = question.content;
600
- const raw = c?.title || c?.question || c?.paragraph || '';
601
- // Prefer explicit title; if question/paragraph is stem-like, avoid raw dump.
602
- if (c?.title)
603
- return stripLightweightMarkdown(c.title) || 'Câu hỏi';
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 'Câu hỏi';
620
+ return typedTitle;
606
621
  }
607
- return stripLightweightMarkdown(raw) || 'Câu hỏi';
622
+ const stripped = stripLightweightMarkdown(typeof raw === 'string' ? raw : '');
623
+ return stripped && stripped.length <= 80 ? stripped : typedTitle;
608
624
  };
609
- const cardTitle = resolveCardTitle();
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 }), _jsxs("span", { className: "shrink-0 text-sm font-normal text-gray-500", children: [question.points, " \u0111i\u1EC3m"] })] }) })) : null, _jsx(CardContent, { className: cardTitle ? 'space-y-4' : 'space-y-4 pt-4', children: renderQuestion() })] }));
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: isReviewMode ? undefined : gradeDetails, disabled: disabled }));
18
+ return (_jsx(SpontaneousQaGroupClient, { questionData: mapQuestionToSpontaneousQaGroupData(question), isReviewMode: isReviewMode, userAnswer: readStudentAnswer(userAnswer), onItemRecorded: isReviewMode ? undefined : onItemRecorded, uploadingIndex: uploadingIndex, gradeDetails: gradeDetails, disabled: disabled }));
19
19
  }
@@ -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';
@@ -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;