@tinyweb_dev/oe-exam-sdk 0.2.12 → 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/MoversFillInBlankGroupRenderer.d.ts +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -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 +43 -4
- 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 +7 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.js +19 -1
- 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/fill-in-blank-group.type.d.ts +14 -2
- 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 +10 -1
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +19 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +9 -2
- 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 +47 -3
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +8 -2
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +21 -2
- package/dist/components/questions/types/answer-the-question-group/transform.js +10 -1
- 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/fill-in-blank-group/FillInBlankGroupClient.js +51 -16
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +52 -20
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +23 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +89 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +24 -6
- package/dist/components/questions/types/fill-in-blank-group/transform.js +18 -3
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +41 -12
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +23 -8
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +11 -2
- package/dist/components/questions/types/match-word-to-picture-group/transform.js +25 -9
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts +4 -2
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +4 -4
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +13 -2
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +25 -13
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +19 -2
- package/dist/components/questions/types/matching-with-lines-group/transform.js +29 -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/types/word-fill-structured-form/WordFillStructuredFormClient.js +18 -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 +126 -6
- 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/fill-in-blank-group.d.ts +9 -2
- 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 +7 -1
- package/dist/shared/types/questions/matching-with-lines-group.d.ts +7 -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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const CHOOSE_THEN_ANSWER_ITEM_TYPE: "CHOOSE_THEN_ANSWER";
|
|
3
|
+
export declare const CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION = "Read and choose. Then answer for you.";
|
|
4
|
+
export interface ChooseThenAnswerOption {
|
|
5
|
+
id: string;
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ChooseThenAnswerItemAnswer {
|
|
9
|
+
optionId: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ChooseThenAnswerStudentItemAnswer {
|
|
12
|
+
optionId: string;
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ChooseThenAnswerGroupItemData {
|
|
16
|
+
question: string;
|
|
17
|
+
options: ChooseThenAnswerOption[];
|
|
18
|
+
optionId: string;
|
|
19
|
+
isExample?: boolean;
|
|
20
|
+
points: number;
|
|
21
|
+
questionNumber: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ChooseThenAnswerGroupData {
|
|
24
|
+
title?: string;
|
|
25
|
+
instruction: string;
|
|
26
|
+
items: ChooseThenAnswerGroupItemData[];
|
|
27
|
+
explanation?: string;
|
|
28
|
+
points?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ChooseThenAnswerGroupCreatorProps {
|
|
31
|
+
initialData?: ChooseThenAnswerGroupData;
|
|
32
|
+
onSave?: (data: ChooseThenAnswerGroupData) => void;
|
|
33
|
+
onCancel?: () => void;
|
|
34
|
+
onChange?: (data: ChooseThenAnswerGroupData) => void;
|
|
35
|
+
externalErrors?: string[];
|
|
36
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
37
|
+
validationRef?: MutableRefObject<{
|
|
38
|
+
triggerValidation: () => Promise<boolean>;
|
|
39
|
+
getFormData?: () => ChooseThenAnswerGroupData;
|
|
40
|
+
} | null>;
|
|
41
|
+
}
|
|
42
|
+
export interface ChooseThenAnswerGroupClientProps {
|
|
43
|
+
questionData: ChooseThenAnswerGroupData;
|
|
44
|
+
isReviewMode?: boolean;
|
|
45
|
+
userAnswers?: ChooseThenAnswerStudentItemAnswer[];
|
|
46
|
+
onAnswerChange?: (index: number, next: ChooseThenAnswerStudentItemAnswer) => void;
|
|
47
|
+
}
|
|
@@ -8,24 +8,36 @@ export interface FillInBlankGroupItemAnswer {
|
|
|
8
8
|
export interface FillInBlankGroupStudentItemAnswer {
|
|
9
9
|
answers: string[];
|
|
10
10
|
}
|
|
11
|
+
/** Per-blank choice list. Index i ↔ `{i}`. `null` = free-type (WFSF WITH_OPTIONS). */
|
|
12
|
+
export type FillInBlankWordBankEntry = {
|
|
13
|
+
options: string[];
|
|
14
|
+
} | null;
|
|
11
15
|
export interface FillInBlankGroupItemData {
|
|
12
16
|
question: string;
|
|
13
17
|
answers: string[][];
|
|
14
18
|
caseSensitive?: boolean;
|
|
15
|
-
imageUrl?: string;
|
|
19
|
+
imageUrl?: string | string[];
|
|
16
20
|
audioUrl?: string;
|
|
17
21
|
showHints?: boolean;
|
|
18
22
|
hints?: string[];
|
|
23
|
+
/** Per-blank dropdown options for this item. Overrides group `wordBank`. */
|
|
24
|
+
wordBank?: FillInBlankWordBankEntry[];
|
|
19
25
|
isExample?: boolean;
|
|
20
26
|
points: number;
|
|
21
27
|
questionNumber: number;
|
|
22
28
|
}
|
|
23
29
|
export interface FillInBlankGroupData {
|
|
24
30
|
instruction: string;
|
|
31
|
+
/** Shared reading passage (HTML) above the item list. */
|
|
32
|
+
passage?: string;
|
|
25
33
|
audioUrl?: string;
|
|
26
|
-
imageUrl?: string;
|
|
34
|
+
imageUrl?: string | string[];
|
|
27
35
|
showHints?: boolean;
|
|
28
36
|
hints?: string[];
|
|
37
|
+
/** When `WITH_OPTIONS`, blanks with `wordBank[i].options` render as dropdowns. */
|
|
38
|
+
viewMode?: 'WITH_OPTIONS';
|
|
39
|
+
/** Fallback per-blank options applied to every item that has no own `wordBank`. */
|
|
40
|
+
wordBank?: FillInBlankWordBankEntry[];
|
|
29
41
|
items: FillInBlankGroupItemData[];
|
|
30
42
|
explanation?: string;
|
|
31
43
|
points?: number;
|
|
@@ -15,5 +15,7 @@ export * from './true-false-correct-group.type';
|
|
|
15
15
|
export * from './fill-in-blank-group.type';
|
|
16
16
|
export * from './match-word-to-picture.type';
|
|
17
17
|
export * from './matching-with-lines-group.type';
|
|
18
|
+
export * from './sort-words-into-categories.type';
|
|
19
|
+
export * from './choose-then-answer-group.type';
|
|
18
20
|
export * from './match-columns-to-make-sentences.type';
|
|
19
21
|
export * from './spontaneous-qa-group.type';
|
|
@@ -15,5 +15,7 @@ export * from './true-false-correct-group.type';
|
|
|
15
15
|
export * from './fill-in-blank-group.type';
|
|
16
16
|
export * from './match-word-to-picture.type';
|
|
17
17
|
export * from './matching-with-lines-group.type';
|
|
18
|
+
export * from './sort-words-into-categories.type';
|
|
19
|
+
export * from './choose-then-answer-group.type';
|
|
18
20
|
export * from './match-columns-to-make-sentences.type';
|
|
19
21
|
export * from './spontaneous-qa-group.type';
|
|
@@ -1,15 +1,24 @@
|
|
|
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
|
|
7
|
+
imageUrl?: string | string[];
|
|
6
8
|
correctAnswer: string;
|
|
7
9
|
isExample?: boolean;
|
|
8
10
|
points: number;
|
|
9
11
|
questionNumber: number;
|
|
10
12
|
}
|
|
11
13
|
export interface MatchWordToPictureGroupData {
|
|
14
|
+
title?: string;
|
|
12
15
|
instruction: string;
|
|
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[];
|
|
13
22
|
wordBank: string[];
|
|
14
23
|
items: MatchWordToPictureGroupItemData[];
|
|
15
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
|
+
}
|
|
@@ -2,10 +2,12 @@ import type { MutableRefObject } from 'react';
|
|
|
2
2
|
export declare const MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION = "Look and match.";
|
|
3
3
|
export interface MatchingWithLinesRightItem {
|
|
4
4
|
id: string;
|
|
5
|
-
text
|
|
5
|
+
text?: string;
|
|
6
|
+
imageUrl?: string;
|
|
6
7
|
}
|
|
7
8
|
export interface MatchingWithLinesGroupItemData {
|
|
8
|
-
imageUrl
|
|
9
|
+
imageUrl?: string;
|
|
10
|
+
text?: string;
|
|
9
11
|
correctAnswer: string;
|
|
10
12
|
isExample?: boolean;
|
|
11
13
|
points: number;
|
|
@@ -14,6 +16,11 @@ export interface MatchingWithLinesGroupItemData {
|
|
|
14
16
|
export interface MatchingWithLinesGroupData {
|
|
15
17
|
title?: string;
|
|
16
18
|
instruction: string;
|
|
19
|
+
/** Shared reading passage (HTML) above the matching board. */
|
|
20
|
+
passage?: string;
|
|
21
|
+
audioUrl?: string;
|
|
22
|
+
/** Shared cover / portrait image (not a left/right match item). */
|
|
23
|
+
imageUrl?: string;
|
|
17
24
|
rightItems: MatchingWithLinesRightItem[];
|
|
18
25
|
items: MatchingWithLinesGroupItemData[];
|
|
19
26
|
explanation?: string;
|
|
@@ -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,13 +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
|
+
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
5
|
+
import { toAnswerTheQuestionGroupImageUrls } from '../../_shared/types/answer-the-question-group.type';
|
|
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) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
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
|
+
} }) }));
|
|
47
|
+
}
|
|
4
48
|
export function AnswerTheQuestionGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
|
|
5
49
|
const items = questionData.items || [];
|
|
6
|
-
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.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) => {
|
|
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) => {
|
|
7
51
|
const userValue = userAnswers[itemIndex] || '';
|
|
8
52
|
const expectedAnswers = Array.isArray(item.expectedAnswers)
|
|
9
53
|
? item.expectedAnswers.filter((answer) => answer.trim() !== '')
|
|
10
54
|
: [];
|
|
11
|
-
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}`));
|
|
12
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] }))] }));
|
|
13
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,6 +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 || '');
|
|
54
|
+
const [audioUrl] = useState(initialData?.audioUrl || '');
|
|
55
|
+
const [imageUrl] = useState(toAnswerTheQuestionGroupImageUrls(initialData?.imageUrl));
|
|
52
56
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
53
57
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
54
58
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
@@ -61,6 +65,8 @@ export function AnswerTheQuestionGroupCreator({ initialData, onChange, externalE
|
|
|
61
65
|
const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation) => ({
|
|
62
66
|
instruction: nextInstruction,
|
|
63
67
|
...(!isContentEmpty(nextPassage) ? { passage: nextPassage.trim() } : {}),
|
|
68
|
+
...(audioUrl.trim() ? { audioUrl: audioUrl.trim() } : {}),
|
|
69
|
+
...(imageUrl.length ? { imageUrl } : {}),
|
|
64
70
|
items: nextItems,
|
|
65
71
|
explanation: nextExplanation,
|
|
66
72
|
points: sumPoints(nextItems),
|
|
@@ -77,7 +83,7 @@ export function AnswerTheQuestionGroupCreator({ initialData, onChange, externalE
|
|
|
77
83
|
previousPayloadRef.current = serialized;
|
|
78
84
|
onUnsavedChangesChange?.(true);
|
|
79
85
|
debouncedOnChange(payload);
|
|
80
|
-
}, [instruction, passage, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
86
|
+
}, [instruction, passage, audioUrl, imageUrl, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
81
87
|
const validate = () => {
|
|
82
88
|
const nextErrors = [];
|
|
83
89
|
if (!instruction.trim()) {
|
|
@@ -111,7 +117,7 @@ export function AnswerTheQuestionGroupCreator({ initialData, onChange, externalE
|
|
|
111
117
|
},
|
|
112
118
|
getFormData: () => buildPayload(),
|
|
113
119
|
};
|
|
114
|
-
}, [validationRef, instruction, passage, items, explanation]);
|
|
120
|
+
}, [validationRef, instruction, passage, audioUrl, imageUrl, items, explanation]);
|
|
115
121
|
const updateItem = (index, patch) => {
|
|
116
122
|
setItems((prev) => {
|
|
117
123
|
const next = [...prev];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fromAnswerTheQuestionGroupImageUrls, toAnswerTheQuestionGroupImageUrls, } from '../../_shared/types/answer-the-question-group.type';
|
|
1
2
|
const DEFAULT_INSTRUCTION = 'Read the text again and answer the questions.';
|
|
2
3
|
function isRecord(value) {
|
|
3
4
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -8,6 +9,13 @@ function asRecord(value) {
|
|
|
8
9
|
function asString(value, fallback = '') {
|
|
9
10
|
return typeof value === 'string' ? value : fallback;
|
|
10
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
|
+
}
|
|
11
19
|
function normalizeExpectedAnswers(value) {
|
|
12
20
|
if (!Array.isArray(value)) {
|
|
13
21
|
return [];
|
|
@@ -37,12 +45,14 @@ export function mapAnswerTheQuestionGroupItem(item, index, fallbackAnswers) {
|
|
|
37
45
|
: (normalizeExpectedAnswers(fallbackAnswers).length > 0
|
|
38
46
|
? normalizeExpectedAnswers(fallbackAnswers)
|
|
39
47
|
: ['']);
|
|
48
|
+
const imageUrl = fromAnswerTheQuestionGroupImageUrls(content.imageUrl ?? record.imageUrl);
|
|
40
49
|
return {
|
|
41
50
|
question: asString(content.question) || asString(record.question),
|
|
42
51
|
expectedAnswers,
|
|
43
52
|
isExample,
|
|
44
53
|
points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
|
|
45
54
|
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
55
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
46
56
|
};
|
|
47
57
|
}
|
|
48
58
|
export function mapQuestionToAnswerTheQuestionGroupData(question) {
|
|
@@ -56,18 +66,27 @@ export function mapQuestionToAnswerTheQuestionGroupData(question) {
|
|
|
56
66
|
: [];
|
|
57
67
|
if (Array.isArray(content.items)) {
|
|
58
68
|
const items = content.items.map((item, index) => mapAnswerTheQuestionGroupItem(item, index, fallbackAnswers[index]?.expectedAnswers));
|
|
69
|
+
const meta = asRecord(content.meta);
|
|
70
|
+
const audioUrl = asOptionalUrl(meta.audioUrl);
|
|
71
|
+
const imageUrl = toAnswerTheQuestionGroupImageUrls(meta.imageUrl);
|
|
59
72
|
return {
|
|
60
|
-
instruction: asString(
|
|
61
|
-
passage: asString(
|
|
73
|
+
instruction: asString(meta.instruction) || DEFAULT_INSTRUCTION,
|
|
74
|
+
passage: asString(meta.passage),
|
|
75
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
76
|
+
...(imageUrl.length ? { imageUrl } : {}),
|
|
62
77
|
items,
|
|
63
78
|
explanation,
|
|
64
79
|
points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
|
|
65
80
|
};
|
|
66
81
|
}
|
|
67
82
|
if (Array.isArray(answer.items)) {
|
|
83
|
+
const audioUrl = asOptionalUrl(answer.audioUrl);
|
|
84
|
+
const imageUrl = toAnswerTheQuestionGroupImageUrls(answer.imageUrl);
|
|
68
85
|
return {
|
|
69
86
|
instruction: asString(answer.instruction, DEFAULT_INSTRUCTION),
|
|
70
87
|
passage: asString(answer.passage),
|
|
88
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
89
|
+
...(imageUrl.length ? { imageUrl } : {}),
|
|
71
90
|
items: answer.items,
|
|
72
91
|
explanation,
|
|
73
92
|
points: typeof answer.points === 'number' ? answer.points : 1,
|