@tinyweb_dev/oe-exam-sdk 0.2.8 → 0.2.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/ExamTakingPageContainer.js +6 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +7 -3
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -4
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.d.ts +13 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.js +30 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/utils/answer-transformers.js +8 -1
- package/dist/components/exams/take/utils/question-transformers.d.ts +16 -1
- package/dist/components/exams/take/utils/question-transformers.js +49 -4
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/questions/_shared/config/question-types.config.js +18 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/index.d.ts +1 -0
- package/dist/components/questions/_shared/types/index.js +1 -0
- package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.d.ts +56 -0
- package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.js +2 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +35 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +2 -0
- package/dist/components/questions/_shared/types/word-ordering-group.type.d.ts +34 -0
- package/dist/components/questions/_shared/types/word-ordering-group.type.js +2 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +3 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
- package/dist/components/questions/creator/question-type-registry.js +6 -0
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +6 -4
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +16 -4
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +2 -3
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +6 -4
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +16 -2
- package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +74 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.js +222 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/index.d.ts +5 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/index.js +5 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.d.ts +8 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.js +199 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/register.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/register.js +36 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/transform.d.ts +2 -0
- package/dist/components/questions/types/match-columns-to-make-sentences/transform.js +66 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +65 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +95 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +6 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +16 -0
- package/dist/components/questions/types/match-word-to-picture-group/index.d.ts +5 -0
- package/dist/components/questions/types/match-word-to-picture-group/index.js +5 -0
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.d.ts +4 -0
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +77 -0
- package/dist/components/questions/types/match-word-to-picture-group/register.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/register.js +36 -0
- package/dist/components/questions/types/match-word-to-picture-group/transform.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture-group/transform.js +36 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +25 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.js +106 -0
- package/dist/components/questions/types/word-ordering-group/index.d.ts +5 -0
- package/dist/components/questions/types/word-ordering-group/index.js +5 -0
- package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.d.ts +4 -0
- package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.js +74 -0
- package/dist/components/questions/types/word-ordering-group/register.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/register.js +36 -0
- package/dist/components/questions/types/word-ordering-group/transform.d.ts +2 -0
- package/dist/components/questions/types/word-ordering-group/transform.js +33 -0
- package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.d.ts +8 -0
- package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.js +49 -0
- package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +2 -2
- package/dist/components/questions/viewer/QuestionViewer.js +6 -0
- package/dist/components/questions/viewer/WordOrderingGroupViewer.d.ts +7 -0
- package/dist/components/questions/viewer/WordOrderingGroupViewer.js +29 -0
- package/dist/components/questions/viewer/index.d.ts +2 -0
- package/dist/components/questions/viewer/index.js +2 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +3 -2
- package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
- package/dist/components/results/renderers/review-question-body-movers.js +6 -2
- package/dist/components/results/review-data.js +13 -0
- package/dist/components/shared/RichTextEditor.d.ts +4 -2
- package/dist/components/shared/RichTextEditor.js +34 -11
- package/dist/components/shared/RichTextHtml.d.ts +8 -0
- package/dist/components/shared/RichTextHtml.js +9 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +6 -0
- package/dist/shared/lib/rich-text.d.ts +3 -0
- package/dist/shared/lib/rich-text.js +24 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +111 -2
- package/dist/shared/lib/utils/question-transform.js +6 -0
- package/dist/shared/lib/validation/choose-correct-answer.validation.js +4 -2
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
- package/dist/shared/types/questions/index.d.ts +3 -0
- package/dist/shared/types/questions/index.js +6 -0
- package/dist/shared/types/questions/match-columns-to-make-sentences.d.ts +50 -0
- package/dist/shared/types/questions/match-columns-to-make-sentences.js +1 -0
- package/dist/shared/types/questions/match-word-to-picture-group.d.ts +31 -0
- package/dist/shared/types/questions/match-word-to-picture-group.js +1 -0
- package/dist/shared/types/questions/word-ordering-group.d.ts +28 -0
- package/dist/shared/types/questions/word-ordering-group.js +1 -0
- package/package.json +1 -1
package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js
CHANGED
|
@@ -4,6 +4,8 @@ import { useState, useEffect, useMemo } from 'react';
|
|
|
4
4
|
import { Check, X, CircleHelp, Lightbulb, ClipboardList, Users, BookOpen } from 'lucide-react';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
6
|
import { cn } from '../../../../shared/lib/utils';
|
|
7
|
+
import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
|
|
8
|
+
import { isRichTextEmpty, richTextToPlainText } from '../../../../shared/lib/rich-text';
|
|
7
9
|
import { normalizeChooseAnswerCorrectAnswer, normalizeChooseAnswerOptions, optionToString } from './normalize';
|
|
8
10
|
// Helper component to resolve presigned URL for each image option
|
|
9
11
|
function OptionImage({ src, alt, className }) {
|
|
@@ -82,7 +84,7 @@ export function ChooseTheCorrectAnswerClient({ questionData, onSubmit, isReviewM
|
|
|
82
84
|
const optText = optionToString(opt);
|
|
83
85
|
return questionData.optionType === 'image'
|
|
84
86
|
? `Đáp án ${answerLetter}`
|
|
85
|
-
: `${answerLetter}. ${optText}`;
|
|
87
|
+
: `${answerLetter}. ${richTextToPlainText(optText)}`;
|
|
86
88
|
}).join(', ');
|
|
87
89
|
};
|
|
88
90
|
// Visibility toggles based on questionConfig (from template)
|
|
@@ -109,7 +111,7 @@ export function ChooseTheCorrectAnswerClient({ questionData, onSubmit, isReviewM
|
|
|
109
111
|
.split('\n')
|
|
110
112
|
.map((line) => `<p class="last:mb-0">${line}</p>`)
|
|
111
113
|
.join(''),
|
|
112
|
-
} })), questionData.groupImageUrl && (_jsxs("div", { className: "mt-5 relative overflow-hidden rounded-xl border border-slate-200/60 bg-slate-50 shadow-sm transition-all hover:shadow-md", children: [_jsx("div", { className: "flex items-center justify-between border-b border-slate-200/60 bg-slate-100/50 px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("span", { className: "ml-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: "T\u00E0i li\u1EC7u \u0111\u00EDnh k\u00E8m" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4 min-h-[160px]", children: _jsx(OptionImage, { src: questionData.groupImageUrl, alt: "Group illustration", className: "max-h-64 w-auto rounded-lg object-contain" }) })] }))] }))] })] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xs", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3.5", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-xs", children: _jsx(CircleHelp, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: isMultipleAnswers ? 'Câu hỏi nhiều đáp án' : 'Chọn đáp án đúng' })] }), questionData.points !== undefined && (_jsx("div", { className: "flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: _jsxs("span", { children: [questionData.points, " \u0111i\u1EC3m"] }) }))] }), _jsxs("div", { className: "p-5 space-y-4", children: [(questionData.askerName || questionData.answererName) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-gray-50 p-2.5 text-xs text-gray-600", children: [_jsx(Users, { className: "h-4 w-4 text-gray-400" }), questionData.askerName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi h\u1ECFi:" }), " ", questionData.askerName] })), questionData.askerName && questionData.answererName && _jsx("span", { children: "\u2022" }), questionData.answererName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi:" }), " ", questionData.answererName] }))] })), showQuestion && (_jsx("div", { className: "rounded-lg bg-gradient-to-r from-blue-50 to-indigo-50 p-4 border border-blue-100", children: _jsx("h3", { className: "text-base font-semibold text-gray-800 leading-relaxed", children: questionData.question
|
|
114
|
+
} })), questionData.groupImageUrl && (_jsxs("div", { className: "mt-5 relative overflow-hidden rounded-xl border border-slate-200/60 bg-slate-50 shadow-sm transition-all hover:shadow-md", children: [_jsx("div", { className: "flex items-center justify-between border-b border-slate-200/60 bg-slate-100/50 px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("span", { className: "ml-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: "T\u00E0i li\u1EC7u \u0111\u00EDnh k\u00E8m" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4 min-h-[160px]", children: _jsx(OptionImage, { src: questionData.groupImageUrl, alt: "Group illustration", className: "max-h-64 w-auto rounded-lg object-contain" }) })] }))] }))] })] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xs", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3.5", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-xs", children: _jsx(CircleHelp, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: isMultipleAnswers ? 'Câu hỏi nhiều đáp án' : 'Chọn đáp án đúng' })] }), questionData.points !== undefined && (_jsx("div", { className: "flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: _jsxs("span", { children: [questionData.points, " \u0111i\u1EC3m"] }) }))] }), _jsxs("div", { className: "p-5 space-y-4", children: [(questionData.askerName || questionData.answererName) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-gray-50 p-2.5 text-xs text-gray-600", children: [_jsx(Users, { className: "h-4 w-4 text-gray-400" }), questionData.askerName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi h\u1ECFi:" }), " ", questionData.askerName] })), questionData.askerName && questionData.answererName && _jsx("span", { children: "\u2022" }), questionData.answererName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi:" }), " ", questionData.answererName] }))] })), showQuestion && (_jsx("div", { className: "rounded-lg bg-gradient-to-r from-blue-50 to-indigo-50 p-4 border border-blue-100", children: isRichTextEmpty(questionData.question) ? (_jsx("h3", { className: "text-base font-semibold text-gray-800 leading-relaxed", children: "C\u00E2u h\u1ECFi ch\u01B0a \u0111\u01B0\u1EE3c nh\u1EADp" })) : (_jsx(RichTextHtml, { html: questionData.question, className: "text-base font-semibold text-gray-800 leading-relaxed" })) })), showImage && (imagePreviewUrl || cleanImageUrl) && (_jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2 w-2 rounded-full bg-red-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-green-400" }), _jsx("span", { className: "ml-2 text-xs font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4", children: isLoading ? (_jsx("div", { className: "flex h-48 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })) : (
|
|
113
115
|
/* eslint-disable-next-line @next/next/no-img-element */
|
|
114
116
|
_jsx("img", { src: imagePreviewUrl || cleanImageUrl, alt: "Question illustration", className: "max-h-56 w-auto object-contain rounded-md", onError: (e) => {
|
|
115
117
|
const target = e.target;
|
|
@@ -136,11 +138,11 @@ export function ChooseTheCorrectAnswerClient({ questionData, onSubmit, isReviewM
|
|
|
136
138
|
: 'bg-gray-200 text-gray-600'
|
|
137
139
|
: isSelected
|
|
138
140
|
? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm'
|
|
139
|
-
: 'bg-gray-100 text-gray-600 group-hover:bg-gray-200'), children: letter }), questionData.optionType === 'image' ? (_jsx("div", { className: "flex-1 flex items-center justify-center py-1", children: _jsx(OptionImage, { src: optionText, alt: `Đáp án ${letter}` }) })) : (_jsx(
|
|
141
|
+
: 'bg-gray-100 text-gray-600 group-hover:bg-gray-200'), children: letter }), questionData.optionType === 'image' ? (_jsx("div", { className: "flex-1 flex items-center justify-center py-1", children: _jsx(OptionImage, { src: optionText, alt: `Đáp án ${letter}` }) })) : (_jsx(RichTextHtml, { html: optionText, className: cn('flex-1 text-sm font-medium', isReviewMode && isCorrectAnswer
|
|
140
142
|
? 'text-green-800'
|
|
141
143
|
: isReviewMode && isSelected && !isCorrectAnswer
|
|
142
144
|
? 'text-red-800'
|
|
143
|
-
: 'text-gray-700')
|
|
145
|
+
: 'text-gray-700') })), isReviewMode && (isCorrectAnswer || isSelected) && (_jsx("div", { className: cn('flex h-6 w-6 items-center justify-center rounded-full', isCorrectAnswer ? 'bg-green-500' : 'bg-red-500'), children: isCorrectAnswer
|
|
144
146
|
? _jsx(Check, { className: "h-4 w-4 text-white" })
|
|
145
147
|
: _jsx(X, { className: "h-4 w-4 text-white" }) }))] }, index));
|
|
146
148
|
})] })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2", 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: isMultipleAnswers ? 'Vui lòng chọn ít nhất một đáp án' : 'Vui lòng chọn một đáp án' })] }))] })] }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50 shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-blue-100 bg-white/50 px-4 py-3", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 text-white shadow-sm", children: _jsx(Lightbulb, { className: "h-4 w-4" }) }), _jsx("h4", { className: "text-sm font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("div", { className: "p-4", children: _jsx("p", { className: "text-sm text-gray-700 leading-relaxed", children: questionData.explanation }) })] })), isReviewMode && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4 py-3", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-slate-500 to-gray-600 text-white shadow-sm", children: _jsx(ClipboardList, { className: "h-4 w-4" }) }), _jsx("h4", { className: "text-sm font-semibold text-gray-800", children: "T\u1ED5ng k\u1EBFt" })] }), _jsxs("div", { className: "p-4 space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg bg-gray-50 p-3", children: [_jsx("span", { className: "text-sm text-gray-600", children: "\u0110\u00E1p \u00E1n c\u1EE7a b\u1EA1n:" }), _jsx("span", { className: cn('text-sm font-semibold', isAnswerCorrect ? 'text-green-600' : hasAnswered ? 'text-red-600' : 'text-gray-500'), children: formatAnswerLabel(selectedAnswer) })] }), _jsxs("div", { className: "flex items-center justify-between rounded-lg bg-green-50 p-3 border border-green-100", children: [_jsx("span", { className: "text-sm text-gray-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng:" }), _jsx("span", { className: "text-sm font-semibold text-green-600", children: formatAnswerLabel(normalizedCorrectAnswers) })] }), _jsxs("div", { className: cn('flex items-center justify-between rounded-lg p-3 border', isAnswerCorrect
|
package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js
CHANGED
|
@@ -14,6 +14,8 @@ import { usePresignedFileUrl, useDebouncedCallback } from '../../../../shared/li
|
|
|
14
14
|
import { useReactHookForm } from '../../../../shared/lib/hooks/useReactHookForm';
|
|
15
15
|
import { Pencil, Eye, Plus, Trash2, HelpCircle, CheckCircle2, Lightbulb, MessageSquare, ImageIcon, Type, Users, BookOpen, Star, ListChecks } from 'lucide-react';
|
|
16
16
|
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
17
|
+
import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
18
|
+
import { isRichTextEmpty } from '../../../../shared/lib/rich-text';
|
|
17
19
|
import { ChooseTheCorrectAnswerClient } from './ChooseTheCorrectAnswerClient';
|
|
18
20
|
import { normalizeChooseAnswerCorrectAnswer, normalizeChooseAnswerOptions, pickRawChooseAnswer, } from './normalize';
|
|
19
21
|
import { chooseCorrectAnswerFormSchema, } from '../../../../shared/lib/validation/choose-correct-answer.validation';
|
|
@@ -410,7 +412,9 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
|
|
|
410
412
|
: undefined })] }));
|
|
411
413
|
}
|
|
412
414
|
// Creator UI
|
|
413
|
-
return (_jsxs("div", { className: "space-y-4", children: [isGrouped && effectiveGroupType === 'ARTICLES' && (_jsx(ArticlesGroupCard, { groupNumber: currentGroupNumber, articleCount: articleCount, articles: articles, onArticlesChange: setArticles, title: groupTitle, onTitleChange: setGroupTitle, isReadOnly: !isFirstInGroup, uploadIdPrefix: `choose-correct-answer-${partId ?? 'articles'}` })), isGrouped && effectiveGroupType === 'BASIC' && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `choose-correct-answer-${partId ?? 'group'}` })), isGrouped && effectiveGroupType === 'DOCUMENT' && (_jsx(DocumentGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `choose-correct-answer-${partId ?? 'document'}` })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100", children: _jsx(HelpCircle, { className: "h-4 w-4 text-gray-600" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "Ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("p", { className: "text-xs text-gray-500", children: "T\u1EA1o c\u00E2u h\u1ECFi tr\u1EAFc nghi\u1EC7m v\u1EDBi nhi\u1EC1u l\u1EF1a ch\u1ECDn" })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [headerExtra, _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: watchedOptions.filter((o) =>
|
|
415
|
+
return (_jsxs("div", { className: "space-y-4", children: [isGrouped && effectiveGroupType === 'ARTICLES' && (_jsx(ArticlesGroupCard, { groupNumber: currentGroupNumber, articleCount: articleCount, articles: articles, onArticlesChange: setArticles, title: groupTitle, onTitleChange: setGroupTitle, isReadOnly: !isFirstInGroup, uploadIdPrefix: `choose-correct-answer-${partId ?? 'articles'}` })), isGrouped && effectiveGroupType === 'BASIC' && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `choose-correct-answer-${partId ?? 'group'}` })), isGrouped && effectiveGroupType === 'DOCUMENT' && (_jsx(DocumentGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `choose-correct-answer-${partId ?? 'document'}` })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100", children: _jsx(HelpCircle, { className: "h-4 w-4 text-gray-600" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "Ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("p", { className: "text-xs text-gray-500", children: "T\u1EA1o c\u00E2u h\u1ECFi tr\u1EAFc nghi\u1EC7m v\u1EDBi nhi\u1EC1u l\u1EF1a ch\u1ECDn" })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [headerExtra, _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: watchedOptions.filter((o) => watchedOptionType === 'image'
|
|
416
|
+
? typeof o === 'string' && o.trim()
|
|
417
|
+
: !isRichTextEmpty(o)).length < 2, className: "gap-2 border-indigo-200 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(Eye, { className: "h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] })] })] }) }), _jsxs(CardContent, { className: "space-y-3 px-4 pb-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-y-2", children: [_jsx(PointsInput, { ...form.register('points', { valueAsNumber: true }), error: form.hasError('points') ? form.getFieldError('points') : undefined }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("button", { type: "button", style: { paddingBlock: 'calc(var(--spacing) * 1)' }, onClick: () => form.setValue('isExample', !watchedIsExample), className: `inline-flex items-center gap-2 rounded-md border px-3.5 text-sm font-medium transition-colors select-none ${watchedIsExample
|
|
414
418
|
? 'border-amber-300 bg-amber-50 text-amber-700'
|
|
415
419
|
: 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'}`, children: [_jsx(Star, { className: `h-4 w-4 ${watchedIsExample ? 'fill-amber-400 text-amber-400' : ''}` }), "Example"] }), _jsxs("button", { type: "button", style: { paddingBlock: 'calc(var(--spacing) * 1)' }, disabled: Boolean(multipleAnswers), onClick: () => {
|
|
416
420
|
const enabled = !watchedMultipleAnswers;
|
|
@@ -422,7 +426,7 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
|
|
|
422
426
|
form.setValue('correctAnswer', enabled ? normalized : (normalized[0] ?? -1), { shouldValidate: true });
|
|
423
427
|
}, className: `inline-flex items-center gap-2 rounded-md border px-3.5 text-sm font-medium transition-colors select-none ${watchedMultipleAnswers
|
|
424
428
|
? 'border-violet-300 bg-violet-50 text-violet-700'
|
|
425
|
-
: 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'} ${Boolean(multipleAnswers) ? 'cursor-not-allowed opacity-50' : ''}`, children: [_jsx(ListChecks, { className: "h-4 w-4" }), "Nhi\u1EC1u \u0111\u00E1p \u00E1n"] })] })] }), viewMode === 'CONVERSATION' && (_jsxs("div", { className: "space-y-3", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Users, { className: "h-4 w-4 text-indigo-500" }), "H\u1ED9i tho\u1EA1i ", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn - d\u00F9ng cho c\u00E2u h\u1ECFi d\u1EA1ng h\u1ED9i tho\u1EA1i)" })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4 rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: "askerName", className: "flex items-center gap-1 text-sm font-medium text-gray-600", children: ["T\u00EAn ng\u01B0\u1EDDi h\u1ECFi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "askerName", ...form.register('askerName'), placeholder: "VD: John, Mary, Teacher...", className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('askerName') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` }), form.hasError('askerName') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('askerName') }))] }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: "answererName", className: "flex items-center gap-1 text-sm font-medium text-gray-600", children: ["T\u00EAn ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "answererName", ...form.register('answererName'), placeholder: "VD: Tom, Anna, Student...", className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('answererName') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` }), form.hasError('answererName') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('answererName') }))] })] })] })), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('question')) && (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "question", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-indigo-500" }), _jsx("span", { className: "flex items-center gap-1", children: "C\u00E2u h\u1ECFi" }), _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(
|
|
429
|
+
: 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'} ${Boolean(multipleAnswers) ? 'cursor-not-allowed opacity-50' : ''}`, children: [_jsx(ListChecks, { className: "h-4 w-4" }), "Nhi\u1EC1u \u0111\u00E1p \u00E1n"] })] })] }), viewMode === 'CONVERSATION' && (_jsxs("div", { className: "space-y-3", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Users, { className: "h-4 w-4 text-indigo-500" }), "H\u1ED9i tho\u1EA1i ", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn - d\u00F9ng cho c\u00E2u h\u1ECFi d\u1EA1ng h\u1ED9i tho\u1EA1i)" })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4 rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: "askerName", className: "flex items-center gap-1 text-sm font-medium text-gray-600", children: ["T\u00EAn ng\u01B0\u1EDDi h\u1ECFi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "askerName", ...form.register('askerName'), placeholder: "VD: John, Mary, Teacher...", className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('askerName') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` }), form.hasError('askerName') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('askerName') }))] }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: "answererName", className: "flex items-center gap-1 text-sm font-medium text-gray-600", children: ["T\u00EAn ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "answererName", ...form.register('answererName'), placeholder: "VD: Tom, Anna, Student...", className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('answererName') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` }), form.hasError('answererName') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('answererName') }))] })] })] })), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('question')) && (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "question", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-indigo-500" }), _jsx("span", { className: "flex items-center gap-1", children: "C\u00E2u h\u1ECFi" }), _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(RichTextEditor, { value: watchedQuestion, onChange: (html) => form.setValue('question', html, { shouldValidate: true }), variant: "compact", minHeightClassName: "min-h-[100px]" }), form.hasError('question') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('question') }))] })), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('imageUrl') || questionConfig.includes('imageUrl ')) && (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-indigo-500" }), "H\u00ECnh \u1EA3nh ", _jsx("span", { className: "text-sm font-normal text-gray-500" })] }), _jsx(FileUpload, { id: "question-image", label: "", accept: "image/*", value: watchedImageUrl || '', onChange: (url) => {
|
|
426
430
|
form.setValue('imageUrl', url, { shouldValidate: true });
|
|
427
431
|
}, onPresignedUrlChange: setImagePreviewUrl, maxSize: 5, placeholder: "Upload \u1EA3nh ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }), form.hasError('imageUrl') && (_jsx("p", { className: "mt-1 text-sm text-red-600", children: form.getFieldError('imageUrl') })), displayPreviewUrl && (_jsx(ImagePreview, { src: displayPreviewUrl, alt: "Preview", className: "mt-2 h-48 w-full max-w-md rounded-lg border border-gray-200" }))] })), externalErrors && externalErrors.length > 0 && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-red-100", children: _jsx("span", { className: "text-lg", children: "\u274C" }) }), _jsxs("div", { className: "text-sm", children: [_jsx("p", { className: "font-semibold text-red-800", children: "L\u1ED7i:" }), _jsx("ul", { className: "mt-1 list-inside list-disc text-red-700", children: externalErrors.map((error, index) => (_jsx("li", { children: error }, index))) })] })] })), form.formState.errors.options?.message && (_jsx("div", { className: "rounded-xl border border-red-200 bg-red-50 p-4", children: _jsx("p", { className: "text-sm text-red-600", children: form.formState.errors.options.message }) }))] })] }), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('options')) && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-indigo-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-indigo-100", children: _jsx(Lightbulb, { className: "h-3.5 w-3.5 text-indigo-600" }) }), _jsxs(CardTitle, { className: "text-base font-semibold text-gray-800", children: ["C\u00E1c l\u1EF1a ch\u1ECDn ", _jsxs("span", { className: "text-xs font-normal text-gray-500", children: ["(", watchedOptions.length, " \u0111\u00E1p \u00E1n)"] })] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-1.5", children: [_jsx(Type, { className: `h-3.5 w-3.5 ${watchedOptionType === 'text' ? 'text-indigo-600' : 'text-gray-400'}` }), _jsx("span", { className: `text-xs font-medium ${watchedOptionType === 'text' ? 'text-indigo-600' : 'text-gray-400'}`, children: "Text" }), _jsx(Switch, { checked: watchedOptionType === 'image', disabled: true, className: watchedOptionType === 'image' ? '!bg-indigo-600 !opacity-100' : '!bg-indigo-600 !opacity-100', onCheckedChange: (checked) => {
|
|
428
432
|
const newType = checked ? 'image' : 'text';
|
|
@@ -488,7 +492,11 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
|
|
|
488
492
|
setOptionPreviewUrls((prev) => ({ ...prev, [index]: presignedUrl }));
|
|
489
493
|
}, maxSize: 5, placeholder: `Upload ảnh đáp án ${optionLetter}...`, autoUpload: true, prefix: "questions/options" }), (optionPreviewUrls[index] || option) && (_jsx(OptionImagePreview, { src: optionPreviewUrls[index] || option, alt: `Đáp án ${optionLetter}` }))] })) : (
|
|
490
494
|
/* Text Input Mode */
|
|
491
|
-
_jsx(
|
|
495
|
+
_jsx(RichTextEditor, { value: option, onChange: (html) => {
|
|
496
|
+
const newOptions = [...watchedOptions];
|
|
497
|
+
newOptions[index] = html;
|
|
498
|
+
form.setValue('options', newOptions, { shouldValidate: true });
|
|
499
|
+
}, variant: "compact", minHeightClassName: "min-h-[72px]" })), optionError && (_jsx("p", { className: "text-sm text-red-600", children: optionError.message })), isCorrect && (_jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-xs font-semibold text-green-700", children: [_jsx(CheckCircle2, { className: "h-3.5 w-3.5" }), watchedMultipleAnswers ? 'Đáp án đúng đã chọn' : 'Đáp án đúng'] }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
492
500
|
if (watchedMultipleAnswers) {
|
|
493
501
|
const currentAnswers = Array.isArray(form.getValues('correctAnswer'))
|
|
494
502
|
? form.getValues('correctAnswer')
|
|
@@ -543,7 +551,11 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
|
|
|
543
551
|
setOptionPreviewUrls((prev) => ({ ...prev, [index]: presignedUrl }));
|
|
544
552
|
}, maxSize: 5, placeholder: `Upload ảnh đáp án ${optionLetter}...`, autoUpload: true, prefix: "questions/options" }), (optionPreviewUrls[index] || option) && (_jsx(OptionImagePreview, { src: optionPreviewUrls[index] || option, alt: `Đáp án ${optionLetter}` }))] })) : (
|
|
545
553
|
/* Text Input Mode */
|
|
546
|
-
_jsx(
|
|
554
|
+
_jsx(RichTextEditor, { value: option, onChange: (html) => {
|
|
555
|
+
const newOptions = [...watchedOptions];
|
|
556
|
+
newOptions[index] = html;
|
|
557
|
+
form.setValue('options', newOptions, { shouldValidate: true });
|
|
558
|
+
}, variant: "compact", minHeightClassName: "min-h-[72px]" })), optionError && (_jsx("p", { className: "text-sm text-red-600", children: optionError.message })), isCorrect && (_jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-xs font-semibold text-green-700", children: [_jsx(CheckCircle2, { className: "h-3.5 w-3.5" }), "\u0110\u00E1p \u00E1n \u0111\u00FAng"] }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
547
559
|
form.setValue('correctAnswer', index, { shouldValidate: true });
|
|
548
560
|
}, className: isCorrect ? 'text-green-600 hover:text-green-700' : 'text-gray-400 hover:text-green-600', title: "Ch\u1ECDn l\u00E0m \u0111\u00E1p \u00E1n \u0111\u00FAng", children: _jsx(CheckCircle2, { className: "h-4 w-4" }) }), watchedOptions.length > 2 && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
|
|
549
561
|
const currentOptions = form.getValues('options');
|
|
@@ -34,11 +34,10 @@ export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode =
|
|
|
34
34
|
next[itemIndex] = optionId;
|
|
35
35
|
onAnswerChange(next);
|
|
36
36
|
};
|
|
37
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), questionData.audioUrl && _jsx(ClientAudio, { url: questionData.audioUrl }), questionData.passage && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none text-gray-800 leading-relaxed", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
|
|
37
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), questionData.audioUrl && _jsx(ClientAudio, { url: questionData.audioUrl }), toChooseAnswerGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh nhóm (${imageIndex + 1})` }, `group-image-${imageIndex}`))) })), questionData.passage && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none text-gray-800 leading-relaxed", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
|
|
38
38
|
const selectedId = userAnswers[itemIndex];
|
|
39
39
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
40
|
-
|
|
41
|
-
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), showItemAudio && item.audioUrl && _jsx(ClientAudio, { url: item.audioUrl }), toChooseAnswerGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), item.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: item.options.map((option, optionIndex) => {
|
|
40
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), item.audioUrl && _jsx(ClientAudio, { url: item.audioUrl }), toChooseAnswerGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), item.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: item.options.map((option, optionIndex) => {
|
|
42
41
|
const isCorrect = option.id === item.correctAnswer;
|
|
43
42
|
const isSelected = option.id === selectedId;
|
|
44
43
|
const optionImageSrc = getChooseAnswerGroupOptionImageSrc(option);
|
|
@@ -70,6 +70,7 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
70
70
|
const [instruction, setInstruction] = useState(initialData?.instruction || 'Choose the correct answer.');
|
|
71
71
|
const [passage, setPassage] = useState(initialData?.passage || '');
|
|
72
72
|
const [audioUrl, setAudioUrl] = useState(initialData?.audioUrl || '');
|
|
73
|
+
const [imageUrl, setImageUrl] = useState(toChooseAnswerGroupImageUrls(initialData?.imageUrl)[0] || '');
|
|
73
74
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
74
75
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
75
76
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
@@ -80,10 +81,11 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
80
81
|
onChangeRef.current = onChange;
|
|
81
82
|
}, [onChange]);
|
|
82
83
|
const isContentEmpty = (html) => !html || html === '<p></p>' || html === '<p><br></p>' || html.trim() === '';
|
|
83
|
-
const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation, nextAudioUrl = audioUrl) => ({
|
|
84
|
+
const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation, nextAudioUrl = audioUrl, nextImageUrl = imageUrl) => ({
|
|
84
85
|
instruction: nextInstruction,
|
|
85
86
|
...(!isContentEmpty(nextPassage) ? { passage: nextPassage.trim() } : {}),
|
|
86
87
|
...(nextAudioUrl.trim() ? { audioUrl: nextAudioUrl.trim() } : {}),
|
|
88
|
+
...(nextImageUrl.trim() ? { imageUrl: nextImageUrl.trim() } : {}),
|
|
87
89
|
items: nextItems,
|
|
88
90
|
explanation: nextExplanation,
|
|
89
91
|
points: sumPoints(nextItems),
|
|
@@ -100,7 +102,7 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
100
102
|
previousPayloadRef.current = serialized;
|
|
101
103
|
onUnsavedChangesChange?.(true);
|
|
102
104
|
debouncedOnChange(payload);
|
|
103
|
-
}, [instruction, passage, audioUrl, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
105
|
+
}, [instruction, passage, audioUrl, imageUrl, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
104
106
|
const validate = () => {
|
|
105
107
|
const nextErrors = [];
|
|
106
108
|
if (!instruction.trim()) {
|
|
@@ -165,10 +167,10 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
165
167
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Ch\u1EC9nh s\u1EEDa"] }) }), _jsx(ChooseTheCorrectAnswerGroupClient, { questionData: buildPayload(), isReviewMode: true })] }));
|
|
166
168
|
}
|
|
167
169
|
const allErrors = [...errors, ...(externalErrors || [])];
|
|
168
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(true), children: [_jsx(Eye, { className: "mr-2 h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] }) }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Nh\u00F3m ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "group-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "group-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: "Choose the correct answer." })] }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: "choose-answer-group-audio", label: "Audio chung (tu\u1EF3 ch\u1ECDn)", accept: "audio/*", value: audioUrl, onChange: setAudioUrl, maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage - Tu\u1EF3 ch\u1ECDn)" }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] }), _jsxs("div", { className: "rounded-xl border border-gray-200 bg-gray-50 p-4", children: [_jsx(PointsInput, { value: sumPoints(items), allowZero: true, readOnly: true }), _jsx("p", { className: "mt-2 text-xs text-gray-500", children: "T\u1ED5ng \u0111i\u1EC3m \u0111\u01B0\u1EE3c c\u1ED9ng t\u1EEB c\u00E1c c\u00E2u kh\u00F4ng ph\u1EA3i example." })] })] })] }), items.map((item, itemIndex) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0", children: [_jsxs(CardTitle, { className: "text-base", children: ["C\u00E2u ", itemIndex + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeItem(itemIndex), disabled: items.length <= 1, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg border border-amber-100 bg-amber-50 px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-amber-800", children: [_jsx(Star, { className: "h-4 w-4" }), "C\u00E2u v\u00ED d\u1EE5"] }), _jsx(Switch, { checked: item.isExample === true, onCheckedChange: (checked) => updateItem(itemIndex, {
|
|
170
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(true), children: [_jsx(Eye, { className: "mr-2 h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] }) }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Nh\u00F3m ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "group-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "group-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: "Choose the correct answer." })] }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: "choose-answer-group-audio", label: "Audio chung (tu\u1EF3 ch\u1ECDn)", accept: "audio/*", value: audioUrl, onChange: setAudioUrl, maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: "choose-answer-group-image", label: "H\u00ECnh \u1EA3nh chung (tu\u1EF3 ch\u1ECDn)", accept: "image/*", value: imageUrl, onChange: setImageUrl, maxSize: 5, placeholder: "Upload \u1EA3nh nh\u00F3m ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }) }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage - Tu\u1EF3 ch\u1ECDn)" }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] }), _jsxs("div", { className: "rounded-xl border border-gray-200 bg-gray-50 p-4", children: [_jsx(PointsInput, { value: sumPoints(items), allowZero: true, readOnly: true }), _jsx("p", { className: "mt-2 text-xs text-gray-500", children: "T\u1ED5ng \u0111i\u1EC3m \u0111\u01B0\u1EE3c c\u1ED9ng t\u1EEB c\u00E1c c\u00E2u kh\u00F4ng ph\u1EA3i example." })] })] })] }), items.map((item, itemIndex) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0", children: [_jsxs(CardTitle, { className: "text-base", children: ["C\u00E2u ", itemIndex + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeItem(itemIndex), disabled: items.length <= 1, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg border border-amber-100 bg-amber-50 px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-amber-800", children: [_jsx(Star, { className: "h-4 w-4" }), "C\u00E2u v\u00ED d\u1EE5"] }), _jsx(Switch, { checked: item.isExample === true, onCheckedChange: (checked) => updateItem(itemIndex, {
|
|
169
171
|
isExample: checked,
|
|
170
172
|
points: checked ? 0 : (item.points || 1),
|
|
171
|
-
}) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "C\u00E2u h\u1ECFi" }), _jsx(Textarea, { value: item.question, onChange: (event) => updateItem(itemIndex, { question: event.target.value }), rows: 2, placeholder: "A place for someone to stay or live is called" })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { className: "flex items-center gap-2", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-teal-500" }), "H\u00ECnh \u1EA3nh (tu\u1EF3 ch\u1ECDn)"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
|
|
173
|
+
}) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "C\u00E2u h\u1ECFi" }), _jsx(Textarea, { value: item.question, onChange: (event) => updateItem(itemIndex, { question: event.target.value }), rows: 2, placeholder: "A place for someone to stay or live is called" })] }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: `choose-answer-item-audio-${itemIndex}`, label: "Audio c\u00E2u h\u1ECFi (tu\u1EF3 ch\u1ECDn)", accept: "audio/*", value: item.audioUrl || '', onChange: (url) => updateItem(itemIndex, { audioUrl: url }), maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { className: "flex items-center gap-2", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-teal-500" }), "H\u00ECnh \u1EA3nh (tu\u1EF3 ch\u1ECDn)"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
|
|
172
174
|
const nextUrls = [...toEditableChooseAnswerGroupImageUrls(item.imageUrl), ''];
|
|
173
175
|
updateItem(itemIndex, { imageUrl: nextUrls });
|
|
174
176
|
}, className: "gap-1.5 border-teal-200 text-teal-600 hover:bg-teal-50 hover:text-teal-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm h\u00ECnh \u1EA3nh"] })] }), toEditableChooseAnswerGroupImageUrls(item.imageUrl).length === 0 ? (_jsx("div", { className: "rounded-lg border border-dashed border-gray-200 bg-gray-50/50 p-3 text-xs text-gray-500", children: "Ch\u01B0a c\u00F3 h\u00ECnh \u1EA3nh. Nh\u1EA5n \"Th\u00EAm h\u00ECnh \u1EA3nh\" \u0111\u1EC3 upload \u1EA3nh minh ho\u1EA1 cho c\u00E2u h\u1ECFi." })) : (_jsx("div", { className: "space-y-3", children: toEditableChooseAnswerGroupImageUrls(item.imageUrl).map((imageUrl, imageIndex) => (_jsx(ChooseAnswerGroupImageUploadItem, { index: imageIndex, itemIndex: itemIndex, imageUrl: imageUrl, onChange: (url) => {
|
|
@@ -8,6 +8,16 @@ function asRecord(value) {
|
|
|
8
8
|
function asString(value, fallback = '') {
|
|
9
9
|
return typeof value === 'string' ? value : fallback;
|
|
10
10
|
}
|
|
11
|
+
function asOptionId(value) {
|
|
12
|
+
if (typeof value === 'string') {
|
|
13
|
+
return value.trim();
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
const first = value.find((item) => typeof item === 'string' && item.trim() !== '');
|
|
17
|
+
return typeof first === 'string' ? first.trim() : '';
|
|
18
|
+
}
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
11
21
|
function asOptionalUrl(value) {
|
|
12
22
|
if (typeof value !== 'string') {
|
|
13
23
|
return undefined;
|
|
@@ -63,7 +73,7 @@ export function mapChooseAnswerGroupItem(item, index, fallbackAnswer) {
|
|
|
63
73
|
question: asString(content.question) || asString(record.question),
|
|
64
74
|
optionType: asString(content.optionType) === 'image' ? 'image' : 'text',
|
|
65
75
|
options,
|
|
66
|
-
correctAnswer:
|
|
76
|
+
correctAnswer: asOptionId(correctAnswer.answer) || fallbackAnswer || options[0]?.id || 'opt-1',
|
|
67
77
|
isExample,
|
|
68
78
|
points: isExample ? 0 : typeof record.points === 'number' ? record.points : 1,
|
|
69
79
|
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
@@ -78,15 +88,17 @@ export function mapQuestionToChooseAnswerGroupData(question) {
|
|
|
78
88
|
const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
|
|
79
89
|
const explanation = String(source.explanation || answer.explanation || '');
|
|
80
90
|
const fallbackAnswers = Array.isArray(correctAnswer.answer)
|
|
81
|
-
? correctAnswer.answer.map((value) =>
|
|
91
|
+
? correctAnswer.answer.map((value) => asOptionId(value))
|
|
82
92
|
: [];
|
|
83
93
|
if (Array.isArray(content.items)) {
|
|
84
94
|
const items = content.items.map((item, index) => mapChooseAnswerGroupItem(item, index, fallbackAnswers[index]));
|
|
85
95
|
const audioUrl = asOptionalUrl(asRecord(content.meta).audioUrl);
|
|
96
|
+
const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(asRecord(content.meta).imageUrl));
|
|
86
97
|
return {
|
|
87
98
|
instruction: asString(asRecord(content.meta).instruction) || 'Choose the correct answer.',
|
|
88
99
|
passage: asString(asRecord(content.meta).passage),
|
|
89
100
|
...(audioUrl ? { audioUrl } : {}),
|
|
101
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
90
102
|
items,
|
|
91
103
|
explanation,
|
|
92
104
|
points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
|
|
@@ -94,10 +106,12 @@ export function mapQuestionToChooseAnswerGroupData(question) {
|
|
|
94
106
|
}
|
|
95
107
|
if (Array.isArray(answer.items)) {
|
|
96
108
|
const audioUrl = asOptionalUrl(answer.audioUrl);
|
|
109
|
+
const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(answer.imageUrl));
|
|
97
110
|
return {
|
|
98
111
|
instruction: asString(answer.instruction, 'Choose the correct answer.'),
|
|
99
112
|
passage: asString(answer.passage),
|
|
100
113
|
...(audioUrl ? { audioUrl } : {}),
|
|
114
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
101
115
|
items: answer.items.map((item, index) => mapChooseAnswerGroupItem(item, index, fallbackAnswers[index])),
|
|
102
116
|
explanation,
|
|
103
117
|
points: typeof answer.points === 'number' ? answer.points : undefined,
|
|
@@ -51,6 +51,10 @@ export const transformChooseTheCorrectAnswerGroup = (question) => {
|
|
|
51
51
|
if (answerData?.audioUrl && answerData.audioUrl.trim()) {
|
|
52
52
|
meta.audioUrl = answerData.audioUrl.trim();
|
|
53
53
|
}
|
|
54
|
+
const groupImageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(answerData?.imageUrl));
|
|
55
|
+
if (groupImageUrl !== undefined) {
|
|
56
|
+
meta.imageUrl = groupImageUrl;
|
|
57
|
+
}
|
|
54
58
|
return {
|
|
55
59
|
apiContent: {
|
|
56
60
|
meta,
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { MatchColumnsGroupClientProps } from '../../_shared/types/match-columns-to-make-sentences.type';
|
|
2
|
+
export declare function MatchColumnsToMakeSentencesClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: MatchColumnsGroupClientProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Lightbulb, Star } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../../../shared/lib/utils';
|
|
5
|
+
import { assembleMatchColumnsSentence, getMatchColumnsChoiceColumns, getMatchColumnsStemColumn, } from './map-match-columns-to-make-sentences-data';
|
|
6
|
+
function emptyAnswer(stemFragmentId) {
|
|
7
|
+
return { fragmentIds: stemFragmentId ? [stemFragmentId] : [], sentence: '' };
|
|
8
|
+
}
|
|
9
|
+
export function MatchColumnsToMakeSentencesClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
10
|
+
const items = questionData.items || [];
|
|
11
|
+
const columns = questionData.columns || [];
|
|
12
|
+
const stemColumn = getMatchColumnsStemColumn(columns);
|
|
13
|
+
const choiceColumns = getMatchColumnsChoiceColumns(columns);
|
|
14
|
+
const canEdit = Boolean(onAnswerChange) && !isReviewMode;
|
|
15
|
+
const resolveAnswer = (itemIndex) => {
|
|
16
|
+
const item = items[itemIndex];
|
|
17
|
+
if (!item)
|
|
18
|
+
return emptyAnswer('');
|
|
19
|
+
if (item.isExample) {
|
|
20
|
+
return { fragmentIds: item.fragmentIds, sentence: item.sentence };
|
|
21
|
+
}
|
|
22
|
+
const raw = userAnswers[itemIndex];
|
|
23
|
+
if (raw && Array.isArray(raw.fragmentIds)) {
|
|
24
|
+
return raw;
|
|
25
|
+
}
|
|
26
|
+
return emptyAnswer(item.stemFragmentId);
|
|
27
|
+
};
|
|
28
|
+
const handleChoiceChange = (itemIndex, choiceIndex, fragmentId) => {
|
|
29
|
+
if (!canEdit || items[itemIndex]?.isExample)
|
|
30
|
+
return;
|
|
31
|
+
const next = items.map((item, index) => {
|
|
32
|
+
if (item.isExample) {
|
|
33
|
+
return { fragmentIds: item.fragmentIds, sentence: item.sentence };
|
|
34
|
+
}
|
|
35
|
+
const current = resolveAnswer(index);
|
|
36
|
+
if (index !== itemIndex)
|
|
37
|
+
return current;
|
|
38
|
+
const fragmentIds = [
|
|
39
|
+
item.stemFragmentId,
|
|
40
|
+
...choiceColumns.map((column, colIndex) => {
|
|
41
|
+
if (colIndex === choiceIndex)
|
|
42
|
+
return fragmentId;
|
|
43
|
+
return current.fragmentIds[colIndex + 1] || '';
|
|
44
|
+
}),
|
|
45
|
+
];
|
|
46
|
+
return {
|
|
47
|
+
fragmentIds,
|
|
48
|
+
sentence: assembleMatchColumnsSentence(fragmentIds, columns),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
onAnswerChange?.(next);
|
|
52
|
+
};
|
|
53
|
+
return (_jsxs("div", { className: "space-y-4", children: [(questionData.title || questionData.instruction) && (_jsxs("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3", children: [questionData.title && (_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-indigo-500", children: questionData.title })), questionData.instruction && (_jsx("p", { className: "text-sm font-medium text-indigo-800", children: questionData.instruction }))] })), _jsx("div", { className: "overflow-x-auto rounded-xl border border-gray-200 bg-white", children: _jsxs("table", { className: "min-w-full text-sm", children: [_jsx("thead", { children: _jsx("tr", { className: "bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500", children: columns.map((column) => (_jsxs("th", { className: "px-3 py-2 font-semibold", children: [column.label || column.id, column.role === 'stem' ? ' (stem)' : ''] }, column.id))) }) }), _jsx("tbody", { children: _jsx("tr", { className: "align-top", children: columns.map((column) => (_jsx("td", { className: "px-3 py-2", children: _jsx("ul", { className: "space-y-1", children: column.fragments.map((fragment) => (_jsxs("li", { className: "text-slate-700", children: [_jsxs("span", { className: "mr-1 font-semibold text-slate-500", children: [fragment.id, "."] }), fragment.text || '—'] }, fragment.id))) }) }, `${column.id}-bank`))) }) })] }) }), _jsx("div", { className: "space-y-3", children: items.map((item, itemIndex) => {
|
|
54
|
+
const current = resolveAnswer(itemIndex);
|
|
55
|
+
const canSelect = canEdit && !item.isExample;
|
|
56
|
+
const stemText = stemColumn?.fragments.find((fragment) => fragment.id === item.stemFragmentId)?.text
|
|
57
|
+
|| item.stemFragmentId
|
|
58
|
+
|| '—';
|
|
59
|
+
const isCorrect = isReviewMode &&
|
|
60
|
+
current.sentence.trim().toLowerCase() === item.sentence.trim().toLowerCase();
|
|
61
|
+
return (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-4 shadow-sm", children: [_jsxs("div", { className: "mb-3 flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "grid gap-3 md:grid-cols-[minmax(0,1fr)_repeat(2,minmax(0,1fr))]", children: [_jsx("div", { className: "rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm font-semibold text-slate-800", children: stemText }), choiceColumns.map((column, choiceIndex) => {
|
|
62
|
+
const selectedId = current.fragmentIds[choiceIndex + 1] || '';
|
|
63
|
+
return (_jsxs("select", { className: cn('rounded-lg border-2 px-3 py-2 text-sm', isReviewMode && selectedId === item.fragmentIds[choiceIndex + 1]
|
|
64
|
+
? 'border-green-400 bg-green-50'
|
|
65
|
+
: selectedId
|
|
66
|
+
? 'border-blue-400 bg-blue-50'
|
|
67
|
+
: 'border-gray-200 bg-white'), disabled: !canSelect, value: selectedId, onChange: (event) => handleChoiceChange(itemIndex, choiceIndex, event.target.value), children: [_jsxs("option", { value: "", children: ["Ch\u1ECDn ", column.label || column.id] }), column.fragments.map((fragment) => (_jsxs("option", { value: fragment.id, children: [fragment.id, ". ", fragment.text] }, fragment.id)))] }, `${itemIndex}-${column.id}`));
|
|
68
|
+
})] }), _jsx("p", { className: cn('mt-3 text-sm', isReviewMode
|
|
69
|
+
? isCorrect
|
|
70
|
+
? 'text-green-700'
|
|
71
|
+
: 'text-red-700'
|
|
72
|
+
: 'text-slate-600'), children: current.sentence || 'Chưa ghép câu' }), isReviewMode && !item.isExample && (_jsxs("p", { className: "mt-1 text-xs text-slate-500", children: ["\u0110\u00E1p \u00E1n: ", item.sentence] }))] }, `${item.questionNumber}-${itemIndex}`));
|
|
73
|
+
}) }), 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] }))] }));
|
|
74
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { MatchColumnsGroupCreatorProps } from '../../_shared/types/match-columns-to-make-sentences.type';
|
|
2
|
+
export declare function MatchColumnsToMakeSentencesCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }: MatchColumnsGroupCreatorProps): import("react").JSX.Element;
|