@tinyweb_dev/oe-exam-sdk 1.0.9 → 1.0.10
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/QuestionRenderer.js +1 -5
- package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js +24 -10
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +12 -2
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +2 -1
- package/dist/components/exams/take/utils/answer-transformers.js +16 -1
- package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +4 -6
- package/dist/components/exams/take/utils/question-transformers.d.ts +5 -0
- package/dist/components/exams/take/utils/question-transformers.js +6 -0
- package/dist/components/questions/_shared/components/compact/KidChoiceRow.d.ts +12 -0
- package/dist/components/questions/_shared/components/compact/KidChoiceRow.js +33 -0
- package/dist/components/questions/_shared/components/compact/KidInstruction.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidInstruction.js +9 -0
- package/dist/components/questions/_shared/components/compact/KidPrompt.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidPrompt.js +9 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.d.ts +12 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +7 -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/types/crossword-puzzle.type.d.ts +5 -1
- package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +17 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +18 -19
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +8 -7
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +5 -4
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +17 -11
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +15 -42
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +15 -36
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +9 -30
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +8 -7
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +50 -32
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +24 -44
- package/dist/components/questions/types/cross-out-word-group/index.d.ts +1 -0
- package/dist/components/questions/types/cross-out-word-group/index.js +1 -0
- package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +9 -9
- package/dist/components/questions/types/cross-out-word-group/tokenize.d.ts +2 -0
- package/dist/components/questions/types/cross-out-word-group/tokenize.js +11 -0
- package/dist/components/questions/types/cross-out-word-group/transform.js +2 -4
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +2 -1
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +8 -6
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +68 -11
- package/dist/components/questions/types/crossword-puzzle/transform.js +59 -30
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +26 -31
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.d.ts +6 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +17 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +32 -28
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +109 -26
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +21 -11
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +1 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +4 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +32 -31
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +71 -28
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +6 -2
- package/dist/components/questions/types/find-words-in-matrix/transform.js +5 -0
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +4 -3
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +11 -30
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +7 -6
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +2 -1
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +14 -13
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +11 -10
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +4 -3
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +13 -11
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +17 -14
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +16 -12
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +14 -14
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +13 -13
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +7 -6
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +8 -26
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +2 -1
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +6 -5
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +2 -1
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +5 -4
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +54 -11
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +11 -7
- package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +8 -2
- package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +1 -0
- package/dist/components/questions/types/spontaneous-qa-group/flatten.js +4 -2
- package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +5 -3
- package/dist/components/questions/types/spontaneous-qa-group/register.js +5 -2
- package/dist/components/questions/types/spontaneous-qa-group/transform.js +6 -2
- package/dist/components/questions/types/true-false/TrueFalseClient.js +5 -54
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +21 -23
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +8 -17
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +76 -81
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +17 -16
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +31 -36
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +15 -14
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +9 -8
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +16 -13
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +1 -4
- package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/FillInBlankGroupViewer.js +13 -7
- package/dist/components/questions/viewer/QuestionViewer.js +2 -2
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +7 -1
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +14 -2
- package/dist/components/results/renderers/review-question-body-movers.js +1 -1
- package/dist/components/results/review-data.js +8 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +31 -5
- package/dist/shared/types/questions/crossword-puzzle.d.ts +1 -0
- package/dist/shared/types/questions/find-words-in-matrix.d.ts +1 -0
- package/package.json +1 -1
package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js
CHANGED
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Mic, Volume2, CheckCircle2, Play, Pause, MessageCircle, Users } from 'lucide-react';
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
5
|
import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
|
|
6
|
+
import { KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
7
|
// ─── Sub-component: Audio Player ─────────────────────────────────────────────
|
|
7
8
|
function ConversationAudioPlayer({ audioUrl, label }) {
|
|
8
9
|
const { previewUrl } = usePresignedFileUrl(audioUrl);
|
|
@@ -33,9 +34,9 @@ export function SpeakingConversationPreviewClient({ questionData, isReviewMode =
|
|
|
33
34
|
: questionData.images.length === 4
|
|
34
35
|
? 'grid-cols-4'
|
|
35
36
|
: 'grid-cols-3';
|
|
36
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "H\u1ED9i tho\u1EA1i Speaking (KFS)", icon: _jsx(MessageCircle, { className: "h-3.5 w-3.5" }), headerExtra: _jsxs("div", { className: `inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold ${isPartnerQuestion
|
|
38
|
+
? 'bg-orange-100 text-orange-700 border border-orange-200'
|
|
39
|
+
: 'bg-green-100 text-green-700 border border-green-200'}`, children: [_jsx(Mic, { className: "h-3 w-3" }), isPartnerQuestion ? 'Bạn thi (ví dụ)' : 'Học sinh trả lời'] }), children: [(questionData.partnerName || questionData.partnerAvatarUrl) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-orange-50 border border-orange-100 px-4 py-2.5", children: [_jsx("span", { className: "text-xs font-medium text-orange-600", children: "Nh\u00E2n v\u1EADt b\u1EA1n thi:" }), _jsx(PartnerAvatar, { avatarUrl: questionData.partnerAvatarUrl, name: questionData.partnerName })] })), questionData.questionText && (_jsx(KidPrompt, { children: questionData.questionText })), _jsxs("div", { className: "space-y-3", children: [questionData.interlocutorAudioUrl && (_jsx(ConversationAudioPlayer, { audioUrl: questionData.interlocutorAudioUrl, label: "Audio ng\u01B0\u1EDDi h\u1ECFi (Interlocutor)" })), questionData.partnerAudioUrl && (_jsx(ConversationAudioPlayer, { audioUrl: questionData.partnerAudioUrl, label: "Audio b\u1EA1n thi (Partner)" }))] }), hasImages && (_jsxs("div", { children: [_jsx("p", { className: "text-xs font-medium text-gray-500 mb-2", children: "H\u00ECnh \u1EA3nh th\u1EA3o lu\u1EADn:" }), _jsx("div", { className: `grid ${gridCols} gap-3`, style: { maxWidth: '500px' }, children: questionData.images.map((img) => (_jsx(PreviewImageItem, { url: img.url, label: img.label }, img.id))) })] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: isPartnerQuestion
|
|
40
|
+
? 'Câu hỏi ví dụ — Bạn thi sẽ trả lời, học sinh lắng nghe'
|
|
41
|
+
: 'Học sinh sẽ nhấn nút ghi âm và nói câu trả lời tại đây' })] }), isReviewMode && questionData.expectedAnswers && questionData.expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n mong mu\u1ED1n" })] }), _jsx("div", { className: "space-y-2", children: questionData.expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] }))] })] }));
|
|
41
42
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Mic, CheckCircle2, Info, CreditCard } from 'lucide-react';
|
|
4
4
|
import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
|
|
5
|
+
import { KidQuestionCard } from '../../_shared/components/compact';
|
|
5
6
|
function asRecord(value) {
|
|
6
7
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
7
8
|
? value
|
|
@@ -23,5 +24,5 @@ export function mapQuestionToSpeakingCueCardData(question) {
|
|
|
23
24
|
}
|
|
24
25
|
export function SpeakingCueCardClient({ questionData, isReviewMode = false, }) {
|
|
25
26
|
const answers = questionData.expectedAnswers || [];
|
|
26
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(
|
|
27
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "Th\u1EBB g\u1EE3i \u00FD (Cue Card)", icon: _jsx(CreditCard, { className: "h-3.5 w-3.5" }), children: [_jsx("div", { className: "rounded-xl border border-purple-100 bg-purple-50/40 px-5 py-4", children: _jsx(RichTextHtml, { html: questionData.cueCardHtml, className: "text-sm text-slate-800", fallback: _jsx("p", { className: "text-sm text-gray-400", children: "Ch\u01B0a c\u00F3 n\u1ED9i dung cue card" }) }) }), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-purple-50 ring-2 ring-purple-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-purple-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh \u0111\u1ECDc th\u1EBB g\u1EE3i \u00FD, chu\u1EA9n b\u1ECB, r\u1ED3i tr\u1EA3 l\u1EDDi b\u1EB1ng gi\u1ECDng n\u00F3i" })] }), isReviewMode && answers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "\u0110\u00E1p \u00E1n m\u1EABu" })] }), _jsx("div", { className: "space-y-2", children: answers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, `${answer}-${index}`))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-amber-700 leading-relaxed whitespace-pre-wrap", children: questionData.explanation })] }))] })] }));
|
|
27
28
|
}
|
package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Mic, CheckCircle2, Info, ImageIcon
|
|
3
|
+
import { Mic, CheckCircle2, Info, ImageIcon } from 'lucide-react';
|
|
4
4
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
5
|
+
import { KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
5
6
|
export function SpeakingDescribeImageClient({ questionData, isReviewMode = false, }) {
|
|
6
7
|
const { previewUrl: imageUrl } = usePresignedFileUrl(questionData.imageUrl || '');
|
|
7
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "M\u00F4 t\u1EA3 h\u00ECnh \u1EA3nh (Speaking)", icon: _jsx(Mic, { className: "h-3.5 w-3.5" }), children: [questionData.question && (_jsx(KidPrompt, { children: questionData.question })), imageUrl ? (_jsx("div", { className: "w-full max-w-3xl mx-auto rounded-xl overflow-hidden shadow-sm border border-gray-200 bg-gray-50 flex justify-center", children: _jsx("img", { src: imageUrl, alt: "Scene", className: "w-[80%] max-h-[400px] object-contain flex-shrink-0" }) })) : (_jsxs("div", { className: "flex flex-col items-center justify-center py-10 rounded-xl border border-dashed border-gray-300 bg-gray-50", children: [_jsx(ImageIcon, { className: "h-8 w-8 text-gray-400 mb-2" }), _jsx("span", { className: "text-sm text-gray-500", children: "Ch\u01B0a t\u1EA3i h\u00ECnh \u1EA3nh m\u00F4 t\u1EA3" })] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: isReviewMode && questionData.audioUrl
|
|
9
|
+
? "Audio ghi âm của học sinh (đã lưu)"
|
|
10
|
+
: "Học sinh sẽ nhìn hình ảnh và nhấn nút micro để mô tả" })] }), isReviewMode && questionData.expectedAnswers && questionData.expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4 mt-6", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n t\u1EF1 \u0111\u1ED9ng (Tham chi\u1EBFu AI)" })] }), _jsx("div", { className: "space-y-2", children: questionData.expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-4 mt-2", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-800", children: "Gi\u1EA3i th\u00EDch gi\u00E1o vi\u00EAn" })] }), _jsx("p", { className: "text-sm text-amber-700 leading-relaxed whitespace-pre-wrap", children: questionData.explanation })] }))] })] }));
|
|
10
11
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { SpontaneousQaGroupClientProps } from '../../_shared/types/spontaneous-qa-group.type';
|
|
2
|
-
export declare function SpontaneousQaGroupClient({ questionData, isReviewMode, }: SpontaneousQaGroupClientProps): import("react").JSX.Element;
|
|
2
|
+
export declare function SpontaneousQaGroupClient({ questionData, isReviewMode, userAnswer, onItemRecorded, uploadingIndex, gradeDetails, disabled, }: SpontaneousQaGroupClientProps): import("react").JSX.Element;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { Lightbulb, Mic, Square, Volume2 } from 'lucide-react';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
|
+
import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
7
|
function QuestionAudio({ url }) {
|
|
7
8
|
const { previewUrl, isLoading } = usePresignedFileUrl(url);
|
|
8
9
|
const src = previewUrl ||
|
|
@@ -12,7 +13,28 @@ function QuestionAudio({ url }) {
|
|
|
12
13
|
}
|
|
13
14
|
return (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-blue-100 bg-blue-50/60 px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 shrink-0 text-blue-500" }), isLoading ? (_jsx("div", { className: "h-8 flex-1 animate-pulse rounded-md bg-blue-100" })) : (_jsx("audio", { controls: true, src: src, className: "h-8 w-full", preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." }))] }));
|
|
14
15
|
}
|
|
15
|
-
function
|
|
16
|
+
function readGradeUser(detail) {
|
|
17
|
+
if (!detail?.userAnswer || typeof detail.userAnswer !== 'object' || Array.isArray(detail.userAnswer)) {
|
|
18
|
+
return { feedback: detail?.feedback };
|
|
19
|
+
}
|
|
20
|
+
const raw = detail.userAnswer;
|
|
21
|
+
return {
|
|
22
|
+
transcript: typeof raw.transcript === 'string' ? raw.transcript : undefined,
|
|
23
|
+
contentScore: typeof raw.contentScore === 'number' ? raw.contentScore : undefined,
|
|
24
|
+
pronunciationScore: typeof raw.pronunciationScore === 'number' ? raw.pronunciationScore : undefined,
|
|
25
|
+
fluencyScore: typeof raw.fluencyScore === 'number' ? raw.fluencyScore : undefined,
|
|
26
|
+
feedback: typeof raw.feedback === 'string' ? raw.feedback : detail.feedback,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function findGradeDetail(details, flattenIndex) {
|
|
30
|
+
if (!details?.length) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return details.find((item) => (item.itemId === String(flattenIndex) ||
|
|
34
|
+
item.itemId === `item-${flattenIndex}` ||
|
|
35
|
+
item.itemId === `item-${flattenIndex + 1}`));
|
|
36
|
+
}
|
|
37
|
+
function SpeakingReplyMic({ flattenIndex, audioKey, disabled, uploading, onRecorded, }) {
|
|
16
38
|
const [status, setStatus] = useState('idle');
|
|
17
39
|
const [error, setError] = useState('');
|
|
18
40
|
const [blobUrl, setBlobUrl] = useState('');
|
|
@@ -33,6 +55,9 @@ function SpeakingReplyMic() {
|
|
|
33
55
|
streamRef.current = null;
|
|
34
56
|
};
|
|
35
57
|
const startRecording = async () => {
|
|
58
|
+
if (disabled || uploading || !onRecorded) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
36
61
|
setError('');
|
|
37
62
|
try {
|
|
38
63
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
@@ -55,6 +80,7 @@ function SpeakingReplyMic() {
|
|
|
55
80
|
});
|
|
56
81
|
setStatus('recorded');
|
|
57
82
|
stopStream();
|
|
83
|
+
onRecorded(flattenIndex, blob);
|
|
58
84
|
};
|
|
59
85
|
recorder.start();
|
|
60
86
|
setStatus('recording');
|
|
@@ -69,17 +95,34 @@ function SpeakingReplyMic() {
|
|
|
69
95
|
recorderRef.current.stop();
|
|
70
96
|
}
|
|
71
97
|
};
|
|
72
|
-
|
|
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'
|
|
73
100
|
? 'animate-pulse bg-red-500 hover:bg-red-600'
|
|
74
|
-
: '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("p", { className: "text-center text-xs text-indigo-700", children:
|
|
75
|
-
? 'Đang
|
|
76
|
-
: status === '
|
|
77
|
-
? '
|
|
78
|
-
:
|
|
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] }));
|
|
79
108
|
}
|
|
80
|
-
export function SpontaneousQaGroupClient({ questionData, isReviewMode = false, }) {
|
|
109
|
+
export function SpontaneousQaGroupClient({ questionData, isReviewMode = false, userAnswer, onItemRecorded, uploadingIndex = null, gradeDetails, disabled = false, }) {
|
|
81
110
|
const parts = questionData.parts || [];
|
|
82
|
-
|
|
111
|
+
const audioKeys = Array.isArray(userAnswer?.audioKeys) ? userAnswer.audioKeys : [];
|
|
112
|
+
let flattenIndex = 0;
|
|
113
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), parts.map((part, partIndex) => (_jsxs(KidQuestionCard, { icon: _jsx(Mic, { className: "h-3.5 w-3.5" }), title: part.partTitle || `PART ${part.partNumber || partIndex + 1}`, children: [part.topic ? _jsx(KidPrompt, { children: part.topic }) : null, part.questions.map((question, questionIndex) => {
|
|
114
|
+
const currentIndex = flattenIndex;
|
|
115
|
+
flattenIndex += 1;
|
|
116
|
+
const audioKey = audioKeys[currentIndex];
|
|
117
|
+
const detail = findGradeDetail(gradeDetails, currentIndex);
|
|
118
|
+
const grade = readGradeUser(detail);
|
|
119
|
+
const showMic = !isReviewMode && !question.isExample;
|
|
120
|
+
return (_jsxs(KidQuestionCard, { title: `Câu ${question.questionNumber || questionIndex + 1}`, 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 })), detail && !question.isExample && (_jsxs("div", { className: "space-y-1 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-700", children: [grade.transcript && (_jsxs("p", { children: [_jsx("span", { className: "font-semibold", children: "Transcript:" }), " ", grade.transcript] })), (grade.contentScore != null || grade.pronunciationScore != null || grade.fluencyScore != null) && (_jsx("p", { children: [
|
|
121
|
+
grade.contentScore != null ? `Content ${Math.round(grade.contentScore)}` : null,
|
|
122
|
+
grade.pronunciationScore != null ? `Pronunciation ${Math.round(grade.pronunciationScore)}` : null,
|
|
123
|
+
grade.fluencyScore != null ? `Fluency ${Math.round(grade.fluencyScore)}` : null,
|
|
124
|
+
].filter(Boolean).join(' · ') })), grade.feedback && _jsx("p", { children: grade.feedback })] })), isReviewMode && question.expectedAnswers.filter((item) => item.trim() !== '').length > 0 && (_jsxs("div", { className: "rounded-lg border border-amber-100 bg-amber-50 px-3 py-2", children: [_jsxs("div", { className: "mb-1 flex items-center gap-1 text-xs font-semibold text-amber-700", children: [_jsx(Lightbulb, { className: "h-3 w-3" }), "\u0110\u00E1p \u00E1n m\u1EABu"] }), _jsx("ul", { className: "list-disc space-y-1 pl-4 text-xs text-amber-800", children: question.expectedAnswers
|
|
83
125
|
.filter((item) => item.trim() !== '')
|
|
84
|
-
.map((item) => (_jsx("li", { children: item }, item))) })] }))] }, `${part.partNumber}-${question.questionNumber}-${questionIndex}`))
|
|
126
|
+
.map((item) => (_jsx("li", { children: item }, item))) })] }))] }, `${part.partNumber}-${question.questionNumber}-${questionIndex}`));
|
|
127
|
+
})] }, `part-${part.partNumber}-${partIndex}`)))] }));
|
|
85
128
|
}
|
|
@@ -6,11 +6,12 @@ import { Label } from '../../../../components/ui/label';
|
|
|
6
6
|
import { Input } from '../../../../components/ui/input';
|
|
7
7
|
import { Textarea } from '../../../../components/ui/textarea';
|
|
8
8
|
import { PointsInput } from '../../../../components/ui/points-input';
|
|
9
|
+
import { Switch } from '../../../../components/ui/switch';
|
|
9
10
|
import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
|
|
10
11
|
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
11
12
|
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
12
13
|
import { toast } from 'sonner';
|
|
13
|
-
import { Layers, Mic, Plus, Trash2, Type, Volume2 } from 'lucide-react';
|
|
14
|
+
import { Layers, Mic, Plus, Star, Trash2, Type, Volume2 } from 'lucide-react';
|
|
14
15
|
import { SpontaneousQaGroupClient } from './SpontaneousQaGroupClient';
|
|
15
16
|
import { createEmptyPart, createEmptyQuestion, normalizeParts, sumPartPoints, } from './creator-helpers';
|
|
16
17
|
export function SpontaneousQaGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
|
|
@@ -172,10 +173,13 @@ export function SpontaneousQaGroupCreator({ initialData, onChange, externalError
|
|
|
172
173
|
onClick: () => setIsClientMode(true),
|
|
173
174
|
} }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "group-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "group-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: "In pairs, ask and answer the questions.", className: "h-10 border-gray-200 bg-white" })] }), _jsx("p", { className: "text-xs text-gray-500", children: "T\u1ED5ng \u0111i\u1EC3m \u0111\u01B0\u1EE3c c\u1ED9ng t\u1EEB \u0111i\u1EC3m t\u1EEBng c\u00E2u trong t\u1EA5t c\u1EA3 part." })] }), parts.map((part, partIndex) => (_jsxs(CompactFieldCard, { header: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("p", { className: "text-sm font-semibold text-gray-800", children: ["Part ", partIndex + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removePart(partIndex), disabled: parts.length <= 1, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), children: [_jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Ti\u00EAu \u0111\u1EC1 part" }), _jsx(Input, { value: part.partTitle, onChange: (event) => updatePart(partIndex, { partTitle: event.target.value }), placeholder: "PART 1" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Ch\u1EE7 \u0111\u1EC1" }), _jsx(Input, { value: part.topic, onChange: (event) => updatePart(partIndex, { topic: event.target.value }), placeholder: "Talk about school." })] })] }), part.questions.map((question, questionIndex) => (_jsxs("div", { className: "space-y-2.5 rounded-md border border-slate-200 px-3 py-2.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("p", { className: "text-sm font-semibold text-slate-700", children: ["C\u00E2u ", questionIndex + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeQuestion(partIndex, questionIndex), disabled: part.questions.length <= 1, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "flex w-fit rounded-lg border border-slate-200 bg-slate-100 p-1", children: [_jsxs("button", { type: "button", onClick: () => updateQuestion(partIndex, questionIndex, { inputType: 'AUDIO' }), className: `flex items-center gap-2 rounded-md px-3 py-1.5 text-xs font-medium ${question.inputType === 'AUDIO' ? 'bg-white text-blue-700 shadow-sm' : 'text-slate-500'}`, children: [_jsx(Mic, { className: "h-3.5 w-3.5" }), " Audio"] }), _jsxs("button", { type: "button", onClick: () => updateQuestion(partIndex, questionIndex, { inputType: 'TEXT' }), className: `flex items-center gap-2 rounded-md px-3 py-1.5 text-xs font-medium ${question.inputType === 'TEXT' ? 'bg-white text-emerald-700 shadow-sm' : 'text-slate-500'}`, children: [_jsx(Type, { className: "h-3.5 w-3.5" }), " Text (TTS)"] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "C\u00E2u h\u1ECFi" }), _jsx(Textarea, { value: question.questionText, onChange: (event) => updateQuestion(partIndex, questionIndex, {
|
|
174
175
|
questionText: event.target.value,
|
|
175
|
-
}), rows: 2, placeholder: "Do you like your timetable at school? Why?/Why not?" })] }), question.inputType === 'AUDIO' ? (_jsx(FileUpload, { id: `audio-${partIndex}-${questionIndex}`, label: "File audio c\u00E2u h\u1ECFi", accept: "audio/*", value: question.audioUrl, onChange: (url) => updateQuestion(partIndex, questionIndex, { audioUrl: url }), maxSize: 20, placeholder: "T\u1EA3i l\u00EAn file audio (MP3, WAV...)", autoUpload: true, prefix: "speaking/spontaneous-qa-group" })) : (_jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: !question.questionText.trim(), onClick: handleGenerateAudio, children: [_jsx(Volume2, { className: "mr-2 h-3.5 w-3.5" }), "T\u1EA1o Audio"] })), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "\u0110\u00E1p \u00E1n m\u1EABu" }), question.expectedAnswers.map((answer, answerIndex) => (_jsx(Input, { value: answer, onChange: (event) => updateExpectedAnswer(partIndex, questionIndex, answerIndex, event.target.value), placeholder: "\u0110\u00E1p \u00E1n m\u1EABu cho AI ch\u1EA5m" }, `ans-${partIndex}-${questionIndex}-${answerIndex}`))), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => addExpectedAnswer(partIndex, questionIndex), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm \u0111\u00E1p \u00E1n m\u1EABu"] })] }), _jsx(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
}), rows: 2, placeholder: "Do you like your timetable at school? Why?/Why not?" })] }), question.inputType === 'AUDIO' ? (_jsx(FileUpload, { id: `audio-${partIndex}-${questionIndex}`, label: "File audio c\u00E2u h\u1ECFi", accept: "audio/*", value: question.audioUrl, onChange: (url) => updateQuestion(partIndex, questionIndex, { audioUrl: url }), maxSize: 20, placeholder: "T\u1EA3i l\u00EAn file audio (MP3, WAV...)", autoUpload: true, prefix: "speaking/spontaneous-qa-group" })) : (_jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: !question.questionText.trim(), onClick: handleGenerateAudio, children: [_jsx(Volume2, { className: "mr-2 h-3.5 w-3.5" }), "T\u1EA1o Audio"] })), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "\u0110\u00E1p \u00E1n m\u1EABu" }), question.expectedAnswers.map((answer, answerIndex) => (_jsx(Input, { value: answer, onChange: (event) => updateExpectedAnswer(partIndex, questionIndex, answerIndex, event.target.value), placeholder: "\u0110\u00E1p \u00E1n m\u1EABu cho AI ch\u1EA5m" }, `ans-${partIndex}-${questionIndex}-${answerIndex}`))), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => addExpectedAnswer(partIndex, questionIndex), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm \u0111\u00E1p \u00E1n m\u1EABu"] })] }), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: question.isExample === true, onCheckedChange: (checked) => updateQuestion(partIndex, questionIndex, {
|
|
177
|
+
isExample: checked,
|
|
178
|
+
points: checked ? 0 : (question.points || 1),
|
|
179
|
+
}) }), _jsxs(Label, { className: "flex cursor-pointer items-center gap-1 text-xs", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "C\u00E2u v\u00ED d\u1EE5"] })] }), !question.isExample && (_jsx(PointsInput, { value: question.points, allowZero: true, onChange: (event) => {
|
|
180
|
+
const parsed = Number.parseInt(event.target.value.replace(/[^0-9]/g, ''), 10);
|
|
181
|
+
updateQuestion(partIndex, questionIndex, {
|
|
182
|
+
points: Number.isNaN(parsed) ? 0 : Math.max(0, parsed),
|
|
183
|
+
});
|
|
184
|
+
} }))] })] }, `q-${partIndex}-${questionIndex}`))), _jsxs(Button, { type: "button", variant: "outline", onClick: () => addQuestion(partIndex), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm c\u00E2u h\u1ECFi"] })] }, `part-${partIndex}`))), _jsxs(Button, { type: "button", variant: "outline", onClick: addPart, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm part"] }), _jsx(CompactExplanationToggle, { value: explanation, onValueChange: setExplanation, defaultVisible: Boolean(initialData?.explanation) }), allErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: allErrors.map((error) => (_jsx("p", { children: error }, error))) }))] }));
|
|
181
185
|
}
|
|
@@ -6,6 +6,7 @@ export function createEmptyQuestion(questionNumber) {
|
|
|
6
6
|
expectedAnswers: [''],
|
|
7
7
|
points: 1,
|
|
8
8
|
questionNumber,
|
|
9
|
+
isExample: false,
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
12
|
export function createEmptyPart(partNumber) {
|
|
@@ -32,12 +33,17 @@ export function normalizeParts(rawParts) {
|
|
|
32
33
|
expectedAnswers: Array.isArray(question.expectedAnswers) && question.expectedAnswers.length > 0
|
|
33
34
|
? question.expectedAnswers
|
|
34
35
|
: [''],
|
|
35
|
-
|
|
36
|
+
isExample: question.isExample === true,
|
|
37
|
+
points: question.isExample === true
|
|
38
|
+
? 0
|
|
39
|
+
: typeof question.points === 'number' ? question.points : 1,
|
|
36
40
|
questionNumber: question.questionNumber || questionIndex + 1,
|
|
37
41
|
}))
|
|
38
42
|
: [createEmptyQuestion(1)],
|
|
39
43
|
}));
|
|
40
44
|
}
|
|
41
45
|
export function sumPartPoints(parts) {
|
|
42
|
-
return parts.reduce((total, part) => (total + part.questions.reduce((partTotal, question) => (partTotal + (
|
|
46
|
+
return parts.reduce((total, part) => (total + part.questions.reduce((partTotal, question) => (partTotal + (question.isExample
|
|
47
|
+
? 0
|
|
48
|
+
: typeof question.points === 'number' ? question.points : 1)), 0)), 0);
|
|
43
49
|
}
|
|
@@ -10,6 +10,7 @@ export interface SpontaneousQaGroupFlatChild {
|
|
|
10
10
|
inputType: 'TEXT' | 'AUDIO';
|
|
11
11
|
expectedAnswers: string[];
|
|
12
12
|
points: number;
|
|
13
|
+
isExample: boolean;
|
|
13
14
|
}
|
|
14
15
|
export declare function flattenSpontaneousQaGroupContent(content: unknown): SpontaneousQaGroupFlatChild[];
|
|
15
16
|
/** Speaking Bank / translate lists — no teacher video or one-at-a-time mic. */
|
|
@@ -42,7 +42,8 @@ export function flattenSpontaneousQaGroupContent(content) {
|
|
|
42
42
|
audioUrl: asString(childContent.audioUrl),
|
|
43
43
|
inputType: asString(childContent.inputType) === 'AUDIO' ? 'AUDIO' : 'TEXT',
|
|
44
44
|
expectedAnswers: normalizeExpectedAnswers(asRecord(child.correctAnswer).expectedAnswers),
|
|
45
|
-
|
|
45
|
+
isExample: child.isExample === true,
|
|
46
|
+
points: child.isExample === true ? 0 : asNumber(child.points, 1),
|
|
46
47
|
});
|
|
47
48
|
});
|
|
48
49
|
});
|
|
@@ -62,7 +63,8 @@ export function flattenSpontaneousQaGroupContent(content) {
|
|
|
62
63
|
audioUrl: asString(childContent.audioUrl),
|
|
63
64
|
inputType: asString(childContent.inputType) === 'AUDIO' ? 'AUDIO' : 'TEXT',
|
|
64
65
|
expectedAnswers: normalizeExpectedAnswers(asRecord(child.correctAnswer).expectedAnswers),
|
|
65
|
-
|
|
66
|
+
isExample: child.isExample === true,
|
|
67
|
+
points: child.isExample === true ? 0 : asNumber(child.points, 1),
|
|
66
68
|
});
|
|
67
69
|
});
|
|
68
70
|
return flattened;
|
package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js
CHANGED
|
@@ -23,6 +23,7 @@ function mapQuestion(item, index) {
|
|
|
23
23
|
const record = asRecord(item);
|
|
24
24
|
const content = asRecord(record.content);
|
|
25
25
|
const correctAnswer = asRecord(record.correctAnswer);
|
|
26
|
+
const isExample = record.isExample === true;
|
|
26
27
|
return {
|
|
27
28
|
questionText: asString(content.questionText) ||
|
|
28
29
|
asString(content.question) ||
|
|
@@ -30,7 +31,8 @@ function mapQuestion(item, index) {
|
|
|
30
31
|
inputType: asString(content.inputType) === 'AUDIO' ? 'AUDIO' : 'TEXT',
|
|
31
32
|
audioUrl: asString(content.audioUrl),
|
|
32
33
|
expectedAnswers: normalizeExpectedAnswers(correctAnswer.expectedAnswers),
|
|
33
|
-
|
|
34
|
+
isExample,
|
|
35
|
+
points: isExample ? 0 : asNumber(record.points, 1),
|
|
34
36
|
questionNumber: asNumber(record.questionNumber, index + 1),
|
|
35
37
|
};
|
|
36
38
|
}
|
|
@@ -66,9 +68,9 @@ export function mapQuestionToSpontaneousQaGroupData(question) {
|
|
|
66
68
|
return {
|
|
67
69
|
instruction: asString(asRecord(list.meta).instruction) ||
|
|
68
70
|
asString(asRecord(content.meta).instruction) ||
|
|
69
|
-
'
|
|
71
|
+
'In pairs, ask and answer the questions.',
|
|
70
72
|
parts,
|
|
71
73
|
explanation: asString(question.explanation),
|
|
72
|
-
points: parts.reduce((total, part) => (total + part.questions.reduce((partTotal, item) => partTotal + (item.points || 0), 0)), 0),
|
|
74
|
+
points: parts.reduce((total, part) => (total + part.questions.reduce((partTotal, item) => (partTotal + (item.isExample ? 0 : (item.points || 0))), 0)), 0),
|
|
73
75
|
};
|
|
74
76
|
}
|
|
@@ -28,6 +28,7 @@ function createEmptyQuestion(questionNumber) {
|
|
|
28
28
|
expectedAnswers: [''],
|
|
29
29
|
points: 1,
|
|
30
30
|
questionNumber,
|
|
31
|
+
isExample: false,
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
function createEmptyPart(partNumber) {
|
|
@@ -43,12 +44,14 @@ function mapQuestion(item, index, fallbackAnswers) {
|
|
|
43
44
|
const content = asRecord(record.content);
|
|
44
45
|
const correctAnswer = asRecord(record.correctAnswer);
|
|
45
46
|
const expected = normalizeExpectedAnswers(correctAnswer.expectedAnswers ?? fallbackAnswers);
|
|
47
|
+
const isExample = record.isExample === true;
|
|
46
48
|
return {
|
|
47
49
|
questionText: asString(content.questionText) || asString(record.questionText),
|
|
48
50
|
inputType: asString(content.inputType) === 'AUDIO' ? 'AUDIO' : 'TEXT',
|
|
49
51
|
audioUrl: asString(content.audioUrl),
|
|
50
52
|
expectedAnswers: expected,
|
|
51
|
-
|
|
53
|
+
isExample,
|
|
54
|
+
points: isExample ? 0 : asNumber(record.points, 1),
|
|
52
55
|
questionNumber: asNumber(record.questionNumber, index + 1),
|
|
53
56
|
};
|
|
54
57
|
}
|
|
@@ -65,7 +68,7 @@ function mapPart(item, index) {
|
|
|
65
68
|
};
|
|
66
69
|
}
|
|
67
70
|
function sumPoints(parts) {
|
|
68
|
-
return parts.reduce((total, part) => (total + part.questions.reduce((partTotal, question) => partTotal + (question.points || 0), 0)), 0);
|
|
71
|
+
return parts.reduce((total, part) => (total + part.questions.reduce((partTotal, question) => (partTotal + (question.isExample ? 0 : (question.points || 0))), 0)), 0);
|
|
69
72
|
}
|
|
70
73
|
export const spontaneousQaGroupRegistration = {
|
|
71
74
|
component: SpontaneousQaGroupCreator,
|
|
@@ -9,14 +9,18 @@ function normalizeExpectedAnswers(value) {
|
|
|
9
9
|
.filter((item) => item !== '');
|
|
10
10
|
}
|
|
11
11
|
function sumPoints(parts) {
|
|
12
|
-
return parts.reduce((total, part) => (total + part.questions.reduce((partTotal, question) => (partTotal + (
|
|
12
|
+
return parts.reduce((total, part) => (total + part.questions.reduce((partTotal, question) => (partTotal + (question.isExample
|
|
13
|
+
? 0
|
|
14
|
+
: typeof question.points === 'number' ? question.points : 1)), 0)), 0);
|
|
13
15
|
}
|
|
14
16
|
function toApiQuestion(question, index) {
|
|
15
17
|
const inputType = question.inputType === 'AUDIO' ? 'AUDIO' : 'TEXT';
|
|
18
|
+
const isExample = question.isExample === true;
|
|
16
19
|
return {
|
|
17
20
|
questionType: CHILD_QUESTION_TYPE,
|
|
18
21
|
questionNumber: question.questionNumber || index + 1,
|
|
19
|
-
points: typeof question.points === 'number' ? question.points : 1,
|
|
22
|
+
points: isExample ? 0 : typeof question.points === 'number' ? question.points : 1,
|
|
23
|
+
...(isExample ? { isExample: true } : {}),
|
|
20
24
|
content: {
|
|
21
25
|
inputType,
|
|
22
26
|
questionText: question.questionText || '',
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { Lightbulb } from 'lucide-react';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
|
-
import {
|
|
6
|
+
import { KidChoiceRow, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
7
7
|
export function TrueFalseClient({ questionData, onSubmit, isReviewMode = false, userAnswer, }) {
|
|
8
8
|
const [selectedAnswer, setSelectedAnswer] = useState(userAnswer !== undefined ? userAnswer : undefined);
|
|
9
|
-
// Presigned URL for image
|
|
10
9
|
const { previewUrl: imagePreviewUrl, isLoading } = usePresignedFileUrl(questionData.imageUrl || '');
|
|
11
|
-
// Sync userAnswer
|
|
12
10
|
useEffect(() => {
|
|
13
11
|
if (userAnswer !== undefined) {
|
|
14
12
|
setSelectedAnswer(userAnswer);
|
|
15
13
|
}
|
|
16
14
|
}, [userAnswer]);
|
|
17
|
-
// Handle answer change
|
|
18
15
|
const handleAnswerChange = (value) => {
|
|
19
16
|
if (isReviewMode)
|
|
20
17
|
return;
|
|
@@ -23,53 +20,7 @@ export function TrueFalseClient({ questionData, onSubmit, isReviewMode = false,
|
|
|
23
20
|
};
|
|
24
21
|
const hasAnswered = selectedAnswer !== undefined;
|
|
25
22
|
const isAnswerCorrect = isReviewMode && selectedAnswer === questionData.answer;
|
|
26
|
-
return (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/* eslint-disable-next-line @next/next/no-img-element */
|
|
30
|
-
_jsx("img", { src: imagePreviewUrl || questionData.imageUrl, alt: "Question illustration", className: "max-h-56 w-auto rounded-md object-contain" })) })] })), _jsxs("div", { className: "space-y-2.5", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-gray-500", children: "Ch\u1ECDn \u0111\u00E1p \u00E1n" }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("button", { type: "button", onClick: () => handleAnswerChange(true), disabled: isReviewMode, className: cn('group flex items-center justify-center gap-3 rounded-xl border-2 p-4 transition-all', isReviewMode
|
|
31
|
-
? questionData.answer === true
|
|
32
|
-
? 'border-green-400 bg-green-50'
|
|
33
|
-
: selectedAnswer === true
|
|
34
|
-
? 'border-red-400 bg-red-50'
|
|
35
|
-
: 'border-gray-200 bg-gray-50'
|
|
36
|
-
: selectedAnswer === true
|
|
37
|
-
? 'border-emerald-500 bg-emerald-50 shadow-sm ring-2 ring-emerald-100'
|
|
38
|
-
: 'border-gray-200 bg-white hover:border-emerald-300 hover:bg-emerald-50/50 hover:shadow-sm'), children: [_jsx("div", { className: cn('flex h-9 w-9 items-center justify-center rounded-full text-sm font-bold transition-all', isReviewMode
|
|
39
|
-
? questionData.answer === true
|
|
40
|
-
? 'bg-green-500 text-white'
|
|
41
|
-
: selectedAnswer === true
|
|
42
|
-
? 'bg-red-500 text-white'
|
|
43
|
-
: 'bg-gray-200 text-gray-600'
|
|
44
|
-
: selectedAnswer === true
|
|
45
|
-
? 'bg-emerald-500 text-white shadow-sm'
|
|
46
|
-
: 'bg-gray-100 text-gray-500 group-hover:bg-emerald-100 group-hover:text-emerald-600'), children: _jsx(Check, { className: "h-4 w-4" }) }), _jsx("span", { className: cn('text-base font-semibold', isReviewMode && questionData.answer === true
|
|
47
|
-
? 'text-green-800'
|
|
48
|
-
: isReviewMode && selectedAnswer === true
|
|
49
|
-
? 'text-red-800'
|
|
50
|
-
: selectedAnswer === true
|
|
51
|
-
? 'text-emerald-700'
|
|
52
|
-
: 'text-gray-600'), children: "\u0110\u00FAng (True)" }), isReviewMode && (questionData.answer === true || selectedAnswer === true) && (_jsx("div", { className: cn('flex h-6 w-6 items-center justify-center rounded-full', questionData.answer === true ? 'bg-green-500' : 'bg-red-500'), children: questionData.answer === true ? (_jsx(Check, { className: "h-4 w-4 text-white" })) : (_jsx(X, { className: "h-4 w-4 text-white" })) }))] }), _jsxs("button", { type: "button", onClick: () => handleAnswerChange(false), disabled: isReviewMode, className: cn('group flex items-center justify-center gap-3 rounded-xl border-2 p-4 transition-all', isReviewMode
|
|
53
|
-
? questionData.answer === false
|
|
54
|
-
? 'border-green-400 bg-green-50'
|
|
55
|
-
: selectedAnswer === false
|
|
56
|
-
? 'border-red-400 bg-red-50'
|
|
57
|
-
: 'border-gray-200 bg-gray-50'
|
|
58
|
-
: selectedAnswer === false
|
|
59
|
-
? 'border-red-500 bg-red-50 shadow-sm ring-2 ring-red-100'
|
|
60
|
-
: 'border-gray-200 bg-white hover:border-red-300 hover:bg-red-50/50 hover:shadow-sm'), children: [_jsx("div", { className: cn('flex h-9 w-9 items-center justify-center rounded-full text-sm font-bold transition-all', isReviewMode
|
|
61
|
-
? questionData.answer === false
|
|
62
|
-
? 'bg-green-500 text-white'
|
|
63
|
-
: selectedAnswer === false
|
|
64
|
-
? 'bg-red-500 text-white'
|
|
65
|
-
: 'bg-gray-200 text-gray-600'
|
|
66
|
-
: selectedAnswer === false
|
|
67
|
-
? 'bg-red-500 text-white shadow-sm'
|
|
68
|
-
: 'bg-gray-100 text-gray-500 group-hover:bg-red-100 group-hover:text-red-600'), children: _jsx(X, { className: "h-4 w-4" }) }), _jsx("span", { className: cn('text-base font-semibold', isReviewMode && questionData.answer === false
|
|
69
|
-
? 'text-green-800'
|
|
70
|
-
: isReviewMode && selectedAnswer === false
|
|
71
|
-
? 'text-red-800'
|
|
72
|
-
: selectedAnswer === false
|
|
73
|
-
? 'text-red-700'
|
|
74
|
-
: 'text-gray-600'), children: "Sai (False)" }), isReviewMode && (questionData.answer === false || selectedAnswer === false) && (_jsx("div", { className: cn('flex h-6 w-6 items-center justify-center rounded-full', questionData.answer === false ? 'bg-green-500' : 'bg-red-500'), children: questionData.answer === false ? (_jsx(Check, { className: "h-4 w-4 text-white" })) : (_jsx(X, { className: "h-4 w-4 text-white" })) }))] })] })] }), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-xs font-bold", children: "!" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "Vui l\u00F2ng ch\u1ECDn \u0111\u00E1p \u00E1n" })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-lg border border-blue-200 bg-blue-50 p-4", children: [_jsx("p", { className: "text-sm font-semibold text-blue-800", children: "Gi\u1EA3i th\u00EDch:" }), _jsx("p", { className: "mt-1 text-sm text-blue-700", children: questionData.explanation })] }))] })] }) }));
|
|
23
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsxs(KidQuestionCard, { title: "\u0110\u00FAng / Sai", isExample: questionData.isExample, review: isReviewMode && hasAnswered ? (isAnswerCorrect ? 'correct' : 'wrong') : null, children: [_jsx(KidPrompt, { children: questionData.question || 'Chưa nhập câu hỏi' }), (imagePreviewUrl || questionData.imageUrl) && (_jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2 w-2 rounded-full bg-red-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-green-400" }), _jsx("span", { className: "ml-2 text-xs font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4", children: isLoading ? (_jsx("div", { className: "flex h-48 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })) : (
|
|
24
|
+
/* eslint-disable-next-line @next/next/no-img-element */
|
|
25
|
+
_jsx("img", { src: imagePreviewUrl || questionData.imageUrl, alt: "H\u00ECnh \u1EA3nh c\u00E2u h\u1ECFi", className: "max-h-56 w-auto rounded-md object-contain" })) })] })), _jsxs("div", { className: "space-y-1.5", children: [_jsx(KidChoiceRow, { letter: "T", selected: selectedAnswer === true, isReviewMode: isReviewMode, isCorrect: isReviewMode && questionData.answer === true, onClick: () => handleAnswerChange(true), children: "\u0110\u00FAng" }), _jsx(KidChoiceRow, { letter: "F", selected: selectedAnswer === false, isReviewMode: isReviewMode, isCorrect: isReviewMode && questionData.answer === false, onClick: () => handleAnswerChange(false), children: "Sai" })] }), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5", children: [_jsx("div", { className: "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-[10px] font-bold", children: "!" }) }), _jsx("span", { className: "text-xs text-amber-800", children: "Vui l\u00F2ng ch\u1ECDn \u0111\u00E1p \u00E1n" })] }))] }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2", children: [_jsx(Lightbulb, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-600" }), _jsxs("p", { className: "text-xs text-gray-700", children: [_jsx("span", { className: "font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch: " }), questionData.explanation] })] }))] }));
|
|
75
26
|
}
|