@tinyweb_dev/oe-exam-sdk 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.js +3 -1
- package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +3 -1
- package/dist/components/questions/_shared/components/compact/KidCollapsibleBlock.d.ts +8 -0
- package/dist/components/questions/_shared/components/compact/KidCollapsibleBlock.js +16 -0
- package/dist/components/questions/_shared/components/compact/KidPassage.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidPassage.js +21 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +3 -1
- package/dist/components/questions/_shared/components/compact/SpeakingReplyMic.d.ts +7 -0
- package/dist/components/questions/_shared/components/compact/SpeakingReplyMic.js +86 -0
- package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
- package/dist/components/questions/_shared/components/compact/index.js +4 -0
- package/dist/components/questions/_shared/components/compact/reviewItemCard.d.ts +3 -0
- package/dist/components/questions/_shared/components/compact/reviewItemCard.js +11 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +14 -5
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -2
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +2 -2
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +19 -8
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +2 -2
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +36 -56
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +2 -2
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +53 -50
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +2 -2
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +8 -2
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +5 -3
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +18 -15
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +2 -2
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +2 -2
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +2 -2
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.d.ts +4 -1
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +4 -4
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +17 -82
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +2 -2
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +2 -2
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +18 -23
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +2 -8
- package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +4 -11
- package/dist/components/questions/types/word-fill-structured-form/transform.js +4 -9
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +4 -2
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +2 -2
- package/dist/components/questions/viewer/QuestionViewer.d.ts +3 -1
- package/dist/components/questions/viewer/QuestionViewer.js +37 -19
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +1 -1
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/lib/i18n/messages/en/common.d.ts +38 -0
- package/dist/shared/lib/i18n/messages/en/common.js +39 -0
- package/dist/shared/lib/i18n/messages/en.d.ts +38 -0
- package/dist/shared/lib/i18n/messages/vi/common.d.ts +38 -0
- package/dist/shared/lib/i18n/messages/vi/common.js +39 -0
- package/dist/shared/lib/i18n/messages/vi.d.ts +38 -0
- package/dist/shared/lib/utils/media-urls.d.ts +2 -0
- package/dist/shared/lib/utils/media-urls.js +23 -0
- package/package.json +1 -1
|
@@ -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 { 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 : [];
|
|
@@ -117,12 +44,20 @@ export function SpontaneousQaGroupClient({ questionData, isReviewMode = false, u
|
|
|
117
44
|
const detail = findGradeDetail(gradeDetails, currentIndex);
|
|
118
45
|
const grade = readGradeUser(detail);
|
|
119
46
|
const showMic = !isReviewMode && !question.isExample;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
47
|
+
const itemNumber = question.questionNumber || questionIndex + 1;
|
|
48
|
+
const scoreLabel = detail?.partialScore != null
|
|
49
|
+
? `${detail.partialScore} / ${question.points || 0}`
|
|
50
|
+
: null;
|
|
51
|
+
if (isReviewMode) {
|
|
52
|
+
return (_jsxs("div", { className: "flex items-start gap-3 rounded-lg border border-slate-200 bg-white px-3 py-2.5", children: [_jsx("span", { className: "mt-0.5 w-6 shrink-0 text-sm font-extrabold text-sky-800", children: itemNumber }), _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-medium leading-snug text-slate-800", children: question.questionText || 'Câu hỏi chưa được nhập' }), question.audioUrl ? _jsx(QuestionAudio, { url: question.audioUrl }) : null, detail && !question.isExample ? (_jsx("p", { className: "line-clamp-2 text-xs text-slate-600", children: [
|
|
53
|
+
grade.feedback,
|
|
54
|
+
grade.contentScore != null
|
|
55
|
+
? `Content ${Math.round(grade.contentScore)}`
|
|
56
|
+
: null,
|
|
57
|
+
]
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.join(' · ') })) : null] }), scoreLabel ? (_jsx("span", { className: "shrink-0 text-xs font-semibold text-slate-500", children: scoreLabel })) : null] }, `${part.partNumber}-${question.questionNumber}-${questionIndex}`));
|
|
60
|
+
}
|
|
61
|
+
return (_jsxs(KidQuestionCard, { title: `Câu ${itemNumber}`, points: detail?.partialScore != null ? undefined : (question.points || 0), isExample: question.isExample, headerExtra: detail?.partialScore != null ? (_jsxs("span", { className: "text-xs text-gray-500", children: [detail.partialScore, " / ", question.points || 0, " \u0111i\u1EC3m"] })) : undefined, children: [_jsx(KidPrompt, { children: question.questionText || 'Câu hỏi chưa được nhập' }), question.audioUrl ? _jsx(QuestionAudio, { url: question.audioUrl }) : null, showMic && (_jsx(SpeakingReplyMic, { flattenIndex: currentIndex, audioKey: typeof audioKey === 'string' ? audioKey : null, disabled: disabled, uploading: uploadingIndex === currentIndex, onRecorded: onItemRecorded }))] }, `${part.partNumber}-${question.questionNumber}-${questionIndex}`));
|
|
127
62
|
})] }, `part-${part.partNumber}-${partIndex}`)))] }));
|
|
128
63
|
}
|
package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Lightbulb } from 'lucide-react';
|
|
4
|
-
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
4
|
+
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
5
5
|
function emptyAnswer() {
|
|
6
6
|
return { isTrue: null, correction: '' };
|
|
7
7
|
}
|
|
@@ -25,7 +25,7 @@ export function TrueFalseCorrectGroupClient({ questionData, isReviewMode = false
|
|
|
25
25
|
const showStudentCorrection = userValue?.isTrue === false || (isReviewMode && !correctIsTrue);
|
|
26
26
|
const hasAnswered = userValue?.isTrue === true || userValue?.isTrue === false;
|
|
27
27
|
const isAnswerCorrect = userValue?.isTrue === correctIsTrue;
|
|
28
|
-
return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
28
|
+
return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnswered
|
|
29
29
|
? (isAnswerCorrect ? 'correct' : 'wrong')
|
|
30
30
|
: null, children: [_jsx(KidPrompt, { children: item.question || 'Câu hỏi chưa được nhập' }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(KidChoiceRow, { letter: "T", selected: userValue?.isTrue === true, isReviewMode: isReviewMode, isCorrect: isReviewMode && correctIsTrue, disabled: !canSelect, onClick: canSelect
|
|
31
31
|
? () => emit(itemIndex, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Lightbulb } from 'lucide-react';
|
|
4
|
-
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
4
|
+
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
5
5
|
export function TrueFalseGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
6
6
|
const items = questionData.items || [];
|
|
7
7
|
const handleSelect = (itemIndex, value) => {
|
|
@@ -19,7 +19,7 @@ export function TrueFalseGroupClient({ questionData, isReviewMode = false, userA
|
|
|
19
19
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
20
20
|
const hasAnswered = userValue !== undefined;
|
|
21
21
|
const isAnswerCorrect = userValue === correctAnswer;
|
|
22
|
-
return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
22
|
+
return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnswered
|
|
23
23
|
? (isAnswerCorrect ? 'correct' : 'wrong')
|
|
24
24
|
: null, children: [_jsx(KidPrompt, { children: item.question || 'Câu hỏi chưa được nhập' }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(KidChoiceRow, { letter: "T", selected: userValue === true, isReviewMode: isReviewMode, isCorrect: isReviewMode && correctAnswer === true, disabled: !canSelect, onClick: canSelect ? () => handleSelect(itemIndex, true) : undefined, children: "\u0110\u00FAng" }), _jsx(KidChoiceRow, { letter: "F", selected: userValue === false, isReviewMode: isReviewMode, isCorrect: isReviewMode && correctAnswer === false, disabled: !canSelect, onClick: canSelect ? () => handleSelect(itemIndex, false) : undefined, children: "Sai" })] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
25
25
|
}), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
|
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,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]
|
|
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
|
-
const numberLabel =
|
|
143
|
-
|
|
135
|
+
const numberLabel = !isReviewMode &&
|
|
136
|
+
typeof questionNumber === 'number' &&
|
|
137
|
+
questionNumber > 0
|
|
138
|
+
? t('question.viewer.partN', { n: questionNumber })
|
|
144
139
|
: '';
|
|
145
|
-
const headerTitle = numberLabel ? (_jsxs(_Fragment, { children: [_jsxs("strong", { className: "font-bold", children: [numberLabel, headerInstruction ? ':' : ''] }), headerInstruction ? ` ${headerInstruction}` : ''] })) : (headerInstruction ||
|
|
140
|
+
const headerTitle = numberLabel ? (_jsxs(_Fragment, { children: [_jsxs("strong", { className: "font-bold", children: [numberLabel, headerInstruction ? ':' : ''] }), headerInstruction ? ` ${headerInstruction}` : ''] })) : (headerInstruction || t('question.viewer.wfsfDefaultTitle'));
|
|
146
141
|
// Do not re-show the same rubric under "Giải thích".
|
|
147
142
|
const displayExplanation = rawExplanation && rawExplanation !== headerInstruction ? rawExplanation : '';
|
|
148
143
|
const handleInputChange = useCallback((key, value) => {
|
|
@@ -159,7 +154,7 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
159
154
|
setExpandedBlank(null);
|
|
160
155
|
}, [isReviewMode, onAnswerChange]);
|
|
161
156
|
const renderBlank = useCallback((key, reactKey) => {
|
|
162
|
-
const filled = filledAnswers[key]
|
|
157
|
+
const filled = wfsfFilledText(filledAnswers[key]);
|
|
163
158
|
const isCorrectFill = isReviewMode && isWfsfBlankCorrect(filled, answers[key], caseSensitive);
|
|
164
159
|
const isWrongFill = isReviewMode && !!filled && !isCorrectFill;
|
|
165
160
|
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,
|
package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { X, RotateCcw } from 'lucide-react';
|
|
5
5
|
import { cn } from '../../../../shared/lib/utils';
|
|
6
|
-
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
|
+
import { KidInstruction, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
7
7
|
export function WordOrderAndMatchGroupClient({ questionData, isReviewMode = false, userAnswers, onAnswerChange, }) {
|
|
8
8
|
const { instruction, matchOptions = ['A', 'B', 'C', 'D', 'E', 'F'], items = [] } = questionData;
|
|
9
9
|
// Initialize interactive state for each item
|
|
@@ -152,7 +152,9 @@ export function WordOrderAndMatchGroupClient({ questionData, isReviewMode = fals
|
|
|
152
152
|
const isPhraseCorrect = normalizeStr(userPhrase) === normalizeStr(item.orderedPhrase || '');
|
|
153
153
|
const isLabelCorrect = (userLabel || '').trim().toUpperCase() === (item.matchLabel || '').trim().toUpperCase();
|
|
154
154
|
const isItemAllCorrect = isPhraseCorrect && isLabelCorrect;
|
|
155
|
-
return (_jsx(KidQuestionCard, { title: isExample
|
|
155
|
+
return (_jsx(KidQuestionCard, { title: isExample
|
|
156
|
+
? 'Ví dụ (Example)'
|
|
157
|
+
: reviewItemTitle(item.questionNumber || itemIdx + 1, isReviewMode, `Câu ${item.questionNumber || itemIdx + 1}`), points: reviewItemPoints(isReviewMode, !isExample && typeof item.points === 'number' ? item.points : undefined), isExample: isExample, review: isReviewMode && !isExample ? (isItemAllCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "grid grid-cols-1 gap-4 lg:grid-cols-12", children: [_jsxs("div", { className: "space-y-3 lg:col-span-8 xl:col-span-9", children: [_jsx("div", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wider", children: "1. S\u1EAFp x\u1EBFp c\u00E1c t\u1EEB th\u00E0nh c\u00E2u ho\u00E0n ch\u1EC9nh:" }), _jsxs("div", { className: cn('min-h-[52px] rounded-xl border-2 border-dashed p-2.5 transition-colors flex flex-wrap items-center gap-1.5', state.placedIndices.length > 0
|
|
156
158
|
? 'border-blue-300 bg-blue-50/40'
|
|
157
159
|
: 'border-gray-300 bg-gray-50/60'), children: [state.placedIndices.length === 0 ? (_jsx("span", { className: "text-xs italic text-gray-400 select-none", children: "Nh\u1EA5n v\u00E0o c\u00E1c t\u1EEB b\u00EAn d\u01B0\u1EDBi \u0111\u1EC3 gh\u00E9p c\u00E2u..." })) : (state.placedIndices.map((wordIdx, orderIdx) => {
|
|
158
160
|
const word = item.scrambledWords[wordIdx];
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Lightbulb } from 'lucide-react';
|
|
4
4
|
import { WordOrderingClient } from '../word-ordering/WordOrderingClient';
|
|
5
|
-
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
5
|
+
import { KidInstruction, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
6
6
|
export function WordOrderingGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
7
7
|
const items = questionData.items || [];
|
|
8
8
|
const handleItemSubmit = (itemIndex, ordered) => {
|
|
@@ -17,7 +17,7 @@ export function WordOrderingGroupClient({ questionData, isReviewMode = false, us
|
|
|
17
17
|
});
|
|
18
18
|
onAnswerChange(next);
|
|
19
19
|
};
|
|
20
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), items.map((item, itemIndex) => (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
20
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), items.map((item, itemIndex) => (_jsx(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, children: _jsx(WordOrderingClient, { questionData: {
|
|
21
21
|
words: item.words,
|
|
22
22
|
answer: item.answer,
|
|
23
23
|
isExample: item.isExample,
|
|
@@ -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,8 @@ 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
|
-
|
|
634
|
+
const showViewerHeader = Boolean(cardTitle) && !isReviewMode;
|
|
635
|
+
return (_jsxs(Card, { children: [showViewerHeader ? (_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center justify-between", children: [_jsx("span", { className: "min-w-0 pr-2 text-base font-semibold leading-snug", children: cardTitle }), _jsx("span", { className: "shrink-0 text-sm font-normal text-gray-500", children: translate('question.viewer.points', { count: question.points }) })] }) })) : null, _jsx(CardContent, { className: showViewerHeader ? 'space-y-4' : 'space-y-4 pt-4', children: renderQuestion() })] }));
|
|
618
636
|
}
|
|
619
637
|
/** Drop lightweight markdown markers for plain header labels (not full token parse). */
|
|
620
638
|
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;
|