@tinyweb_dev/oe-exam-sdk 0.2.13 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +13 -3
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.d.ts +14 -0
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +32 -0
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.d.ts +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +10 -2
- package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.js +23 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/index.js +2 -0
- package/dist/components/exams/take/utils/answer-transformers.js +34 -1
- package/dist/components/exams/take/utils/question-transformers.d.ts +42 -3
- package/dist/components/exams/take/utils/question-transformers.js +71 -1
- package/dist/components/questions/_shared/config/question-types.config.js +12 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +5 -1
- package/dist/components/questions/_shared/types/answer-the-question-group.type.js +7 -0
- package/dist/components/questions/_shared/types/answer-the-question.type.d.ts +3 -1
- package/dist/components/questions/_shared/types/answer-the-question.type.js +19 -1
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +47 -0
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.js +2 -0
- package/dist/components/questions/_shared/types/index.d.ts +2 -0
- package/dist/components/questions/_shared/types/index.js +2 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +9 -3
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +19 -0
- package/dist/components/questions/_shared/types/sort-words-into-categories.type.d.ts +43 -0
- package/dist/components/questions/_shared/types/sort-words-into-categories.type.js +1 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +2 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +2 -0
- package/dist/components/questions/creator/question-type-registry.js +4 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +30 -6
- package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.js +56 -0
- package/dist/components/questions/types/answer-the-question/transform.js +9 -3
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +42 -9
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +7 -5
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +18 -4
- package/dist/components/questions/types/answer-the-question-group/transform.js +8 -2
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +2 -0
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +34 -0
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +174 -0
- package/dist/components/questions/types/choose-then-answer-group/index.d.ts +5 -0
- package/dist/components/questions/types/choose-then-answer-group/index.js +5 -0
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +11 -0
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +127 -0
- package/dist/components/questions/types/choose-then-answer-group/register.d.ts +3 -0
- package/dist/components/questions/types/choose-then-answer-group/register.js +37 -0
- package/dist/components/questions/types/choose-then-answer-group/transform.d.ts +2 -0
- package/dist/components/questions/types/choose-then-answer-group/transform.js +37 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +39 -10
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +15 -8
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +8 -3
- package/dist/components/questions/types/match-word-to-picture-group/transform.js +21 -13
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.d.ts +12 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.js +74 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.d.ts +2 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +24 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.d.ts +2 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.js +171 -0
- package/dist/components/questions/types/sort-words-into-categories/index.d.ts +4 -0
- package/dist/components/questions/types/sort-words-into-categories/index.js +4 -0
- package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.d.ts +11 -0
- package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.js +153 -0
- package/dist/components/questions/types/sort-words-into-categories/register.d.ts +3 -0
- package/dist/components/questions/types/sort-words-into-categories/register.js +37 -0
- package/dist/components/questions/types/sort-words-into-categories/transform.d.ts +2 -0
- package/dist/components/questions/types/sort-words-into-categories/transform.js +33 -0
- package/dist/components/questions/viewer/AnswerTheQuestionViewer.d.ts +9 -0
- package/dist/components/questions/viewer/AnswerTheQuestionViewer.js +8 -0
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +10 -0
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +39 -0
- package/dist/components/questions/viewer/QuestionViewer.js +13 -2
- package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.d.ts +10 -0
- package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.js +27 -0
- package/dist/components/questions/viewer/index.d.ts +3 -0
- package/dist/components/questions/viewer/index.js +3 -0
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +2 -1
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +12 -3
- package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
- package/dist/components/results/renderers/review-question-body-movers.js +24 -2
- package/dist/components/results/review-data.js +28 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +4 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +98 -2
- package/dist/shared/lib/utils/question-transform-types.d.ts +1 -1
- package/dist/shared/lib/utils/question-transform.js +4 -0
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/answer-the-question-group.d.ts +3 -0
- package/dist/shared/types/questions/choose-then-answer-group.d.ts +46 -0
- package/dist/shared/types/questions/choose-then-answer-group.js +1 -0
- package/dist/shared/types/questions/index.d.ts +2 -0
- package/dist/shared/types/questions/index.js +3 -0
- package/dist/shared/types/questions/match-word-to-picture-group.d.ts +6 -2
- package/dist/shared/types/questions/sort-words-into-categories.d.ts +37 -0
- package/dist/shared/types/questions/sort-words-into-categories.js +1 -0
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { MutableRefObject } from 'react';
|
|
2
2
|
export declare const MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION = "Look and complete.";
|
|
3
3
|
export declare const MATCH_WORD_TO_PICTURE_ITEM_TYPE: "MATCH_WORD_TO_PICTURE";
|
|
4
|
+
export declare function toMatchWordToPictureImageUrls(value?: unknown): string[];
|
|
5
|
+
export declare function fromMatchWordToPictureImageUrls(value?: unknown): string | string[] | undefined;
|
|
4
6
|
export interface MatchWordToPictureGroupItemData {
|
|
5
|
-
imageUrl?: string;
|
|
7
|
+
imageUrl?: string | string[];
|
|
6
8
|
correctAnswer: string;
|
|
7
9
|
isExample?: boolean;
|
|
8
10
|
points: number;
|
|
@@ -11,8 +13,12 @@ export interface MatchWordToPictureGroupItemData {
|
|
|
11
13
|
export interface MatchWordToPictureGroupData {
|
|
12
14
|
title?: string;
|
|
13
15
|
instruction: string;
|
|
14
|
-
/** Shared
|
|
15
|
-
|
|
16
|
+
/** Shared reading passage (HTML) above the match-word-to-picture UI. */
|
|
17
|
+
passage?: string;
|
|
18
|
+
/** Shared listening audio played once under the instruction. */
|
|
19
|
+
audioUrl?: string;
|
|
20
|
+
/** Shared cover/scene photo under the instruction (can coexist with item images). */
|
|
21
|
+
imageUrl?: string | string[];
|
|
16
22
|
wordBank: string[];
|
|
17
23
|
items: MatchWordToPictureGroupItemData[];
|
|
18
24
|
explanation?: string;
|
|
@@ -1,2 +1,21 @@
|
|
|
1
1
|
export const MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION = 'Look and complete.';
|
|
2
2
|
export const MATCH_WORD_TO_PICTURE_ITEM_TYPE = 'MATCH_WORD_TO_PICTURE';
|
|
3
|
+
export function toMatchWordToPictureImageUrls(value) {
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
return value
|
|
6
|
+
.filter((url) => typeof url === 'string')
|
|
7
|
+
.map((url) => url.trim())
|
|
8
|
+
.filter((url) => url !== '');
|
|
9
|
+
}
|
|
10
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
11
|
+
return [value.trim()];
|
|
12
|
+
}
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
export function fromMatchWordToPictureImageUrls(value) {
|
|
16
|
+
const urls = toMatchWordToPictureImageUrls(value);
|
|
17
|
+
if (urls.length === 0) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return urls.length === 1 ? urls[0] : urls;
|
|
21
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION = "Read and sort the words.";
|
|
3
|
+
export interface SortWordsCategory {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SortWordsWordItem {
|
|
8
|
+
id: string;
|
|
9
|
+
text: string;
|
|
10
|
+
categoryId: string;
|
|
11
|
+
isExample?: boolean;
|
|
12
|
+
points: number;
|
|
13
|
+
}
|
|
14
|
+
export interface SortWordsPlacement {
|
|
15
|
+
wordId: string;
|
|
16
|
+
categoryId: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SortWordsIntoCategoriesData {
|
|
19
|
+
title?: string;
|
|
20
|
+
instruction: string;
|
|
21
|
+
categories: SortWordsCategory[];
|
|
22
|
+
words: SortWordsWordItem[];
|
|
23
|
+
explanation?: string;
|
|
24
|
+
points?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface SortWordsIntoCategoriesCreatorProps {
|
|
27
|
+
initialData?: SortWordsIntoCategoriesData;
|
|
28
|
+
onSave?: (data: SortWordsIntoCategoriesData) => void;
|
|
29
|
+
onCancel?: () => void;
|
|
30
|
+
onChange?: (data: SortWordsIntoCategoriesData) => void;
|
|
31
|
+
externalErrors?: string[];
|
|
32
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
33
|
+
validationRef?: MutableRefObject<{
|
|
34
|
+
triggerValidation: () => Promise<boolean>;
|
|
35
|
+
getFormData?: () => SortWordsIntoCategoriesData;
|
|
36
|
+
} | null>;
|
|
37
|
+
}
|
|
38
|
+
export interface SortWordsIntoCategoriesClientProps {
|
|
39
|
+
questionData: SortWordsIntoCategoriesData;
|
|
40
|
+
isReviewMode?: boolean;
|
|
41
|
+
userPlacements?: SortWordsPlacement[];
|
|
42
|
+
onPlacementsChange?: (next: SortWordsPlacement[]) => void;
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION = 'Read and sort the words.';
|
|
@@ -71,6 +71,8 @@ const getQuestionTypeName = (type) => {
|
|
|
71
71
|
'MATCH_COLUMNS_TO_MAKE_SENTENCES': 'Ghép cột thành câu',
|
|
72
72
|
'MATCH_WORD_TO_PICTURE_GROUP': 'Nối từ với tranh (nhóm)',
|
|
73
73
|
'WORD_ORDERING_GROUP': 'Sắp xếp từ (nhóm)',
|
|
74
|
+
'SORT_WORDS_INTO_CATEGORIES': 'Sắp xếp từ vào nhóm',
|
|
75
|
+
'CHOOSE_THEN_ANSWER_GROUP': 'Chọn đáp án rồi trả lời',
|
|
74
76
|
};
|
|
75
77
|
return type ? typeMap[type] || type.replace(/_/g, ' ').toLowerCase() : 'Chưa xác định';
|
|
76
78
|
};
|
|
@@ -31,6 +31,8 @@ export function QuestionTypeSelector({ value, onValueChange, disabled, }) {
|
|
|
31
31
|
'MATCH_COLUMNS_TO_MAKE_SENTENCES',
|
|
32
32
|
'MATCH_WORD_TO_PICTURE_GROUP',
|
|
33
33
|
'WORD_ORDERING_GROUP',
|
|
34
|
+
'SORT_WORDS_INTO_CATEGORIES',
|
|
35
|
+
'CHOOSE_THEN_ANSWER_GROUP',
|
|
34
36
|
'WRITE_SENTENCES',
|
|
35
37
|
'FILL_IN_BLANK_GROUP',
|
|
36
38
|
];
|
|
@@ -14,6 +14,7 @@ import { chooseTheCorrectAnswerRegistration } from '../types/choose-the-correct-
|
|
|
14
14
|
import { chooseTheCorrectAnswerGroupRegistration } from '../types/choose-the-correct-answer-group/register';
|
|
15
15
|
import { trueFalseGroupRegistration } from '../types/true-false-group/register';
|
|
16
16
|
import { trueFalseCorrectGroupRegistration } from '../types/true-false-correct-group/register';
|
|
17
|
+
import { chooseThenAnswerGroupRegistration } from '../types/choose-then-answer-group/register';
|
|
17
18
|
import { fillInBlankGroupRegistration } from '../types/fill-in-blank-group/register';
|
|
18
19
|
import { crossOutWordGroupRegistration } from '../types/cross-out-word-group/register';
|
|
19
20
|
import { writeAShortLetterRegistration } from '../types/write-a-short-letter/register';
|
|
@@ -49,6 +50,7 @@ import { wordFillStructuredFormRegistration } from '../types/word-fill-structure
|
|
|
49
50
|
import { wordOrderAndMatchGroupRegistration } from '../types/word-order-and-match-group/register';
|
|
50
51
|
import { crosswordPuzzleRegistration } from '../types/crossword-puzzle/register';
|
|
51
52
|
import { findWordsInMatrixRegistration } from '../types/find-words-in-matrix/register';
|
|
53
|
+
import { sortWordsIntoCategoriesRegistration } from '../types/sort-words-into-categories/register';
|
|
52
54
|
import { matchColumnsToMakeSentencesRegistration } from '../types/match-columns-to-make-sentences/register';
|
|
53
55
|
import { matchWordToPictureGroupRegistration } from '../types/match-word-to-picture-group/register';
|
|
54
56
|
import { wordOrderingGroupRegistration } from '../types/word-ordering-group/register';
|
|
@@ -58,6 +60,7 @@ export const questionTypeRegistry = {
|
|
|
58
60
|
CHOOSE_THE_CORRECT_ANSWER_GROUP: chooseTheCorrectAnswerGroupRegistration,
|
|
59
61
|
TRUE_FALSE_GROUP: trueFalseGroupRegistration,
|
|
60
62
|
TRUE_FALSE_CORRECT_GROUP: trueFalseCorrectGroupRegistration,
|
|
63
|
+
CHOOSE_THEN_ANSWER_GROUP: chooseThenAnswerGroupRegistration,
|
|
61
64
|
FILL_IN_BLANK_GROUP: fillInBlankGroupRegistration,
|
|
62
65
|
CROSS_OUT_WORD_GROUP: crossOutWordGroupRegistration,
|
|
63
66
|
WORD_ORDER_AND_MATCH_GROUP: wordOrderAndMatchGroupRegistration,
|
|
@@ -104,6 +107,7 @@ export const questionTypeRegistry = {
|
|
|
104
107
|
WORD_FILL_STRUCTURED_FORM: wordFillStructuredFormRegistration,
|
|
105
108
|
CROSSWORD_PUZZLE: crosswordPuzzleRegistration,
|
|
106
109
|
FIND_WORDS_IN_MATRIX: findWordsInMatrixRegistration,
|
|
110
|
+
SORT_WORDS_INTO_CATEGORIES: sortWordsIntoCategoriesRegistration,
|
|
107
111
|
MATCH_COLUMNS_TO_MAKE_SENTENCES: matchColumnsToMakeSentencesRegistration,
|
|
108
112
|
MATCH_WORD_TO_PICTURE_GROUP: matchWordToPictureGroupRegistration,
|
|
109
113
|
WORD_ORDERING_GROUP: wordOrderingGroupRegistration,
|
|
@@ -5,21 +5,45 @@ import { Input } from '../../../../components/ui/input';
|
|
|
5
5
|
import { Label } from '../../../../components/ui/label';
|
|
6
6
|
import { CheckCircle2, XCircle, BookOpen } from 'lucide-react';
|
|
7
7
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
8
|
+
import { toAnswerTheQuestionImageUrls, } from '../../_shared/types/answer-the-question.type';
|
|
8
9
|
import { useState } from 'react';
|
|
10
|
+
function isDisplayableMediaUrl(value) {
|
|
11
|
+
return (value.startsWith('http://')
|
|
12
|
+
|| value.startsWith('https://')
|
|
13
|
+
|| value.startsWith('blob:')
|
|
14
|
+
|| value.startsWith('data:')
|
|
15
|
+
|| value.startsWith('/'));
|
|
16
|
+
}
|
|
17
|
+
function MediaPreview({ src, alt }) {
|
|
18
|
+
const { previewUrl, isLoading } = usePresignedFileUrl(src);
|
|
19
|
+
if (isLoading) {
|
|
20
|
+
return _jsx("div", { className: "h-16 w-16 animate-pulse rounded-md bg-slate-100" });
|
|
21
|
+
}
|
|
22
|
+
const url = previewUrl || (isDisplayableMediaUrl(src) ? src : '');
|
|
23
|
+
if (!url) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return (_jsx("img", { src: url, alt: alt, className: "h-auto max-h-80 w-full object-contain", onError: (event) => {
|
|
27
|
+
const target = event.target;
|
|
28
|
+
target.onerror = null;
|
|
29
|
+
target.src = '/images/placeholder-image.svg';
|
|
30
|
+
} }));
|
|
31
|
+
}
|
|
9
32
|
export function AnswerTheQuestionClient({ questionData, onSubmit, isReviewMode = false, userAnswer, autoFillCorrectAnswers = false, explanation, }) {
|
|
10
33
|
const [answer, setAnswer] = useState(userAnswer || '');
|
|
11
|
-
const
|
|
12
|
-
if (!questionData.question) {
|
|
34
|
+
const imageUrls = toAnswerTheQuestionImageUrls(questionData.imageUrl);
|
|
35
|
+
if (!questionData.question && imageUrls.length === 0) {
|
|
13
36
|
return (_jsx(Card, { children: _jsx(CardContent, { className: "p-6", children: _jsx("p", { className: "text-gray-500", children: "Kh\u00F4ng c\u00F3 d\u1EEF li\u1EC7u c\u00E2u h\u1ECFi" }) }) }));
|
|
14
37
|
}
|
|
15
38
|
const isExample = questionData.isExample || false;
|
|
39
|
+
const correctAnswer = questionData.correctAnswer || '';
|
|
16
40
|
const displayedAnswer = autoFillCorrectAnswers || isExample
|
|
17
|
-
?
|
|
41
|
+
? correctAnswer
|
|
18
42
|
: answer;
|
|
19
43
|
const isCorrect = isReviewMode && userAnswer !== undefined
|
|
20
|
-
? userAnswer.trim().toLowerCase() ===
|
|
44
|
+
? userAnswer.trim().toLowerCase() === correctAnswer.trim().toLowerCase()
|
|
21
45
|
: undefined;
|
|
22
|
-
return (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-teal-400 to-cyan-500" }), _jsxs(CardContent, { className: "p-6 space-y-6", children: [(questionData.title || questionData.groupContent) && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-gradient-to-r from-indigo-50 to-blue-50 p-4 space-y-2", children: [questionData.title && (_jsx("h3", { className: "text-base font-bold text-indigo-900", children: questionData.title })), questionData.groupContent && (_jsx("p", { className: "text-sm text-indigo-700 leading-relaxed whitespace-pre-wrap", children: questionData.groupContent }))] })),
|
|
46
|
+
return (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-teal-400 to-cyan-500" }), _jsxs(CardContent, { className: "p-6 space-y-6", children: [(questionData.title || questionData.groupContent) && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-gradient-to-r from-indigo-50 to-blue-50 p-4 space-y-2", children: [questionData.title && (_jsx("h3", { className: "text-base font-bold text-indigo-900", children: questionData.title })), questionData.groupContent && (_jsx("p", { className: "text-sm text-indigo-700 leading-relaxed whitespace-pre-wrap", children: questionData.groupContent }))] })), imageUrls.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: imageUrls.map((url, imageIndex) => (_jsx("div", { className: "relative max-w-lg overflow-hidden rounded-xl border border-gray-200 bg-gray-50 shadow-sm", children: _jsx(MediaPreview, { src: url, alt: `Question image ${imageIndex + 1}` }) }, `atq-image-${imageIndex}`))) })), _jsxs("div", { className: "rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-5", children: [isExample && (_jsxs("div", { className: "mb-2 inline-flex items-center gap-1.5 rounded-full bg-amber-100 border border-amber-300 px-3 py-1", children: [_jsx(BookOpen, { className: "h-3.5 w-3.5 text-amber-600" }), _jsx("span", { className: "text-xs font-semibold text-amber-700 uppercase tracking-wide", children: "Example" })] })), questionData.question ? (_jsx(Label, { className: "text-base font-semibold text-gray-800 mb-3 block", children: questionData.question })) : null, questionData.suggestedAnswers && questionData.suggestedAnswers.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 mt-2 mb-1", children: questionData.suggestedAnswers.map((suggestion, index) => (_jsx("button", { type: "button", disabled: isReviewMode || isExample, onClick: () => {
|
|
23
47
|
if (!isReviewMode && !isExample) {
|
|
24
48
|
setAnswer(suggestion);
|
|
25
49
|
onSubmit?.(suggestion);
|
|
@@ -33,5 +57,5 @@ export function AnswerTheQuestionClient({ questionData, onSubmit, isReviewMode =
|
|
|
33
57
|
? isCorrect
|
|
34
58
|
? 'border-green-400 bg-green-50 text-green-700'
|
|
35
59
|
: 'border-red-400 bg-red-50 text-red-700'
|
|
36
|
-
: 'border-teal-300 focus:border-teal-500 focus:ring-teal-200'}` }), questionData.suffixText && (_jsx("span", { className: "text-base font-medium text-gray-700", children: questionData.suffixText })), isReviewMode && isCorrect !== undefined && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(CheckCircle2, { className: "h-5 w-5 text-green-500" })) : (_jsx(XCircle, { className: "h-5 w-5 text-red-500" })) }))] }), isReviewMode && isCorrect === false && (_jsxs("div", { className: "mt-3 flex items-center gap-2 rounded-lg bg-green-50 border border-green-200 px-4 py-2", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsxs("span", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children:
|
|
60
|
+
: 'border-teal-300 focus:border-teal-500 focus:ring-teal-200'}` }), questionData.suffixText && (_jsx("span", { className: "text-base font-medium text-gray-700", children: questionData.suffixText })), isReviewMode && isCorrect !== undefined && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(CheckCircle2, { className: "h-5 w-5 text-green-500" })) : (_jsx(XCircle, { className: "h-5 w-5 text-red-500" })) }))] }), isReviewMode && isCorrect === false && (_jsxs("div", { className: "mt-3 flex items-center gap-2 rounded-lg bg-green-50 border border-green-200 px-4 py-2", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsxs("span", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: correctAnswer })] })] })), autoFillCorrectAnswers && (_jsxs("div", { className: "mt-3 flex items-center gap-2 rounded-lg bg-teal-50 border border-teal-200 px-4 py-2", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-teal-600" }), _jsxs("span", { className: "text-sm text-teal-700", children: ["\u0110\u00E1p \u00E1n: ", _jsx("strong", { children: correctAnswer })] })] }))] }), explanation && isReviewMode && (_jsxs("div", { className: "rounded-xl border border-blue-200 bg-blue-50 p-4", children: [_jsx("p", { className: "text-sm font-semibold text-blue-800 mb-1", children: "Gi\u1EA3i th\u00EDch:" }), _jsx("p", { className: "text-sm text-blue-700", children: explanation })] }))] })] }));
|
|
37
61
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { fromAnswerTheQuestionImageUrls } from '../../_shared/types/answer-the-question.type';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return isRecord(value) ? value : {};
|
|
7
|
+
}
|
|
8
|
+
function asString(value, fallback = '') {
|
|
9
|
+
return typeof value === 'string' ? value : fallback;
|
|
10
|
+
}
|
|
11
|
+
function asStringArray(value) {
|
|
12
|
+
if (!Array.isArray(value)) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const items = value
|
|
16
|
+
.filter((item) => typeof item === 'string')
|
|
17
|
+
.map((item) => item.trim())
|
|
18
|
+
.filter((item) => item !== '');
|
|
19
|
+
return items.length > 0 ? items : undefined;
|
|
20
|
+
}
|
|
21
|
+
function resolveCorrectAnswer(correct, answer) {
|
|
22
|
+
if (typeof correct.answer === 'string') {
|
|
23
|
+
return correct.answer;
|
|
24
|
+
}
|
|
25
|
+
if (typeof answer.correctAnswer === 'string') {
|
|
26
|
+
return answer.correctAnswer;
|
|
27
|
+
}
|
|
28
|
+
if (typeof answer.answer === 'string') {
|
|
29
|
+
return answer.answer;
|
|
30
|
+
}
|
|
31
|
+
const expected = correct.expectedAnswers ?? answer.expectedAnswers;
|
|
32
|
+
if (Array.isArray(expected) && typeof expected[0] === 'string') {
|
|
33
|
+
return expected[0];
|
|
34
|
+
}
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
export function mapQuestionToAnswerTheQuestionData(question) {
|
|
38
|
+
const source = asRecord(question);
|
|
39
|
+
const content = asRecord(source.content);
|
|
40
|
+
const answer = asRecord(source.answer);
|
|
41
|
+
const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer);
|
|
42
|
+
const imageUrl = fromAnswerTheQuestionImageUrls(content.imageUrl ?? answer.imageUrl ?? source.imageUrl);
|
|
43
|
+
return {
|
|
44
|
+
question: asString(content.question) || asString(answer.question),
|
|
45
|
+
correctAnswer: resolveCorrectAnswer(correctAnswer, answer),
|
|
46
|
+
suffixText: asString(content.suffixText) || asString(answer.suffixText) || undefined,
|
|
47
|
+
prefixText: asString(content.prefixText) || asString(answer.prefixText) || undefined,
|
|
48
|
+
points: typeof source.points === 'number' ? source.points : undefined,
|
|
49
|
+
isExample: source.isExample === true || answer.isExample === true,
|
|
50
|
+
suggestedAnswers: asStringArray(content.suggestedAnswers) || asStringArray(answer.suggestedAnswers),
|
|
51
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
52
|
+
title: asString(content.title) || asString(answer.title) || undefined,
|
|
53
|
+
groupContent: asString(content.groupContent) || asString(answer.groupContent) || undefined,
|
|
54
|
+
explanation: asString(source.explanation) || asString(answer.explanation) || undefined,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fromAnswerTheQuestionImageUrls } from '../../_shared/types/answer-the-question.type';
|
|
1
2
|
export const transformAnswerTheQuestion = (question) => {
|
|
2
3
|
const answerData = question.answer;
|
|
3
4
|
if (!answerData?.question) {
|
|
@@ -12,14 +13,19 @@ export const transformAnswerTheQuestion = (question) => {
|
|
|
12
13
|
if (answerData.prefixText) {
|
|
13
14
|
apiContent.prefixText = answerData.prefixText;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const imageUrl = fromAnswerTheQuestionImageUrls(answerData.imageUrl);
|
|
17
|
+
if (imageUrl !== undefined) {
|
|
18
|
+
apiContent.imageUrl = imageUrl;
|
|
19
|
+
}
|
|
20
|
+
// Group chrome (title/passage) stays on questionGroup.payload.
|
|
21
|
+
// Per-question image lives on content.imageUrl — same split as FILL_IN_BLANK.
|
|
22
|
+
const hasGroupData = !!(answerData.title || answerData.subtitle || answerData.groupContent);
|
|
17
23
|
const basicGroupData = hasGroupData
|
|
18
24
|
? {
|
|
19
25
|
title: answerData.title || '',
|
|
20
26
|
subtitle: answerData.subtitle || '',
|
|
21
27
|
content: answerData.groupContent || '',
|
|
22
|
-
imageUrl:
|
|
28
|
+
imageUrl: '',
|
|
23
29
|
}
|
|
24
30
|
: undefined;
|
|
25
31
|
return {
|
package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js
CHANGED
|
@@ -1,24 +1,57 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { BookOpen, CircleHelp, Lightbulb, Sparkles, Star } from 'lucide-react';
|
|
3
|
+
import { BookOpen, CircleHelp, Lightbulb, Sparkles, Star, Volume2 } from 'lucide-react';
|
|
4
4
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
5
5
|
import { toAnswerTheQuestionGroupImageUrls } from '../../_shared/types/answer-the-question-group.type';
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
function isDisplayableMediaUrl(value) {
|
|
7
|
+
return (value.startsWith('http://')
|
|
8
|
+
|| value.startsWith('https://')
|
|
9
|
+
|| value.startsWith('blob:')
|
|
10
|
+
|| value.startsWith('data:')
|
|
11
|
+
|| value.startsWith('/'));
|
|
12
|
+
}
|
|
13
|
+
function isPlayableAudioUrl(value) {
|
|
14
|
+
return (value.startsWith('http://')
|
|
15
|
+
|| value.startsWith('https://')
|
|
16
|
+
|| value.startsWith('blob:')
|
|
17
|
+
|| value.startsWith('data:'));
|
|
18
|
+
}
|
|
19
|
+
function toPresignKey(value) {
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
if (isPlayableAudioUrl(trimmed)) {
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
return trimmed.replace(/^\/+/, '');
|
|
25
|
+
}
|
|
26
|
+
function MediaPreview({ src, kind = 'image', alt }) {
|
|
27
|
+
const fileKey = kind === 'audio' ? toPresignKey(src) : src;
|
|
28
|
+
const { previewUrl, isLoading } = usePresignedFileUrl(fileKey);
|
|
29
|
+
if (isLoading) {
|
|
30
|
+
return _jsx("div", { className: "h-16 w-16 animate-pulse rounded-md bg-slate-100" });
|
|
31
|
+
}
|
|
32
|
+
const url = previewUrl
|
|
33
|
+
|| (kind === 'audio'
|
|
34
|
+
? (isPlayableAudioUrl(fileKey) ? fileKey : '')
|
|
35
|
+
: (isDisplayableMediaUrl(src) ? src : ''));
|
|
36
|
+
if (!url) {
|
|
11
37
|
return null;
|
|
12
38
|
}
|
|
13
|
-
|
|
39
|
+
if (kind === 'audio') {
|
|
40
|
+
return _jsx("audio", { controls: true, src: url, className: "h-8 max-w-full", preload: "metadata" });
|
|
41
|
+
}
|
|
42
|
+
return (_jsx("div", { className: "max-w-sm overflow-hidden rounded-lg border border-gray-200 bg-gray-50", children: _jsx("img", { src: url, alt: alt, className: "max-h-56 w-auto object-contain p-2", onError: (event) => {
|
|
43
|
+
const target = event.target;
|
|
44
|
+
target.onerror = null;
|
|
45
|
+
target.src = '/images/placeholder-image.svg';
|
|
46
|
+
} }) }));
|
|
14
47
|
}
|
|
15
48
|
export function AnswerTheQuestionGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
|
|
16
49
|
const items = questionData.items || [];
|
|
17
|
-
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 })), toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(
|
|
50
|
+
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 && (_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(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: `Hình ảnh (${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 leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
|
|
18
51
|
const userValue = userAnswers[itemIndex] || '';
|
|
19
52
|
const expectedAnswers = Array.isArray(item.expectedAnswers)
|
|
20
53
|
? item.expectedAnswers.filter((answer) => answer.trim() !== '')
|
|
21
54
|
: [];
|
|
22
|
-
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: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] })] }), _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' }), _jsx("div", { className: "rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-700", children: userValue || (isReviewMode ? '(Chưa trả lời)' : 'Học sinh sẽ nhập câu trả lời tại đây') }), isReviewMode && expectedAnswers.length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n m\u1EABu (AI)" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] }))] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
55
|
+
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: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] })] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-4", children: [toAnswerTheQuestionGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), _jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), _jsx("div", { className: "rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-700", children: userValue || (isReviewMode ? '(Chưa trả lời)' : 'Học sinh sẽ nhập câu trả lời tại đây') }), isReviewMode && expectedAnswers.length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n m\u1EABu (AI)" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] }))] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
23
56
|
}), 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] }))] }));
|
|
24
57
|
}
|
package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js
CHANGED
|
@@ -12,6 +12,7 @@ import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
|
12
12
|
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
13
13
|
import { Eye, Pencil, Plus, Sparkles, Star, Trash2 } from 'lucide-react';
|
|
14
14
|
import { AnswerTheQuestionGroupClient } from './AnswerTheQuestionGroupClient';
|
|
15
|
+
import { toAnswerTheQuestionGroupImageUrls } from '../../_shared/types/answer-the-question-group.type';
|
|
15
16
|
const DEFAULT_INSTRUCTION = 'Read the text again and answer the questions.';
|
|
16
17
|
function createEmptyItem(questionNumber) {
|
|
17
18
|
return {
|
|
@@ -38,6 +39,7 @@ function normalizeItems(rawItems) {
|
|
|
38
39
|
isExample: item.isExample === true,
|
|
39
40
|
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
40
41
|
questionNumber: item.questionNumber || index + 1,
|
|
42
|
+
...(item.imageUrl !== undefined ? { imageUrl: item.imageUrl } : {}),
|
|
41
43
|
}));
|
|
42
44
|
}
|
|
43
45
|
function sumPoints(items) {
|
|
@@ -49,9 +51,8 @@ function isContentEmpty(html) {
|
|
|
49
51
|
export function AnswerTheQuestionGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
|
|
50
52
|
const [instruction, setInstruction] = useState(initialData?.instruction || DEFAULT_INSTRUCTION);
|
|
51
53
|
const [passage, setPassage] = useState(initialData?.passage || '');
|
|
52
|
-
const [
|
|
53
|
-
|
|
54
|
-
: []);
|
|
54
|
+
const [audioUrl] = useState(initialData?.audioUrl || '');
|
|
55
|
+
const [imageUrl] = useState(toAnswerTheQuestionGroupImageUrls(initialData?.imageUrl));
|
|
55
56
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
56
57
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
57
58
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
@@ -64,6 +65,7 @@ export function AnswerTheQuestionGroupCreator({ initialData, onChange, externalE
|
|
|
64
65
|
const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation) => ({
|
|
65
66
|
instruction: nextInstruction,
|
|
66
67
|
...(!isContentEmpty(nextPassage) ? { passage: nextPassage.trim() } : {}),
|
|
68
|
+
...(audioUrl.trim() ? { audioUrl: audioUrl.trim() } : {}),
|
|
67
69
|
...(imageUrl.length ? { imageUrl } : {}),
|
|
68
70
|
items: nextItems,
|
|
69
71
|
explanation: nextExplanation,
|
|
@@ -81,7 +83,7 @@ export function AnswerTheQuestionGroupCreator({ initialData, onChange, externalE
|
|
|
81
83
|
previousPayloadRef.current = serialized;
|
|
82
84
|
onUnsavedChangesChange?.(true);
|
|
83
85
|
debouncedOnChange(payload);
|
|
84
|
-
}, [instruction, passage, imageUrl, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
86
|
+
}, [instruction, passage, audioUrl, imageUrl, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
85
87
|
const validate = () => {
|
|
86
88
|
const nextErrors = [];
|
|
87
89
|
if (!instruction.trim()) {
|
|
@@ -115,7 +117,7 @@ export function AnswerTheQuestionGroupCreator({ initialData, onChange, externalE
|
|
|
115
117
|
},
|
|
116
118
|
getFormData: () => buildPayload(),
|
|
117
119
|
};
|
|
118
|
-
}, [validationRef, instruction, passage, imageUrl, items, explanation]);
|
|
120
|
+
}, [validationRef, instruction, passage, audioUrl, imageUrl, items, explanation]);
|
|
119
121
|
const updateItem = (index, patch) => {
|
|
120
122
|
setItems((prev) => {
|
|
121
123
|
const next = [...prev];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { toAnswerTheQuestionGroupImageUrls } from '../../_shared/types/answer-the-question-group.type';
|
|
1
|
+
import { fromAnswerTheQuestionGroupImageUrls, toAnswerTheQuestionGroupImageUrls, } from '../../_shared/types/answer-the-question-group.type';
|
|
2
2
|
const DEFAULT_INSTRUCTION = 'Read the text again and answer the questions.';
|
|
3
3
|
function isRecord(value) {
|
|
4
4
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -9,6 +9,13 @@ function asRecord(value) {
|
|
|
9
9
|
function asString(value, fallback = '') {
|
|
10
10
|
return typeof value === 'string' ? value : fallback;
|
|
11
11
|
}
|
|
12
|
+
function asOptionalUrl(value) {
|
|
13
|
+
if (typeof value !== 'string') {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
return trimmed !== '' ? trimmed : undefined;
|
|
18
|
+
}
|
|
12
19
|
function normalizeExpectedAnswers(value) {
|
|
13
20
|
if (!Array.isArray(value)) {
|
|
14
21
|
return [];
|
|
@@ -38,12 +45,14 @@ export function mapAnswerTheQuestionGroupItem(item, index, fallbackAnswers) {
|
|
|
38
45
|
: (normalizeExpectedAnswers(fallbackAnswers).length > 0
|
|
39
46
|
? normalizeExpectedAnswers(fallbackAnswers)
|
|
40
47
|
: ['']);
|
|
48
|
+
const imageUrl = fromAnswerTheQuestionGroupImageUrls(content.imageUrl ?? record.imageUrl);
|
|
41
49
|
return {
|
|
42
50
|
question: asString(content.question) || asString(record.question),
|
|
43
51
|
expectedAnswers,
|
|
44
52
|
isExample,
|
|
45
53
|
points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
|
|
46
54
|
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
55
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
47
56
|
};
|
|
48
57
|
}
|
|
49
58
|
export function mapQuestionToAnswerTheQuestionGroupData(question) {
|
|
@@ -57,10 +66,13 @@ export function mapQuestionToAnswerTheQuestionGroupData(question) {
|
|
|
57
66
|
: [];
|
|
58
67
|
if (Array.isArray(content.items)) {
|
|
59
68
|
const items = content.items.map((item, index) => mapAnswerTheQuestionGroupItem(item, index, fallbackAnswers[index]?.expectedAnswers));
|
|
60
|
-
const
|
|
69
|
+
const meta = asRecord(content.meta);
|
|
70
|
+
const audioUrl = asOptionalUrl(meta.audioUrl);
|
|
71
|
+
const imageUrl = toAnswerTheQuestionGroupImageUrls(meta.imageUrl);
|
|
61
72
|
return {
|
|
62
|
-
instruction: asString(
|
|
63
|
-
passage: asString(
|
|
73
|
+
instruction: asString(meta.instruction) || DEFAULT_INSTRUCTION,
|
|
74
|
+
passage: asString(meta.passage),
|
|
75
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
64
76
|
...(imageUrl.length ? { imageUrl } : {}),
|
|
65
77
|
items,
|
|
66
78
|
explanation,
|
|
@@ -68,10 +80,12 @@ export function mapQuestionToAnswerTheQuestionGroupData(question) {
|
|
|
68
80
|
};
|
|
69
81
|
}
|
|
70
82
|
if (Array.isArray(answer.items)) {
|
|
83
|
+
const audioUrl = asOptionalUrl(answer.audioUrl);
|
|
71
84
|
const imageUrl = toAnswerTheQuestionGroupImageUrls(answer.imageUrl);
|
|
72
85
|
return {
|
|
73
86
|
instruction: asString(answer.instruction, DEFAULT_INSTRUCTION),
|
|
74
87
|
passage: asString(answer.passage),
|
|
88
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
75
89
|
...(imageUrl.length ? { imageUrl } : {}),
|
|
76
90
|
items: answer.items,
|
|
77
91
|
explanation,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { toAnswerTheQuestionGroupImageUrls } from '../../_shared/types/answer-the-question-group.type';
|
|
1
|
+
import { fromAnswerTheQuestionGroupImageUrls, toAnswerTheQuestionGroupImageUrls, } from '../../_shared/types/answer-the-question-group.type';
|
|
2
2
|
const CHILD_QUESTION_TYPE = 'ANSWER_THE_QUESTION';
|
|
3
3
|
const GRADING_TYPE = 'AI';
|
|
4
4
|
const DEFAULT_INSTRUCTION = 'Read the text again and answer the questions.';
|
|
@@ -27,10 +27,12 @@ export const transformAnswerTheQuestionGroup = (question) => {
|
|
|
27
27
|
const answerData = question.answer;
|
|
28
28
|
const items = Array.isArray(answerData?.items) ? answerData.items : [];
|
|
29
29
|
if (items.length === 0) {
|
|
30
|
+
const emptyAudioUrl = typeof answerData?.audioUrl === 'string' ? answerData.audioUrl.trim() : '';
|
|
30
31
|
return {
|
|
31
32
|
apiContent: {
|
|
32
33
|
meta: {
|
|
33
|
-
instruction: DEFAULT_INSTRUCTION,
|
|
34
|
+
instruction: answerData?.instruction || DEFAULT_INSTRUCTION,
|
|
35
|
+
...(emptyAudioUrl ? { audioUrl: emptyAudioUrl } : {}),
|
|
34
36
|
gradingType: GRADING_TYPE,
|
|
35
37
|
isAiGraded: true,
|
|
36
38
|
},
|
|
@@ -41,11 +43,13 @@ export const transformAnswerTheQuestionGroup = (question) => {
|
|
|
41
43
|
}
|
|
42
44
|
const apiItems = items.map((item, index) => {
|
|
43
45
|
const expectedAnswers = normalizeExpectedAnswers(item.expectedAnswers);
|
|
46
|
+
const imageUrl = fromAnswerTheQuestionGroupImageUrls(item.imageUrl);
|
|
44
47
|
return {
|
|
45
48
|
questionType: CHILD_QUESTION_TYPE,
|
|
46
49
|
...(item.isExample ? { isExample: true } : {}),
|
|
47
50
|
content: {
|
|
48
51
|
question: item.question || '',
|
|
52
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
49
53
|
},
|
|
50
54
|
correctAnswer: { expectedAnswers },
|
|
51
55
|
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
@@ -53,12 +57,14 @@ export const transformAnswerTheQuestionGroup = (question) => {
|
|
|
53
57
|
};
|
|
54
58
|
});
|
|
55
59
|
const passage = answerData?.passage?.trim();
|
|
60
|
+
const audioUrl = typeof answerData?.audioUrl === 'string' ? answerData.audioUrl.trim() : '';
|
|
56
61
|
const imageUrl = toAnswerTheQuestionGroupImageUrls(answerData?.imageUrl);
|
|
57
62
|
return {
|
|
58
63
|
apiContent: {
|
|
59
64
|
meta: {
|
|
60
65
|
instruction: answerData?.instruction || DEFAULT_INSTRUCTION,
|
|
61
66
|
...(!isContentEmpty(passage) ? { passage } : {}),
|
|
67
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
62
68
|
...(imageUrl.length ? { imageUrl } : {}),
|
|
63
69
|
gradingType: GRADING_TYPE,
|
|
64
70
|
isAiGraded: true,
|
package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { ChooseThenAnswerGroupClientProps } from '../../_shared/types/choose-then-answer-group.type';
|
|
2
|
+
export declare function ChooseThenAnswerGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: ChooseThenAnswerGroupClientProps): import("react").JSX.Element;
|
package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
function emptyAnswer() {
|
|
6
|
+
return { optionId: '', text: '' };
|
|
7
|
+
}
|
|
8
|
+
export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
9
|
+
const canEdit = Boolean(onAnswerChange) && !isReviewMode;
|
|
10
|
+
const items = questionData.items || [];
|
|
11
|
+
return (_jsxs("div", { className: "space-y-4", children: [(questionData.title || questionData.instruction) && (_jsxs("div", { className: "rounded-xl border border-slate-200 bg-slate-50 px-4 py-3", children: [questionData.title && (_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-slate-500", children: questionData.title })), questionData.instruction && (_jsx("p", { className: "text-sm font-medium text-slate-800", children: questionData.instruction }))] })), items.map((item, itemIndex) => {
|
|
12
|
+
const stored = userAnswers[itemIndex] || emptyAnswer();
|
|
13
|
+
const current = item.isExample
|
|
14
|
+
? { optionId: item.optionId, text: stored.text }
|
|
15
|
+
: stored;
|
|
16
|
+
const expectedId = item.optionId;
|
|
17
|
+
const parts = (item.question || '').split('{0}');
|
|
18
|
+
const setAnswer = (next) => {
|
|
19
|
+
if (!canEdit || item.isExample)
|
|
20
|
+
return;
|
|
21
|
+
onAnswerChange?.(itemIndex, {
|
|
22
|
+
optionId: next.optionId ?? current.optionId,
|
|
23
|
+
text: next.text ?? current.text,
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
return (_jsxs("div", { className: "space-y-2 rounded-xl border border-gray-200 bg-white p-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-full bg-slate-800 text-xs font-bold text-white", children: 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-[10px] font-semibold uppercase text-amber-800", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("p", { className: "flex flex-wrap items-center gap-1.5 text-[15px] leading-relaxed text-slate-800", children: [_jsx("span", { children: parts[0] || '' }), _jsx("span", { className: "inline-flex items-center gap-1", children: item.options.map((option, optionIndex) => {
|
|
27
|
+
const selected = current.optionId === option.id;
|
|
28
|
+
const showReview = isReviewMode && !item.isExample && Boolean(expectedId);
|
|
29
|
+
const isCorrect = showReview && option.id === expectedId;
|
|
30
|
+
const isWrong = showReview && selected && option.id !== expectedId;
|
|
31
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1", children: [optionIndex > 0 && _jsx("span", { className: "text-slate-400", children: "|" }), _jsx("button", { type: "button", disabled: !canEdit || item.isExample, onClick: () => setAnswer({ optionId: option.id }), className: cn('rounded-md border px-2.5 py-1 text-sm transition-colors', selected && !isCorrect && !isWrong && 'border-black bg-black text-white', !selected && !isCorrect && !isWrong && 'border-gray-300 bg-white text-gray-900', isCorrect && 'border-emerald-500 bg-emerald-50 text-emerald-900', isWrong && 'border-red-500 bg-red-50 text-red-900', (!canEdit || item.isExample) && 'cursor-default'), children: option.text || option.id })] }, option.id));
|
|
32
|
+
}) }), _jsx("span", { children: parts.slice(1).join('{0}') })] }), _jsx("textarea", { value: current.text, disabled: !canEdit || item.isExample, onChange: (event) => setAnswer({ text: event.target.value }), placeholder: "Answer for you", rows: 3, className: "w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800 outline-none focus:border-slate-400 disabled:bg-slate-50" })] }, `${item.questionNumber}-${itemIndex}`));
|
|
33
|
+
}), 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] }))] }));
|
|
34
|
+
}
|
package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { ChooseThenAnswerGroupCreatorProps } from '../../_shared/types/choose-then-answer-group.type';
|
|
2
|
+
export declare function ChooseThenAnswerGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }: ChooseThenAnswerGroupCreatorProps): import("react").JSX.Element;
|