@tinyweb_dev/oe-exam-sdk 1.0.9 → 1.0.11
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 -7
- 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/question-renderers/MoversMatchWordToPictureGroupRenderer.d.ts +14 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureGroupRenderer.js +36 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +4 -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/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 +17 -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 +32 -3
- package/dist/components/exams/take/utils/question-transformers.js +51 -2
- 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/match-word-to-picture-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +14 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +2 -2
- package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +17 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -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 +24 -35
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +16 -3
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +2 -1
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +25 -6
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +1 -1
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +22 -10
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +60 -18
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +2 -1
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +24 -5
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +8 -4
- package/dist/components/questions/types/matching-with-lines-group/transform.js +5 -4
- 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 +7 -3
- package/dist/components/results/review-data.js +20 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +48 -7
- 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/dist/shared/types/questions/matching-with-lines-group.d.ts +2 -2
- package/package.json +1 -1
package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js
CHANGED
|
@@ -1,47 +1,34 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { BookOpen, Check,
|
|
3
|
+
import { BookOpen, Check, ChevronDown, Lightbulb, Volume2, X } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
|
-
import { toMatchWordToPictureImageUrls } from '../../_shared/types/match-word-to-picture-group.type';
|
|
6
|
+
import { toMatchWordToPictureImageUrls, toMatchWordToPictureStorageKey, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
7
|
+
import { KidInstruction, KidPrompt, KidQuestionCard, } from '../../_shared/components/compact';
|
|
7
8
|
function normalizeWord(value) {
|
|
8
9
|
return value.trim().toLowerCase();
|
|
9
10
|
}
|
|
10
|
-
function
|
|
11
|
+
function isPlayableUrl(value) {
|
|
11
12
|
return (value.startsWith('http://')
|
|
12
13
|
|| value.startsWith('https://')
|
|
13
14
|
|| value.startsWith('blob:')
|
|
14
15
|
|| value.startsWith('data:'));
|
|
15
16
|
}
|
|
16
|
-
function toPresignKey(value) {
|
|
17
|
-
const trimmed = value.trim();
|
|
18
|
-
if (isPlayableAudioUrl(trimmed)) {
|
|
19
|
-
return trimmed;
|
|
20
|
-
}
|
|
21
|
-
return trimmed.replace(/^\/+/, '');
|
|
22
|
-
}
|
|
23
17
|
function ClientAudio({ url }) {
|
|
24
|
-
const fileKey =
|
|
18
|
+
const fileKey = toMatchWordToPictureStorageKey(url);
|
|
25
19
|
const { previewUrl, isLoading } = usePresignedFileUrl(fileKey);
|
|
26
|
-
const src = previewUrl || (
|
|
20
|
+
const src = previewUrl || (isPlayableUrl(fileKey) ? fileKey : '');
|
|
27
21
|
if (isLoading) {
|
|
28
22
|
return (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx("div", { className: "h-8 w-full max-w-xs animate-pulse rounded-md bg-slate-100" })] }));
|
|
29
23
|
}
|
|
30
|
-
if (!src)
|
|
24
|
+
if (!src)
|
|
31
25
|
return null;
|
|
32
|
-
}
|
|
33
|
-
return (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx("audio", { controls: true, src: src, className: "h-8 max-w-full", preload: "metadata" })] }));
|
|
34
|
-
}
|
|
35
|
-
function isDisplayableImageUrl(value) {
|
|
36
|
-
return (value.startsWith('http://')
|
|
37
|
-
|| value.startsWith('https://')
|
|
38
|
-
|| value.startsWith('blob:')
|
|
39
|
-
|| value.startsWith('data:'));
|
|
26
|
+
return (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx("audio", { controls: true, src: src, className: "h-8 max-w-full", preload: "metadata", children: _jsx("track", { kind: "captions" }) })] }));
|
|
40
27
|
}
|
|
41
28
|
function ClientImageItem({ url, alt, large = false, }) {
|
|
42
|
-
const fileKey =
|
|
29
|
+
const fileKey = toMatchWordToPictureStorageKey(url);
|
|
43
30
|
const { previewUrl, isLoading } = usePresignedFileUrl(fileKey);
|
|
44
|
-
const src = previewUrl || (
|
|
31
|
+
const src = previewUrl || (isPlayableUrl(fileKey) ? fileKey : '');
|
|
45
32
|
if (!url) {
|
|
46
33
|
return (_jsx("div", { className: "flex h-28 w-28 items-center justify-center rounded-lg border border-dashed border-gray-300 bg-gray-50 text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 \u1EA3nh" }));
|
|
47
34
|
}
|
|
@@ -50,9 +37,16 @@ function ClientImageItem({ url, alt, large = false, }) {
|
|
|
50
37
|
}
|
|
51
38
|
return (_jsx("div", { className: cn('relative flex-shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-gray-50', large ? 'max-h-80 w-full max-w-xl' : 'h-28 w-28'), children: isLoading ? (_jsx("div", { className: "flex h-28 w-full items-center justify-center", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-violet-500" }) })) : (_jsx("img", { src: src, alt: alt || 'Picture', className: cn('object-contain', large ? 'mx-auto max-h-80 w-auto' : 'h-full w-full') })) }));
|
|
52
39
|
}
|
|
40
|
+
function WordDropdown({ value, wordBank, usedWords, disabled, onChange, }) {
|
|
41
|
+
return (_jsxs("div", { className: "group relative min-w-[160px] sm:min-w-[210px]", children: [_jsxs("select", { value: value, onChange: (event) => onChange(event.target.value), disabled: disabled, className: "h-12 w-full appearance-none rounded-2xl border border-slate-200 bg-white px-4 pr-11 text-sm font-semibold text-slate-700 shadow-sm outline-none transition-all hover:border-violet-300 focus:border-violet-500 focus:ring-4 focus:ring-violet-100/50 disabled:cursor-not-allowed disabled:opacity-60", "aria-label": "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p", children: [_jsx("option", { value: "", children: "-- Ch\u1ECDn t\u1EEB --" }), wordBank.map((word, index) => {
|
|
42
|
+
const isUsedByOther = usedWords.has(normalizeWord(word)) && normalizeWord(word) !== normalizeWord(value);
|
|
43
|
+
return (_jsx("option", { value: word, disabled: isUsedByOther, children: word }, `${word}-${index}`));
|
|
44
|
+
})] }), _jsx("div", { className: "pointer-events-none absolute right-2.5 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full bg-slate-100 text-slate-500 group-focus-within:bg-violet-100 group-focus-within:text-violet-600", children: _jsx(ChevronDown, { className: "h-3.5 w-3.5", strokeWidth: 2.5 }) })] }));
|
|
45
|
+
}
|
|
53
46
|
export function MatchWordToPictureGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
54
47
|
const items = questionData.items || [];
|
|
55
48
|
const wordBank = questionData.wordBank || [];
|
|
49
|
+
const coverUrls = toMatchWordToPictureImageUrls(questionData.imageUrl);
|
|
56
50
|
const handleSelect = (itemIndex, value) => {
|
|
57
51
|
const item = items[itemIndex];
|
|
58
52
|
if (isReviewMode || item?.isExample || !onAnswerChange) {
|
|
@@ -69,11 +63,11 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
|
|
|
69
63
|
usedWords.add(normalizeWord(value));
|
|
70
64
|
}
|
|
71
65
|
});
|
|
72
|
-
return (_jsxs("div", { className: "space-y-4", children: [
|
|
66
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.title ? _jsx(KidPrompt, { children: questionData.title }) : null, _jsx(KidInstruction, { children: questionData.instruction }), questionData.audioUrl ? _jsx(ClientAudio, { url: questionData.audioUrl }) : null, coverUrls.length > 0 ? (_jsx("div", { className: "flex flex-wrap justify-center gap-3 rounded-lg border border-slate-100 bg-slate-50 p-3", children: coverUrls.map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh bài (${imageIndex + 1})`, large: true }, `mwtp-cover-${imageIndex}`))) })) : null, 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 leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })) : null, wordBank.length > 0 && (_jsxs("div", { className: "rounded-xl border border-blue-100 bg-blue-50/60 p-3", children: [_jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wider text-blue-700", children: "Word bank" }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => {
|
|
73
67
|
const isUsed = usedWords.has(normalizeWord(word));
|
|
74
68
|
return (_jsx("span", { className: cn('rounded-full border px-3 py-1 text-sm font-semibold', isUsed
|
|
75
|
-
? 'border-
|
|
76
|
-
: 'border-
|
|
69
|
+
? 'border-blue-500 bg-blue-50 text-blue-800 ring-2 ring-blue-100'
|
|
70
|
+
: 'border-gray-200 bg-white text-slate-700'), children: word }, `${word}-${index}`));
|
|
77
71
|
}) })] })), items.map((item, itemIndex) => {
|
|
78
72
|
const selectedWord = item.isExample
|
|
79
73
|
? item.correctAnswer
|
|
@@ -81,14 +75,9 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
|
|
|
81
75
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
82
76
|
const isCorrect = normalizeWord(selectedWord) === normalizeWord(item.correctAnswer) &&
|
|
83
77
|
Boolean(item.correctAnswer);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
: 'border-violet-300 focus:border-violet-500 focus:ring-violet-200', !canSelect && 'cursor-default opacity-80'), children: [_jsx("option", { value: "", children: "-- Ch\u1ECDn t\u1EEB --" }), wordBank.map((word, wordIndex) => {
|
|
89
|
-
const isUsedByOther = usedWords.has(normalizeWord(word)) &&
|
|
90
|
-
normalizeWord(word) !== normalizeWord(selectedWord);
|
|
91
|
-
return (_jsx("option", { value: word, disabled: isUsedByOther, children: word }, `${word}-${wordIndex}`));
|
|
92
|
-
})] })), isReviewMode && !item.isExample && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(Check, { className: "h-5 w-5 text-green-500" })) : (_jsx(X, { className: "h-5 w-5 text-red-500" })) }))] }), isReviewMode && !item.isExample && !isCorrect && (_jsxs("p", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: item.correctAnswer || '—' })] }))] })] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
78
|
+
const itemImages = toMatchWordToPictureImageUrls(item.imageUrl);
|
|
79
|
+
return (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start", children: [itemImages.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-2", children: itemImages.map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}${imageIndex > 0 ? ` (${imageIndex + 1})` : ''}` }, `${item.questionNumber}-${imageIndex}`))) })) : null, _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), item.isExample ? (_jsx("span", { className: "text-sm italic text-gray-800 underline decoration-gray-400", children: item.correctAnswer })) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(WordDropdown, { value: selectedWord, wordBank: wordBank, usedWords: usedWords, disabled: !canSelect, onChange: (value) => handleSelect(itemIndex, value) }), isReviewMode && (isCorrect
|
|
80
|
+
? _jsx(Check, { className: "h-5 w-5 text-emerald-600" })
|
|
81
|
+
: _jsx(X, { className: "h-5 w-5 text-red-500" }))] })), isReviewMode && !item.isExample && !isCorrect && (_jsxs("p", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: item.correctAnswer || '—' })] }))] })] }) }, `${item.questionNumber}-${itemIndex}`));
|
|
93
82
|
}), 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] }))] }));
|
|
94
83
|
}
|
|
@@ -32,7 +32,7 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
32
32
|
const [title, setTitle] = useState(initialData?.title || '');
|
|
33
33
|
const [instruction, setInstruction] = useState(initialData?.instruction || seeded.instruction || MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION);
|
|
34
34
|
const [passage, setPassage] = useState(initialData?.passage || seeded.passage || '');
|
|
35
|
-
const [audioUrl] = useState(initialData?.audioUrl || seeded.audioUrl || '');
|
|
35
|
+
const [audioUrl, setAudioUrl] = useState(initialData?.audioUrl || seeded.audioUrl || '');
|
|
36
36
|
const [imageUrl, setImageUrl] = useState(toMatchWordToPictureImageUrls(initialData?.imageUrl)[0] || '');
|
|
37
37
|
const [wordBank, setWordBank] = useState(initialData?.wordBank?.length ? initialData.wordBank : seeded.wordBank);
|
|
38
38
|
const [items, setItems] = useState(() => (initialData?.items?.length ? initialData.items : seeded.items));
|
|
@@ -76,12 +76,25 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
76
76
|
nextErrors.push('Word bank cần ít nhất một từ.');
|
|
77
77
|
if (items.length < 1)
|
|
78
78
|
nextErrors.push('Cần ít nhất một câu hỏi.');
|
|
79
|
+
const wordSet = new Set(words.map((word) => word.trim().toLowerCase()));
|
|
80
|
+
const seenAnswers = new Set();
|
|
79
81
|
items.forEach((item, index) => {
|
|
80
82
|
if (!imageUrl.trim() && toMatchWordToPictureImageUrls(item.imageUrl).length === 0) {
|
|
81
83
|
nextErrors.push(`Câu ${index + 1}: cần ảnh (hoặc ảnh chung).`);
|
|
82
84
|
}
|
|
83
|
-
|
|
85
|
+
const answer = item.correctAnswer.trim();
|
|
86
|
+
if (!answer) {
|
|
84
87
|
nextErrors.push(`Câu ${index + 1}: cần chọn từ đúng.`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const normalized = answer.toLowerCase();
|
|
91
|
+
if (!wordSet.has(normalized)) {
|
|
92
|
+
nextErrors.push(`Câu ${index + 1}: đáp án không có trong word bank.`);
|
|
93
|
+
}
|
|
94
|
+
if (seenAnswers.has(normalized)) {
|
|
95
|
+
nextErrors.push(`Câu ${index + 1}: từ "${answer}" đã được dùng (mỗi từ chỉ một lần).`);
|
|
96
|
+
}
|
|
97
|
+
seenAnswers.add(normalized);
|
|
85
98
|
});
|
|
86
99
|
setErrors(nextErrors);
|
|
87
100
|
return nextErrors.length === 0;
|
|
@@ -102,7 +115,7 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
102
115
|
if (isClientMode) {
|
|
103
116
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(MatchWordToPictureGroupClient, { questionData: currentPayload })] }));
|
|
104
117
|
}
|
|
105
|
-
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => _jsx("li", { children: err }, i)) }) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(ImageIcon, { className: "h-3.5 w-3.5" }), title: "N\u1ED1i t\u1EEB v\u1EDBi tranh (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwtp-group-title", className: "text-sm font-semibold text-gray-800", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "mwtp-group-title", className: "h-10 border-gray-200 bg-white", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "VOCABULARY" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwtp-group-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "mwtp-group-instruction", className: "h-10 border-gray-200 bg-white", value: instruction, onChange: (e) => setInstruction(e.target.value) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage)" }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] }), _jsx(MatchWordToPictureGroupImageField, { id: "mwtp-group-
|
|
118
|
+
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => _jsx("li", { children: err }, i)) }) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(ImageIcon, { className: "h-3.5 w-3.5" }), title: "N\u1ED1i t\u1EEB v\u1EDBi tranh (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwtp-group-title", className: "text-sm font-semibold text-gray-800", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "mwtp-group-title", className: "h-10 border-gray-200 bg-white", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "VOCABULARY" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwtp-group-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "mwtp-group-instruction", className: "h-10 border-gray-200 bg-white", value: instruction, onChange: (e) => setInstruction(e.target.value) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage)" }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] }), _jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsx(MatchWordToPictureGroupImageField, { id: "mwtp-group-audio", label: "Audio chung (optional)", accept: "audio/*", value: audioUrl, onChange: setAudioUrl }), _jsx(MatchWordToPictureGroupImageField, { id: "mwtp-group-shared-image", label: "\u1EA2nh chung (optional)", value: imageUrl, onChange: setImageUrl })] }), wordBank.map((word, index) => (_jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { value: word, onChange: (e) => setWordBank((prev) => prev.map((item, i) => (i === index ? e.target.value : item))), placeholder: `Từ ${index + 1}`, className: "h-10 border-gray-200 bg-white" }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => setWordBank((prev) => prev.filter((_, i) => i !== index)), className: "h-7 w-7", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }, index))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setWordBank((prev) => [...prev, '']), className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm t\u1EEB"] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-sm", children: [_jsx(CardHeader, { className: "px-3 py-2", children: _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs(CardTitle, { className: "text-sm font-semibold text-gray-800", children: ["Tranh (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setItems((prev) => [...prev, createEmptyMatchWordToPictureGroupItem(prev.length + 1)]), className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm tranh"] })] }) }), _jsxs(CardContent, { className: "space-y-2.5 px-3 pb-3", children: [items.map((item, index) => (_jsxs("div", { className: "space-y-2.5 rounded-md border border-gray-200 bg-white px-3 py-2.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("span", { className: "text-sm font-medium", children: ["C\u00E2u ", item.questionNumber || index + 1] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { className: "flex items-center gap-1 text-xs", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "Example"] }), !item.isExample && (_jsx("div", { className: "w-24", children: _jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
|
|
106
119
|
points: parsePointsValue(event.target.value, true),
|
|
107
120
|
}) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => setItems((prev) => prev.filter((_, i) => i !== index).map((entry, i) => ({ ...entry, questionNumber: i + 1 }))), className: "h-7 w-7 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] })] }), _jsx(MatchWordToPictureGroupImageField, { id: `mwtp-group-image-${index}`, label: "\u1EA2nh", value: toMatchWordToPictureImageUrls(item.imageUrl)[0] || '', onChange: (url) => updateItem(index, {
|
|
108
121
|
imageUrl: fromMatchWordToPictureImageUrls(url),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export declare function MatchWordToPictureGroupImageField({ id, label, value, onChange, }: {
|
|
1
|
+
export declare function MatchWordToPictureGroupImageField({ id, label, value, onChange, accept, }: {
|
|
2
2
|
id: string;
|
|
3
3
|
label: string;
|
|
4
4
|
value: string;
|
|
5
5
|
onChange: (url: string) => void;
|
|
6
|
+
accept?: 'image/*' | 'audio/*';
|
|
6
7
|
}): import("react").JSX.Element;
|
|
@@ -6,11 +6,30 @@ import { FileUpload } from '../../../../components/ui/file-upload';
|
|
|
6
6
|
import { ImagePreview } from '../../../../components/ui/image-preview';
|
|
7
7
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
8
8
|
import { Trash2 } from 'lucide-react';
|
|
9
|
-
|
|
9
|
+
import { toMatchWordToPictureStorageKey } from '../../_shared/types/match-word-to-picture-group.type';
|
|
10
|
+
function isDisplayableUrl(value) {
|
|
11
|
+
return (value.startsWith('http://') ||
|
|
12
|
+
value.startsWith('https://') ||
|
|
13
|
+
value.startsWith('blob:') ||
|
|
14
|
+
value.startsWith('data:'));
|
|
15
|
+
}
|
|
16
|
+
function resolveDisplayUrl(fileUrl, previewOverride, fetchedPreviewUrl) {
|
|
17
|
+
if (previewOverride)
|
|
18
|
+
return previewOverride;
|
|
19
|
+
if (fetchedPreviewUrl)
|
|
20
|
+
return fetchedPreviewUrl;
|
|
21
|
+
if (isDisplayableUrl(fileUrl))
|
|
22
|
+
return fileUrl;
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
export function MatchWordToPictureGroupImageField({ id, label, value, onChange, accept = 'image/*', }) {
|
|
10
26
|
const [previewOverride, setPreviewOverride] = useState('');
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return (_jsxs("div", { className: "space-y-2", children: [_jsx(FileUpload, { id: id, label: label, accept:
|
|
27
|
+
const storageKey = toMatchWordToPictureStorageKey(value);
|
|
28
|
+
const { previewUrl: fetchedPreviewUrl, isLoading } = usePresignedFileUrl(storageKey);
|
|
29
|
+
const displayUrl = resolveDisplayUrl(value, previewOverride, fetchedPreviewUrl);
|
|
30
|
+
const isImage = accept === 'image/*';
|
|
31
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(FileUpload, { id: id, label: label, accept: accept, value: value, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: isImage ? 5 : 20, placeholder: isImage ? 'Upload ảnh hoặc paste URL' : 'Upload audio hoặc paste URL', autoUpload: true, prefix: "questions" }), value && isImage && (_jsxs("div", { className: "flex items-start gap-2", children: [isLoading && !displayUrl ? (_jsx("div", { className: "h-20 w-20 animate-pulse rounded-md bg-slate-100" })) : (_jsx(ImagePreview, { src: displayUrl || value, alt: label, className: "h-20 w-20 rounded-md object-contain" })), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
32
|
+
setPreviewOverride('');
|
|
33
|
+
onChange('');
|
|
34
|
+
}, className: "text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-4 w-4" }) })] })), value && !isImage && (isLoading && !displayUrl ? (_jsx("div", { className: "h-8 w-48 animate-pulse rounded-md bg-slate-100" })) : (_jsx("audio", { controls: true, src: displayUrl, className: "h-8 max-w-full", children: _jsx("track", { kind: "captions" }) })))] }));
|
|
16
35
|
}
|
|
@@ -104,6 +104,6 @@ export function MatchingWithLinesBoard({ leftItems, rightItems, values, correctV
|
|
|
104
104
|
const used = values.includes(item.id);
|
|
105
105
|
return (_jsxs("button", { type: "button", onClick: () => handleRightClick(item.id), disabled: locked, "aria-label": `Connect right item ${item.text || index + 1}`, className: cn('box-border flex h-auto min-h-14 w-max items-center gap-2 rounded-lg border border-gray-200 bg-white px-2 py-1 text-left shadow-sm', selectedLeft !== null && !locked && 'cursor-pointer hover:border-teal-400', used && 'border-teal-200'), children: [_jsx("span", { ref: (node) => {
|
|
106
106
|
rightDotRefs.current[index] = node;
|
|
107
|
-
}, className: cn('h-4 w-4 shrink-0 rounded-full border-2 bg-white', used || selectedLeft !== null ? 'border-teal-600' : 'border-gray-400', selectedLeft !== null && 'bg-teal-100'), "aria-hidden": true }), item.imageUrl ? (_jsx("div", { className: "relative size-11 shrink-0 overflow-hidden rounded-md bg-gray-50", children: _jsx(ResolvedImage, { src: item.imageUrl, alt: item.text || `Match option ${index + 1}`, className: "h-full w-full object-contain" }) })) : (_jsx("span", { className: "whitespace-nowrap text-sm font-medium text-gray-800", children: item.text
|
|
107
|
+
}, className: cn('h-4 w-4 shrink-0 rounded-full border-2 bg-white', used || selectedLeft !== null ? 'border-teal-600' : 'border-gray-400', selectedLeft !== null && 'bg-teal-100'), "aria-hidden": true }), item.imageUrl ? (_jsx("div", { className: "relative size-11 shrink-0 overflow-hidden rounded-md bg-gray-50", children: _jsx(ResolvedImage, { src: item.imageUrl, alt: item.text || `Match option ${index + 1}`, className: "h-full w-full object-contain" }) })) : null, item.text?.trim() ? (_jsx("span", { className: "whitespace-nowrap text-sm font-medium text-gray-800", children: item.text })) : !item.imageUrl ? (_jsx("span", { className: "whitespace-nowrap text-sm font-medium text-gray-800", children: "\u2014" })) : null] }, item.id || `right-${index}`));
|
|
108
108
|
}) })] })] }));
|
|
109
109
|
}
|
package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { BookOpen, Lightbulb } from 'lucide-react';
|
|
3
|
+
import { BookOpen, Lightbulb, Volume2 } from 'lucide-react';
|
|
4
4
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
5
|
-
import {
|
|
5
|
+
import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
|
|
6
6
|
import { MatchingWithLinesBoard } from './MatchingWithLinesBoard';
|
|
7
|
+
import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
8
|
+
import { toChooseAnswerGroupImageUrls } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
9
|
+
function ClientImageItem({ url, alt }) {
|
|
10
|
+
const { previewUrl, isLoading } = usePresignedFileUrl(url);
|
|
11
|
+
const src = previewUrl ||
|
|
12
|
+
(typeof url === 'string' && (url.startsWith('http') || url.startsWith('blob:')) ? url : '');
|
|
13
|
+
if (!src && !isLoading) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return (_jsx("div", { className: "max-w-sm overflow-hidden rounded-lg border border-gray-200 bg-gray-50", children: isLoading ? (_jsx("div", { className: "flex h-36 w-48 items-center justify-center", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-teal-500" }) })) : (_jsx("img", { src: src, alt: alt || 'Image', className: "max-h-56 w-auto object-contain p-2" })) }));
|
|
17
|
+
}
|
|
7
18
|
function ClientAudio({ url }) {
|
|
8
19
|
const { previewUrl, isLoading } = usePresignedFileUrl(url);
|
|
9
20
|
const src = previewUrl ||
|
|
10
21
|
(typeof url === 'string' && (url.startsWith('http') || url.startsWith('blob:')) ? url : '');
|
|
11
|
-
if (!src && !isLoading)
|
|
22
|
+
if (!src && !isLoading) {
|
|
12
23
|
return null;
|
|
13
|
-
|
|
24
|
+
}
|
|
25
|
+
return (_jsxs("div", { className: "flex items-center gap-3 rounded-xl border border-indigo-100 bg-indigo-50/70 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 shrink-0 text-indigo-600" }), isLoading ? (_jsx("div", { className: "h-8 flex-1 animate-pulse rounded-md bg-indigo-100" })) : (_jsx("audio", { controls: true, className: "h-9 w-full", src: src, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." }))] }));
|
|
14
26
|
}
|
|
15
27
|
export function MatchingWithLinesGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, onAnswersChange, }) {
|
|
16
28
|
const items = questionData.items || [];
|
|
@@ -32,10 +44,10 @@ export function MatchingWithLinesGroupClient({ questionData, isReviewMode = fals
|
|
|
32
44
|
}
|
|
33
45
|
});
|
|
34
46
|
};
|
|
35
|
-
return (_jsxs("div", { className: "space-y-4", children: [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.title ? _jsx(KidPrompt, { children: questionData.title }) : null, _jsx(KidInstruction, { children: questionData.instruction }), toChooseAnswerGroupImageUrls(questionData.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `group-audio-${audioIndex}`))), 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: "overflow-x-auto", children: _jsx(RichTextHtml, { html: questionData.passage, className: "text-gray-800 leading-relaxed" }) })] })) : null, _jsx(KidQuestionCard, { title: "N\u1ED1i b\u1EB1ng \u0111\u01B0\u1EDDng k\u1EBB", children: _jsx(MatchingWithLinesBoard, { leftItems: items.map((item) => ({
|
|
48
|
+
imageUrl: item.imageUrl,
|
|
49
|
+
text: item.text,
|
|
50
|
+
isExample: item.isExample,
|
|
51
|
+
questionNumber: item.questionNumber,
|
|
52
|
+
})), rightItems: rightItems, values: values, correctValues: isReviewMode ? correctValues : undefined, onChange: handleChange, isReviewMode: isReviewMode, disabled: isReviewMode || !canEdit }) }), 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] }))] }));
|
|
41
53
|
}
|
package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js
CHANGED
|
@@ -4,17 +4,28 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
5
|
import { Label } from '../../../../components/ui/label';
|
|
6
6
|
import { Input } from '../../../../components/ui/input';
|
|
7
|
+
import { Textarea } from '../../../../components/ui/textarea';
|
|
7
8
|
import { PointsInput } from '../../../../components/ui/points-input';
|
|
8
9
|
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
9
10
|
import { Switch } from '../../../../components/ui/switch';
|
|
10
11
|
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
11
|
-
import { Link2, Plus, Star, Trash2 } from 'lucide-react';
|
|
12
|
+
import { BookOpen, ImageIcon, Link2, Plus, Star, Trash2, Volume2 } from 'lucide-react';
|
|
13
|
+
import { ChooseAnswerGroupAudioUploadItem, ChooseAnswerGroupImageUploadItem, } from '../choose-the-correct-answer-group/ChooseAnswerGroupImageUpload';
|
|
14
|
+
import { fromChooseAnswerGroupImageUrls, toEditableChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
12
15
|
import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
|
|
13
16
|
import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
14
17
|
import { MatchingWithLinesGroupClient } from './MatchingWithLinesGroupClient';
|
|
15
18
|
import { MatchingWithLinesImageField } from './MatchingWithLinesImageField';
|
|
16
19
|
import { createEmptyMatchingPair, nextRightId } from './map-matching-with-lines-group-data';
|
|
17
20
|
import { MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/matching-with-lines-group.type';
|
|
21
|
+
function rightOptionLabel(item) {
|
|
22
|
+
const text = item.text?.trim();
|
|
23
|
+
if (text)
|
|
24
|
+
return `${text} (${item.id})`;
|
|
25
|
+
if (item.imageUrl?.trim())
|
|
26
|
+
return `Ảnh (${item.id})`;
|
|
27
|
+
return item.id;
|
|
28
|
+
}
|
|
18
29
|
function parsePointsValue(raw, allowZero = false) {
|
|
19
30
|
if (raw === '')
|
|
20
31
|
return allowZero ? 0 : 1;
|
|
@@ -66,27 +77,36 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
66
77
|
const [rightItems, setRightItems] = useState(() => normalizeRightItems(initialData?.rightItems));
|
|
67
78
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items, normalizeRightItems(initialData?.rightItems)));
|
|
68
79
|
const [passage, setPassage] = useState(initialData?.passage || '');
|
|
69
|
-
const [
|
|
70
|
-
const [
|
|
80
|
+
const [imageUrls, setImageUrls] = useState(() => (toEditableChooseAnswerGroupImageUrls(initialData?.imageUrl)));
|
|
81
|
+
const [audioUrls, setAudioUrls] = useState(() => (toEditableChooseAnswerGroupImageUrls(initialData?.audioUrl)));
|
|
71
82
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
72
83
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
84
|
+
const [isPassageVisible, setIsPassageVisible] = useState(() => {
|
|
85
|
+
const html = initialData?.passage;
|
|
86
|
+
return Boolean(html && html !== '<p></p>' && html !== '<p><br></p>' && html.trim());
|
|
87
|
+
});
|
|
73
88
|
const [errors, setErrors] = useState([]);
|
|
74
89
|
const onChangeRef = useRef(onChange);
|
|
75
90
|
const previousPayloadRef = useRef('');
|
|
76
91
|
useEffect(() => {
|
|
77
92
|
onChangeRef.current = onChange;
|
|
78
93
|
}, [onChange]);
|
|
79
|
-
const buildPayload = (nextTitle = title, nextInstruction = instruction, nextPassage = passage, nextRightItems = rightItems, nextItems = items,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
const buildPayload = (nextTitle = title, nextInstruction = instruction, nextPassage = passage, nextRightItems = rightItems, nextItems = items, nextAudioUrls = audioUrls, nextImageUrls = imageUrls, nextExplanation = explanation) => {
|
|
95
|
+
const nextAudioUrl = fromChooseAnswerGroupImageUrls(nextAudioUrls);
|
|
96
|
+
const nextImageUrl = fromChooseAnswerGroupImageUrls(nextImageUrls);
|
|
97
|
+
const isContentEmpty = (html) => !html || html === '<p></p>' || html === '<p><br></p>' || html.trim() === '';
|
|
98
|
+
return {
|
|
99
|
+
title: nextTitle,
|
|
100
|
+
instruction: nextInstruction,
|
|
101
|
+
...(!isContentEmpty(nextPassage) ? { passage: nextPassage.trim() } : {}),
|
|
102
|
+
...(nextAudioUrl !== undefined ? { audioUrl: nextAudioUrl } : {}),
|
|
103
|
+
...(nextImageUrl !== undefined ? { imageUrl: nextImageUrl } : {}),
|
|
104
|
+
rightItems: nextRightItems,
|
|
105
|
+
items: nextItems,
|
|
106
|
+
explanation: nextExplanation,
|
|
107
|
+
points: sumPoints(nextItems),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
90
110
|
const debouncedOnChange = useDebouncedCallback((payload) => {
|
|
91
111
|
onChangeRef.current?.(payload);
|
|
92
112
|
}, 300);
|
|
@@ -98,7 +118,7 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
98
118
|
debouncedOnChange(payload);
|
|
99
119
|
onUnsavedChangesChange?.(true);
|
|
100
120
|
}
|
|
101
|
-
}, [title, instruction, passage,
|
|
121
|
+
}, [title, instruction, passage, audioUrls, imageUrls, rightItems, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
102
122
|
const validate = () => {
|
|
103
123
|
const nextErrors = [];
|
|
104
124
|
if (!instruction.trim()) {
|
|
@@ -144,7 +164,7 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
144
164
|
getFormData: () => buildPayload(),
|
|
145
165
|
};
|
|
146
166
|
}
|
|
147
|
-
}, [validationRef, title, instruction, passage,
|
|
167
|
+
}, [validationRef, title, instruction, passage, audioUrls, imageUrls, rightItems, items, explanation]);
|
|
148
168
|
const updateItem = (index, patch) => {
|
|
149
169
|
setItems((prev) => {
|
|
150
170
|
const next = [...prev];
|
|
@@ -189,7 +209,29 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
189
209
|
if (isClientMode) {
|
|
190
210
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(MatchingWithLinesGroupClient, { questionData: currentPayload, userAnswers: currentPayload.items.map((item) => item.correctAnswer) })] }));
|
|
191
211
|
}
|
|
192
|
-
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(Link2, { className: "h-3.5 w-3.5" }), title: "N\u1ED1i b\u1EB1ng \u0111\u01B0\u1EDDng k\u1EBB (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwl-title", className: "text-sm font-semibold text-gray-800", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "mwl-title", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "VOCABULARY", className: "h-10 border-gray-200 bg-white" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwl-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }),
|
|
212
|
+
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(Link2, { className: "h-3.5 w-3.5" }), title: "N\u1ED1i b\u1EB1ng \u0111\u01B0\u1EDDng k\u1EBB (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwl-title", className: "text-sm font-semibold text-gray-800", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "mwl-title", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "VOCABULARY", className: "h-10 border-gray-200 bg-white" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwl-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsxs("div", { className: "relative", children: [_jsx(Textarea, { id: "mwl-instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION, rows: 2, className: "min-h-[56px] border-gray-200 pb-11" }), _jsxs("div", { className: "absolute bottom-1.5 right-1.5 z-10 flex items-center gap-1 rounded-md border border-indigo-100 bg-white/95 p-0.5 shadow-sm", children: [_jsx("button", { type: "button", title: "Th\u00EAm h\u00ECnh \u1EA3nh", onClick: () => {
|
|
213
|
+
const hasEmpty = imageUrls.some((url) => !url.trim());
|
|
214
|
+
if (imageUrls.length === 0 || !hasEmpty) {
|
|
215
|
+
setImageUrls([...imageUrls, '']);
|
|
216
|
+
}
|
|
217
|
+
}, className: `inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${imageUrls.length > 0
|
|
218
|
+
? 'bg-indigo-600 text-white shadow-sm'
|
|
219
|
+
: 'bg-indigo-50 text-indigo-600 hover:bg-indigo-100'}`, children: _jsx(ImageIcon, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", title: "Th\u00EAm audio", onClick: () => {
|
|
220
|
+
const hasEmpty = audioUrls.some((url) => !url.trim());
|
|
221
|
+
if (audioUrls.length === 0 || !hasEmpty) {
|
|
222
|
+
setAudioUrls([...audioUrls, '']);
|
|
223
|
+
}
|
|
224
|
+
}, className: `inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${audioUrls.length > 0
|
|
225
|
+
? 'bg-violet-600 text-white shadow-sm'
|
|
226
|
+
: 'bg-violet-50 text-violet-600 hover:bg-violet-100'}`, children: _jsx(Volume2, { className: "h-4 w-4" }) })] })] })] }), imageUrls.length > 0 && (_jsxs("div", { className: "space-y-2 rounded-md border border-gray-200 bg-slate-50/80 p-2", children: [imageUrls.map((url, imageIndex) => (_jsx(ChooseAnswerGroupImageUploadItem, { index: imageIndex, itemIndex: -1, imageUrl: url, onChange: (nextUrl) => {
|
|
227
|
+
setImageUrls((current) => current.map((item, idx) => (idx === imageIndex ? nextUrl : item)));
|
|
228
|
+
}, onRemove: () => {
|
|
229
|
+
setImageUrls((current) => current.filter((_, idx) => idx !== imageIndex));
|
|
230
|
+
} }, `mwl-group-image-${imageIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setImageUrls((current) => (current.length ? [...current, ''] : [''])), className: "h-8 gap-1.5 border-dashed border-indigo-300 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm \u1EA3nh"] })] })), audioUrls.length > 0 && (_jsxs("div", { className: "space-y-2 rounded-md border border-gray-200 bg-slate-50/80 p-2", children: [audioUrls.map((url, audioIndex) => (_jsx(ChooseAnswerGroupAudioUploadItem, { index: audioIndex, itemIndex: -1, audioUrl: url, onChange: (nextUrl) => {
|
|
231
|
+
setAudioUrls((current) => current.map((item, idx) => (idx === audioIndex ? nextUrl : item)));
|
|
232
|
+
}, onRemove: () => {
|
|
233
|
+
setAudioUrls((current) => current.filter((_, idx) => idx !== audioIndex));
|
|
234
|
+
} }, `mwl-group-audio-${audioIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setAudioUrls((current) => (current.length ? [...current, ''] : [''])), className: "h-8 gap-1.5 border-dashed border-violet-300 text-violet-600 hover:bg-violet-50 hover:text-violet-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm audio"] })] })), !isPassageVisible ? (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsPassageVisible(true), className: "h-8 gap-1.5 border-gray-200 px-2.5 text-xs text-gray-600 hover:bg-gray-50", children: [_jsx(BookOpen, { className: "h-3.5 w-3.5" }), "Th\u00EAm n\u1ED9i dung b\u00E0i \u0111\u1ECDc"] })) : (_jsxs("div", { className: "relative space-y-1.5 pr-10", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsPassageVisible(false), className: "absolute right-0 top-0 z-10 h-7 px-2 text-xs text-gray-500 hover:text-gray-700", children: "\u1EA8n" }), _jsxs(Label, { className: "text-sm font-semibold text-gray-800", children: ["N\u1ED9i dung b\u00E0i \u0111\u1ECDc ", _jsx("span", { className: "text-xs font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, variant: "compact", minHeightClassName: "min-h-[72px]" })] }))] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-sm", children: [_jsx(CardHeader, { className: "px-3 py-2", children: _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs(CardTitle, { className: "text-sm font-semibold text-gray-800", children: ["C\u1EB7p n\u1ED1i (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addPair, className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm c\u1EB7p"] })] }) }), _jsxs(CardContent, { className: "space-y-2.5 px-3 pb-3", children: [items.map((item, index) => (_jsxs("div", { className: "space-y-2.5 rounded-md border border-gray-200 bg-white px-3 py-2.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u1EB7p ", index + 1] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `mwl-example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { htmlFor: `mwl-example-${index}`, className: "flex cursor-pointer items-center gap-1 text-xs", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "Example"] })] }), !item.isExample && (_jsx("div", { className: "w-24", children: _jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
|
|
193
235
|
points: parsePointsValue(event.target.value, true),
|
|
194
|
-
}) }) })), items.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removePair(index), className: "h-7 w-7 text-red-500 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] })] }), _jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsxs("div", { className: "space-y-2.5", children: [_jsx(MatchingWithLinesImageField, { id: `mwl-image-${index}`, label: "\u1EA2nh c\u1ED9t tr\u00E1i", value: item.imageUrl || '', onChange: (url) => updateItem(index, { imageUrl: url }) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-left-text-${index}`, children: "Ch\u1EEF c\u1ED9t tr\u00E1i (text-to-text)" }), _jsx(Input, { id: `mwl-left-text-${index}`, value: item.text || '', onChange: (e) => updateItem(index, { text: e.target.value }), placeholder: "ball", className: "mt-1 bg-white" })] })] }), _jsxs("div", { className: "space-y-2.5", children: [_jsx(MatchingWithLinesImageField, { id: `mwl-right-image-${index}`, label: "\u1EA2nh c\u1ED9t ph\u1EA3i (image-to-image)", value: rightItems[index]?.imageUrl || '', onChange: (url) => updateRightItem(index, { imageUrl: url || undefined }) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-right-text-${index}`, children: "T\u1EEB c\u1ED9t ph\u1EA3i" }), _jsx(Input, { id: `mwl-right-text-${index}`, value: rightItems[index]?.text || '', onChange: (e) => updateRightItem(index, { text: e.target.value }), placeholder: "paintbrush", className: "mt-1 bg-white" })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-answer-${index}`, children: "\u0110\u00E1p \u00E1n \u0111\u00FAng (id t\u1EEB)" }), _jsxs("select", { id: `mwl-answer-${index}`, value: item.correctAnswer, onChange: (e) => updateItem(index, { correctAnswer: e.target.value }), className: "mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm", children: [_jsx("option", { value: "", children: "Ch\u1ECDn t\u1EEB \u0111\u00FAng" }), rightItems.map((right) => (
|
|
236
|
+
}) }) })), items.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removePair(index), className: "h-7 w-7 text-red-500 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] })] }), _jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsxs("div", { className: "space-y-2.5", children: [_jsx(MatchingWithLinesImageField, { id: `mwl-image-${index}`, label: "\u1EA2nh c\u1ED9t tr\u00E1i", value: item.imageUrl || '', onChange: (url) => updateItem(index, { imageUrl: url }) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-left-text-${index}`, children: "Ch\u1EEF c\u1ED9t tr\u00E1i (text-to-text)" }), _jsx(Input, { id: `mwl-left-text-${index}`, value: item.text || '', onChange: (e) => updateItem(index, { text: e.target.value }), placeholder: "ball", className: "mt-1 bg-white" })] })] }), _jsxs("div", { className: "space-y-2.5", children: [_jsx(MatchingWithLinesImageField, { id: `mwl-right-image-${index}`, label: "\u1EA2nh c\u1ED9t ph\u1EA3i (image-to-image)", value: rightItems[index]?.imageUrl || '', onChange: (url) => updateRightItem(index, { imageUrl: url || undefined }) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-right-text-${index}`, children: "T\u1EEB c\u1ED9t ph\u1EA3i" }), _jsx(Input, { id: `mwl-right-text-${index}`, value: rightItems[index]?.text || '', onChange: (e) => updateRightItem(index, { text: e.target.value }), placeholder: "paintbrush", className: "mt-1 bg-white" })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-answer-${index}`, children: "\u0110\u00E1p \u00E1n \u0111\u00FAng (id t\u1EEB)" }), _jsxs("select", { id: `mwl-answer-${index}`, value: item.correctAnswer, onChange: (e) => updateItem(index, { correctAnswer: e.target.value }), className: "mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm", children: [_jsx("option", { value: "", children: "Ch\u1ECDn t\u1EEB \u0111\u00FAng" }), rightItems.map((right) => (_jsx("option", { value: right.id, children: rightOptionLabel(right) }, right.id)))] })] })] })] })] }, `${rightItems[index]?.id || index}`))), _jsx(CompactExplanationToggle, { value: explanation, onValueChange: setExplanation, defaultVisible: Boolean(initialData?.explanation) })] })] })] }));
|
|
195
237
|
}
|
package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export declare function MatchingWithLinesImageField({ id, label, value, onChange, }: {
|
|
1
|
+
export declare function MatchingWithLinesImageField({ id, label, value, onChange, accept, }: {
|
|
2
2
|
id: string;
|
|
3
3
|
label: string;
|
|
4
4
|
value: string;
|
|
5
5
|
onChange: (url: string) => void;
|
|
6
|
+
accept?: 'image/*' | 'audio/*';
|
|
6
7
|
}): import("react").JSX.Element;
|
package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js
CHANGED
|
@@ -6,21 +6,40 @@ import { FileUpload } from '../../../../components/ui/file-upload';
|
|
|
6
6
|
import { ImagePreview } from '../../../../components/ui/image-preview';
|
|
7
7
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
8
8
|
import { Trash2 } from 'lucide-react';
|
|
9
|
+
function isDisplayableUrl(value) {
|
|
10
|
+
return (value.startsWith('http://') ||
|
|
11
|
+
value.startsWith('https://') ||
|
|
12
|
+
value.startsWith('blob:') ||
|
|
13
|
+
value.startsWith('data:'));
|
|
14
|
+
}
|
|
15
|
+
function toStorageKey(value) {
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
if (!trimmed)
|
|
18
|
+
return '';
|
|
19
|
+
if (isDisplayableUrl(trimmed))
|
|
20
|
+
return trimmed;
|
|
21
|
+
if (trimmed.startsWith('/images/') || trimmed.startsWith('/_next/')) {
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
return trimmed.replace(/^\/+/, '');
|
|
25
|
+
}
|
|
9
26
|
function resolveDisplayUrl(fileUrl, previewOverride, fetchedPreviewUrl) {
|
|
10
27
|
if (previewOverride)
|
|
11
28
|
return previewOverride;
|
|
12
29
|
if (fetchedPreviewUrl)
|
|
13
30
|
return fetchedPreviewUrl;
|
|
14
|
-
if (
|
|
31
|
+
if (isDisplayableUrl(fileUrl))
|
|
15
32
|
return fileUrl;
|
|
16
33
|
return '';
|
|
17
34
|
}
|
|
18
|
-
export function MatchingWithLinesImageField({ id, label, value, onChange, }) {
|
|
35
|
+
export function MatchingWithLinesImageField({ id, label, value, onChange, accept = 'image/*', }) {
|
|
19
36
|
const [previewOverride, setPreviewOverride] = useState('');
|
|
20
|
-
const
|
|
37
|
+
const storageKey = toStorageKey(value);
|
|
38
|
+
const { previewUrl: fetchedPreviewUrl, isLoading } = usePresignedFileUrl(storageKey);
|
|
21
39
|
const displayUrl = resolveDisplayUrl(value, previewOverride, fetchedPreviewUrl);
|
|
22
|
-
|
|
40
|
+
const isImage = accept === 'image/*';
|
|
41
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(FileUpload, { id: id, label: label, accept: accept, value: value, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: isImage ? 5 : 20, placeholder: isImage ? 'Upload ảnh hoặc paste URL' : 'Upload audio hoặc paste URL', autoUpload: true, prefix: "questions" }), value && isImage && (_jsxs("div", { className: "flex items-start gap-2", children: [isLoading && !displayUrl ? (_jsx("div", { className: "h-20 w-20 animate-pulse rounded-md bg-slate-100" })) : (_jsx(ImagePreview, { src: displayUrl || value, alt: label, className: "h-20 w-20 rounded-md object-contain" })), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
23
42
|
setPreviewOverride('');
|
|
24
43
|
onChange('');
|
|
25
|
-
}, className: "text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }))] }));
|
|
44
|
+
}, className: "text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-4 w-4" }) })] })), value && !isImage && (isLoading && !displayUrl ? (_jsx("div", { className: "h-8 w-48 animate-pulse rounded-md bg-slate-100" })) : (_jsx("audio", { controls: true, src: displayUrl, className: "h-8 max-w-full", children: _jsx("track", { kind: "captions" }) })))] }));
|
|
26
45
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/matching-with-lines-group.type';
|
|
2
|
+
import { fromChooseAnswerGroupImageUrls, toChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
3
|
+
function readSharedMedia(value) {
|
|
4
|
+
return fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(value));
|
|
5
|
+
}
|
|
2
6
|
function isRecord(value) {
|
|
3
7
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
8
|
}
|
|
@@ -72,9 +76,9 @@ export function mapQuestionToMatchingWithLinesGroupData(question) {
|
|
|
72
76
|
const meta = asRecord(content.meta ?? answer.meta);
|
|
73
77
|
if (Array.isArray(content.items)) {
|
|
74
78
|
const items = content.items.map((item, index) => mapMatchingWithLinesGroupItem(item, index, fallbackAnswers[index]));
|
|
75
|
-
const audioUrl =
|
|
79
|
+
const audioUrl = readSharedMedia(meta.audioUrl) ?? readSharedMedia(answer.audioUrl);
|
|
76
80
|
const passage = asString(meta.passage).trim() || asString(answer.passage).trim();
|
|
77
|
-
const imageUrl =
|
|
81
|
+
const imageUrl = readSharedMedia(meta.imageUrl) ?? readSharedMedia(answer.imageUrl);
|
|
78
82
|
return {
|
|
79
83
|
title: asString(meta.title) || asString(answer.title),
|
|
80
84
|
instruction: asString(meta.instruction) ||
|
|
@@ -91,9 +95,9 @@ export function mapQuestionToMatchingWithLinesGroupData(question) {
|
|
|
91
95
|
}
|
|
92
96
|
if (Array.isArray(answer.items)) {
|
|
93
97
|
const items = answer.items.map((item, index) => mapMatchingWithLinesGroupItem(item, index, fallbackAnswers[index]));
|
|
94
|
-
const audioUrl =
|
|
98
|
+
const audioUrl = readSharedMedia(answer.audioUrl);
|
|
95
99
|
const passage = asString(answer.passage).trim();
|
|
96
|
-
const imageUrl =
|
|
100
|
+
const imageUrl = readSharedMedia(answer.imageUrl);
|
|
97
101
|
return {
|
|
98
102
|
title: asString(answer.title),
|
|
99
103
|
instruction: asString(answer.instruction) || MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
|