@tinyweb_dev/oe-exam-sdk 0.2.4 → 0.2.6
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 +19 -3
- package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.d.ts +10 -0
- package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.js +38 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -11
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.d.ts +22 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +60 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.js +75 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.d.ts +13 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +31 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.js +92 -0
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +2 -1
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +2 -2
- package/dist/components/exams/take/components/question-renderers/index.d.ts +4 -0
- package/dist/components/exams/take/components/question-renderers/index.js +4 -0
- package/dist/components/exams/take/exam-taking.utils.d.ts +1 -0
- package/dist/components/exams/take/exam-taking.utils.js +9 -1
- package/dist/components/exams/take/types.d.ts +3 -2
- package/dist/components/exams/take/utils/answer-transformers.js +28 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +73 -1
- package/dist/components/exams/take/utils/question-transformers.js +188 -0
- package/dist/components/questions/_shared/config/question-types.config.js +24 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +9 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +49 -1
- package/dist/components/questions/_shared/types/fill-in-blank-group.type.d.ts +50 -0
- package/dist/components/questions/_shared/types/fill-in-blank-group.type.js +2 -0
- package/dist/components/questions/_shared/types/index.d.ts +4 -0
- package/dist/components/questions/_shared/types/index.js +4 -0
- package/dist/components/questions/_shared/types/match-word-to-picture.type.d.ts +62 -0
- package/dist/components/questions/_shared/types/match-word-to-picture.type.js +1 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +40 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.js +1 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +42 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.js +2 -0
- package/dist/components/questions/_shared/types/write-a-short-letter.type.d.ts +4 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +6 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
- package/dist/components/questions/creator/question-type-registry.js +8 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +13 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +32 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.d.ts +1 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +42 -6
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +40 -20
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.d.ts +1 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +5 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +64 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +158 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.d.ts +7 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +30 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +20 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +99 -0
- package/dist/components/questions/types/fill-in-blank-group/index.d.ts +5 -0
- package/dist/components/questions/types/fill-in-blank-group/index.js +5 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.d.ts +4 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +102 -0
- package/dist/components/questions/types/fill-in-blank-group/register.d.ts +3 -0
- package/dist/components/questions/types/fill-in-blank-group/register.js +37 -0
- package/dist/components/questions/types/fill-in-blank-group/transform.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/transform.js +68 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +31 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.js +267 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +65 -0
- package/dist/components/questions/types/match-word-to-picture/WordBankEditor.d.ts +8 -0
- package/dist/components/questions/types/match-word-to-picture/WordBankEditor.js +15 -0
- package/dist/components/questions/types/match-word-to-picture/index.d.ts +6 -0
- package/dist/components/questions/types/match-word-to-picture/index.js +6 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.js +53 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.d.ts +4 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.js +75 -0
- package/dist/components/questions/types/match-word-to-picture/register.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/register.js +55 -0
- package/dist/components/questions/types/match-word-to-picture/transform.d.ts +11 -0
- package/dist/components/questions/types/match-word-to-picture/transform.js +43 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts +20 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +109 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +30 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +180 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +6 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +26 -0
- package/dist/components/questions/types/matching-with-lines-group/index.d.ts +4 -0
- package/dist/components/questions/types/matching-with-lines-group/index.js +4 -0
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.d.ts +8 -0
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +104 -0
- package/dist/components/questions/types/matching-with-lines-group/register.d.ts +3 -0
- package/dist/components/questions/types/matching-with-lines-group/register.js +37 -0
- package/dist/components/questions/types/matching-with-lines-group/transform.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/transform.js +44 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +25 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.js +144 -0
- package/dist/components/questions/types/true-false-correct-group/index.d.ts +5 -0
- package/dist/components/questions/types/true-false-correct-group/index.js +5 -0
- package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.d.ts +4 -0
- package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.js +79 -0
- package/dist/components/questions/types/true-false-correct-group/register.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/register.js +36 -0
- package/dist/components/questions/types/true-false-correct-group/transform.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/transform.js +49 -0
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +11 -1
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +8 -2
- package/dist/components/questions/types/write-a-short-letter/register.js +4 -0
- package/dist/components/questions/types/write-a-short-letter/transform.js +4 -0
- package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.d.ts +2 -1
- package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +2 -2
- package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +10 -0
- package/dist/components/questions/viewer/FillInBlankGroupViewer.js +14 -0
- package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.d.ts +9 -0
- package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +11 -0
- package/dist/components/questions/viewer/MatchWordToPictureViewer.d.ts +9 -0
- package/dist/components/questions/viewer/MatchWordToPictureViewer.js +8 -0
- package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.d.ts +7 -0
- package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.js +48 -0
- package/dist/components/questions/viewer/QuestionViewer.js +24 -3
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +9 -0
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -0
- package/dist/components/questions/viewer/index.d.ts +5 -0
- package/dist/components/questions/viewer/index.js +5 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
- package/dist/components/results/renderers/review-question-body-movers.js +74 -3
- package/dist/components/results/review-data.js +59 -0
- package/dist/components/themes/english-certification/EcWritingTask.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +8 -0
- package/dist/shared/lib/hooks/usePresignedFileUrl.d.ts +3 -1
- package/dist/shared/lib/hooks/usePresignedFileUrl.js +30 -12
- package/dist/shared/lib/utils/question-reverse-transform.js +142 -0
- package/dist/shared/lib/utils/question-transform.js +8 -0
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
- package/dist/shared/types/questions/fill-in-blank-group.d.ts +45 -0
- package/dist/shared/types/questions/fill-in-blank-group.js +1 -0
- package/dist/shared/types/questions/index.d.ts +4 -0
- package/dist/shared/types/questions/index.js +8 -0
- package/dist/shared/types/questions/match-word-to-picture.d.ts +35 -0
- package/dist/shared/types/questions/match-word-to-picture.js +5 -0
- package/dist/shared/types/questions/matching-with-lines-group.d.ts +39 -0
- package/dist/shared/types/questions/matching-with-lines-group.js +1 -0
- package/dist/shared/types/questions/true-false-correct-group.d.ts +39 -0
- package/dist/shared/types/questions/true-false-correct-group.js +1 -0
- package/package.json +7 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const FILL_IN_BLANK_ITEM_TYPE: "FILL_IN_BLANK";
|
|
3
|
+
export declare const FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION = "Complete the sentences.";
|
|
4
|
+
export interface FillInBlankGroupItemAnswer {
|
|
5
|
+
answers: string[][];
|
|
6
|
+
caseSensitive?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface FillInBlankGroupStudentItemAnswer {
|
|
9
|
+
answers: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface FillInBlankGroupItemData {
|
|
12
|
+
question: string;
|
|
13
|
+
answers: string[][];
|
|
14
|
+
caseSensitive?: boolean;
|
|
15
|
+
imageUrl?: string;
|
|
16
|
+
audioUrl?: string;
|
|
17
|
+
showHints?: boolean;
|
|
18
|
+
hints?: string[];
|
|
19
|
+
isExample?: boolean;
|
|
20
|
+
points: number;
|
|
21
|
+
questionNumber: number;
|
|
22
|
+
}
|
|
23
|
+
export interface FillInBlankGroupData {
|
|
24
|
+
instruction: string;
|
|
25
|
+
audioUrl?: string;
|
|
26
|
+
imageUrl?: string;
|
|
27
|
+
showHints?: boolean;
|
|
28
|
+
hints?: string[];
|
|
29
|
+
items: FillInBlankGroupItemData[];
|
|
30
|
+
explanation?: string;
|
|
31
|
+
points?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface FillInBlankGroupCreatorProps {
|
|
34
|
+
initialData?: FillInBlankGroupData;
|
|
35
|
+
onSave?: (data: FillInBlankGroupData) => void;
|
|
36
|
+
onCancel?: () => void;
|
|
37
|
+
onChange?: (data: FillInBlankGroupData) => void;
|
|
38
|
+
externalErrors?: string[];
|
|
39
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
40
|
+
validationRef?: MutableRefObject<{
|
|
41
|
+
triggerValidation: () => Promise<boolean>;
|
|
42
|
+
getFormData?: () => FillInBlankGroupData;
|
|
43
|
+
} | null>;
|
|
44
|
+
}
|
|
45
|
+
export interface FillInBlankGroupClientProps {
|
|
46
|
+
questionData: FillInBlankGroupData;
|
|
47
|
+
isReviewMode?: boolean;
|
|
48
|
+
userAnswers?: FillInBlankGroupStudentItemAnswer[];
|
|
49
|
+
onAnswerChange?: (answers: FillInBlankGroupStudentItemAnswer[]) => void;
|
|
50
|
+
}
|
|
@@ -11,4 +11,8 @@ export * from './read-and-color-objects.type';
|
|
|
11
11
|
export * from './fill-in-blank.type';
|
|
12
12
|
export * from './read-passage-and-answer-questions.type';
|
|
13
13
|
export * from './word-fill-structured-form.type';
|
|
14
|
+
export * from './true-false-correct-group.type';
|
|
15
|
+
export * from './fill-in-blank-group.type';
|
|
16
|
+
export * from './match-word-to-picture.type';
|
|
17
|
+
export * from './matching-with-lines-group.type';
|
|
14
18
|
export * from './spontaneous-qa-group.type';
|
|
@@ -11,4 +11,8 @@ export * from './read-and-color-objects.type';
|
|
|
11
11
|
export * from './fill-in-blank.type';
|
|
12
12
|
export * from './read-passage-and-answer-questions.type';
|
|
13
13
|
export * from './word-fill-structured-form.type';
|
|
14
|
+
export * from './true-false-correct-group.type';
|
|
15
|
+
export * from './fill-in-blank-group.type';
|
|
16
|
+
export * from './match-word-to-picture.type';
|
|
17
|
+
export * from './matching-with-lines-group.type';
|
|
14
18
|
export * from './spontaneous-qa-group.type';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ValidationRef } from './look-and-choose-answer.type';
|
|
3
|
+
export type { ValidationRef };
|
|
4
|
+
export declare const MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION = "Look and complete.";
|
|
5
|
+
export interface MatchWordToPictureGroup {
|
|
6
|
+
number: number;
|
|
7
|
+
questionCount: number;
|
|
8
|
+
}
|
|
9
|
+
export interface MatchWordToPictureData {
|
|
10
|
+
imageUrl: string;
|
|
11
|
+
correctAnswer: string;
|
|
12
|
+
points?: number;
|
|
13
|
+
groupNumber?: number;
|
|
14
|
+
isExample?: boolean;
|
|
15
|
+
wordBank: string[];
|
|
16
|
+
instruction?: string;
|
|
17
|
+
groups?: MatchWordToPictureGroup[];
|
|
18
|
+
explanation?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface MatchWordToPictureCreatorProps {
|
|
21
|
+
initialData?: MatchWordToPictureData;
|
|
22
|
+
onSave?: (data: MatchWordToPictureData) => void;
|
|
23
|
+
onSaveToQuestionBank?: (data: MatchWordToPictureData) => void;
|
|
24
|
+
onCancel?: () => void;
|
|
25
|
+
onChange?: (data: MatchWordToPictureData) => void;
|
|
26
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
27
|
+
externalErrors?: string[];
|
|
28
|
+
groups?: MatchWordToPictureGroup[];
|
|
29
|
+
groupConfig?: string[];
|
|
30
|
+
questionConfig?: string[];
|
|
31
|
+
questionIndexInPart?: number;
|
|
32
|
+
partId?: string;
|
|
33
|
+
validationRef?: React.MutableRefObject<ValidationRef | null>;
|
|
34
|
+
}
|
|
35
|
+
export interface MatchWordToPictureClientProps {
|
|
36
|
+
questionData: MatchWordToPictureData;
|
|
37
|
+
onSubmit?: (answer: string) => void;
|
|
38
|
+
isReviewMode?: boolean;
|
|
39
|
+
userAnswer?: string;
|
|
40
|
+
autoFillCorrectAnswers?: boolean;
|
|
41
|
+
explanation?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface MatchWordToPictureGroupItemData {
|
|
44
|
+
imageUrl: string;
|
|
45
|
+
correctAnswer: string;
|
|
46
|
+
isExample?: boolean;
|
|
47
|
+
points: number;
|
|
48
|
+
questionNumber: number;
|
|
49
|
+
}
|
|
50
|
+
export interface MatchWordToPictureGroupData {
|
|
51
|
+
instruction: string;
|
|
52
|
+
wordBank: string[];
|
|
53
|
+
items: MatchWordToPictureGroupItemData[];
|
|
54
|
+
explanation?: string;
|
|
55
|
+
points?: number;
|
|
56
|
+
}
|
|
57
|
+
export interface MatchWordToPictureGroupClientProps {
|
|
58
|
+
questionData: MatchWordToPictureGroupData;
|
|
59
|
+
isReviewMode?: boolean;
|
|
60
|
+
userAnswers?: string[];
|
|
61
|
+
onAnswerChange?: (answers: string[]) => void;
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION = 'Look and complete.';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION = "Look and match.";
|
|
3
|
+
export interface MatchingWithLinesRightItem {
|
|
4
|
+
id: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export interface MatchingWithLinesGroupItemData {
|
|
8
|
+
imageUrl: string;
|
|
9
|
+
correctAnswer: string;
|
|
10
|
+
isExample?: boolean;
|
|
11
|
+
points: number;
|
|
12
|
+
questionNumber: number;
|
|
13
|
+
}
|
|
14
|
+
export interface MatchingWithLinesGroupData {
|
|
15
|
+
title?: string;
|
|
16
|
+
instruction: string;
|
|
17
|
+
rightItems: MatchingWithLinesRightItem[];
|
|
18
|
+
items: MatchingWithLinesGroupItemData[];
|
|
19
|
+
explanation?: string;
|
|
20
|
+
points?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface MatchingWithLinesGroupCreatorProps {
|
|
23
|
+
initialData?: MatchingWithLinesGroupData;
|
|
24
|
+
onSave?: (data: MatchingWithLinesGroupData) => void;
|
|
25
|
+
onCancel?: () => void;
|
|
26
|
+
onChange?: (data: MatchingWithLinesGroupData) => void;
|
|
27
|
+
externalErrors?: string[];
|
|
28
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
29
|
+
validationRef?: MutableRefObject<{
|
|
30
|
+
triggerValidation: () => Promise<boolean>;
|
|
31
|
+
getFormData?: () => MatchingWithLinesGroupData;
|
|
32
|
+
} | null>;
|
|
33
|
+
}
|
|
34
|
+
export interface MatchingWithLinesGroupClientProps {
|
|
35
|
+
questionData: MatchingWithLinesGroupData;
|
|
36
|
+
isReviewMode?: boolean;
|
|
37
|
+
userAnswers?: string[];
|
|
38
|
+
onAnswerChange?: (index: number, rightId: string) => void;
|
|
39
|
+
onAnswersChange?: (next: string[]) => void;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION = 'Look and match.';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const TRUE_FALSE_CORRECT_ITEM_TYPE: "TRUE_FALSE_CORRECT";
|
|
3
|
+
export declare const TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION = "Tick (\u2713) the true and cross (\u2717) the false sentences. Correct the false sentences.";
|
|
4
|
+
export interface TrueFalseCorrectItemAnswer {
|
|
5
|
+
isTrue: boolean;
|
|
6
|
+
correction: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TrueFalseCorrectStudentItemAnswer {
|
|
9
|
+
isTrue: boolean | null;
|
|
10
|
+
correction: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TrueFalseCorrectGroupItemData {
|
|
13
|
+
question: string;
|
|
14
|
+
isTrue: boolean;
|
|
15
|
+
correction: string;
|
|
16
|
+
isExample?: boolean;
|
|
17
|
+
points: number;
|
|
18
|
+
questionNumber: number;
|
|
19
|
+
}
|
|
20
|
+
export interface TrueFalseCorrectGroupData {
|
|
21
|
+
instruction: string;
|
|
22
|
+
items: TrueFalseCorrectGroupItemData[];
|
|
23
|
+
explanation?: string;
|
|
24
|
+
points?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface TrueFalseCorrectGroupCreatorProps {
|
|
27
|
+
initialData?: TrueFalseCorrectGroupData;
|
|
28
|
+
onSave?: (data: TrueFalseCorrectGroupData) => void;
|
|
29
|
+
onCancel?: () => void;
|
|
30
|
+
onChange?: (data: TrueFalseCorrectGroupData) => void;
|
|
31
|
+
externalErrors?: string[];
|
|
32
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
33
|
+
validationRef?: MutableRefObject<{
|
|
34
|
+
triggerValidation: () => Promise<boolean>;
|
|
35
|
+
getFormData?: () => TrueFalseCorrectGroupData;
|
|
36
|
+
} | null>;
|
|
37
|
+
}
|
|
38
|
+
export interface TrueFalseCorrectGroupClientProps {
|
|
39
|
+
questionData: TrueFalseCorrectGroupData;
|
|
40
|
+
isReviewMode?: boolean;
|
|
41
|
+
userAnswers?: TrueFalseCorrectStudentItemAnswer[];
|
|
42
|
+
}
|
|
@@ -7,6 +7,8 @@ export interface WriteAShortLetterData {
|
|
|
7
7
|
maxWords?: number;
|
|
8
8
|
instructions?: WriteAShortLetterInstructions;
|
|
9
9
|
guideImages?: string[];
|
|
10
|
+
/** Optional HTML/text article the student must read before writing. */
|
|
11
|
+
passage?: string;
|
|
10
12
|
points?: number;
|
|
11
13
|
}
|
|
12
14
|
export interface WriteAShortLetterCreatorProps {
|
|
@@ -26,6 +28,8 @@ export interface WriteAShortLetterQuestionData {
|
|
|
26
28
|
maxWords?: number;
|
|
27
29
|
instructions?: WriteAShortLetterInstructions;
|
|
28
30
|
guideImages?: string[];
|
|
31
|
+
/** Optional HTML/text article the student must read before writing. */
|
|
32
|
+
passage?: string;
|
|
29
33
|
}
|
|
30
34
|
export interface WriteAShortLetterClientProps {
|
|
31
35
|
questionData: WriteAShortLetterQuestionData;
|
|
@@ -23,10 +23,13 @@ const IMAGE_BASED_TYPES = [
|
|
|
23
23
|
'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER',
|
|
24
24
|
'LOOK_PICTURE_FILL_WORD_HINT',
|
|
25
25
|
'FILL_IN_BLANK',
|
|
26
|
+
'FILL_IN_BLANK_GROUP',
|
|
26
27
|
'LABEL_THE_PICTURE',
|
|
27
28
|
'READ_AND_COLOR_OBJECTS',
|
|
28
29
|
'IMAGE_OBJECT_MATCHING',
|
|
29
30
|
'FILL_IN_BLANK',
|
|
31
|
+
'MATCH_WORD_TO_PICTURE',
|
|
32
|
+
'MATCHING_WITH_LINES_GROUP',
|
|
30
33
|
];
|
|
31
34
|
// Passage-based question types
|
|
32
35
|
const PASSAGE_BASED_TYPES = [
|
|
@@ -61,7 +64,10 @@ const getQuestionTypeName = (type) => {
|
|
|
61
64
|
'IMAGE_OBJECT_MATCHING': 'Ghép hình ảnh - Đối tượng',
|
|
62
65
|
'WRITE_CORRECT_VERB_FORM': 'Viết dạng động từ',
|
|
63
66
|
'FILL_IN_BLANK': 'Điền vào chỗ trống',
|
|
67
|
+
'FILL_IN_BLANK_GROUP': 'Điền chỗ trống (nhóm)',
|
|
64
68
|
'READ_PASSAGE_AND_ANSWER_QUESTIONS': 'Đọc hiểu',
|
|
69
|
+
'MATCH_WORD_TO_PICTURE': 'Nối từ với tranh',
|
|
70
|
+
'MATCHING_WITH_LINES_GROUP': 'Nối bằng đường kẻ (nhóm)',
|
|
65
71
|
};
|
|
66
72
|
return type ? typeMap[type] || type.replace(/_/g, ' ').toLowerCase() : 'Chưa xác định';
|
|
67
73
|
};
|
|
@@ -26,7 +26,10 @@ export function QuestionTypeSelector({ value, onValueChange, disabled, }) {
|
|
|
26
26
|
'ANSWER_THE_QUESTION',
|
|
27
27
|
'WORD_ORDERING',
|
|
28
28
|
'MATCH_BY_WRITING_ANSWER',
|
|
29
|
+
'MATCH_WORD_TO_PICTURE',
|
|
30
|
+
'MATCHING_WITH_LINES_GROUP',
|
|
29
31
|
'WRITE_SENTENCES',
|
|
32
|
+
'FILL_IN_BLANK_GROUP',
|
|
30
33
|
];
|
|
31
34
|
return (_jsxs(Select, { value: value, onValueChange: onValueChange, disabled: disabled, children: [_jsx(SelectTrigger, { id: "questionType", className: "mt-1", children: _jsx(SelectValue, { placeholder: "Ch\u1ECDn lo\u1EA1i c\u00E2u h\u1ECFi" }) }), _jsx(SelectContent, { children: questionTypes.map((type) => (_jsx(SelectItem, { value: type, children: getQuestionTypeLabel(type) }, type))) })] }));
|
|
32
35
|
}
|
|
@@ -13,6 +13,8 @@ import { readPassageAndAnswerQuestionsRegistration } from '../types/read-passage
|
|
|
13
13
|
import { chooseTheCorrectAnswerRegistration } from '../types/choose-the-correct-answer/register';
|
|
14
14
|
import { chooseTheCorrectAnswerGroupRegistration } from '../types/choose-the-correct-answer-group/register';
|
|
15
15
|
import { trueFalseGroupRegistration } from '../types/true-false-group/register';
|
|
16
|
+
import { trueFalseCorrectGroupRegistration } from '../types/true-false-correct-group/register';
|
|
17
|
+
import { fillInBlankGroupRegistration } from '../types/fill-in-blank-group/register';
|
|
16
18
|
import { crossOutWordGroupRegistration } from '../types/cross-out-word-group/register';
|
|
17
19
|
import { writeAShortLetterRegistration } from '../types/write-a-short-letter/register';
|
|
18
20
|
import { writeCorrectVerbFormRegistration } from '../types/write-correct-verb-form/register';
|
|
@@ -23,6 +25,8 @@ import { trueFalseRegistration } from '../types/true-false/register';
|
|
|
23
25
|
import { wordOrderingRegistration } from '../types/word-ordering/register';
|
|
24
26
|
import { wordFillParagraphRegistration } from '../types/word-fill-paragraph/register';
|
|
25
27
|
import { matchByWritingAnswerRegistration } from '../types/match-by-writing-answer/register';
|
|
28
|
+
import { matchWordToPictureRegistration } from '../types/match-word-to-picture/register';
|
|
29
|
+
import { matchingWithLinesGroupRegistration } from '../types/matching-with-lines-group/register';
|
|
26
30
|
import { writeSentencesRegistration } from '../types/write-sentences/register';
|
|
27
31
|
import { listenAndChooseFromAnswerGroupRegistration } from '../types/listen-and-choose-from-answer-group/register';
|
|
28
32
|
import { listenAndChooseObjectsInSceneRegistration } from '../types/listen-and-choose-objects-in-scene/register';
|
|
@@ -50,6 +54,8 @@ export const questionTypeRegistry = {
|
|
|
50
54
|
CHOOSE_THE_CORRECT_ANSWER: chooseTheCorrectAnswerRegistration,
|
|
51
55
|
CHOOSE_THE_CORRECT_ANSWER_GROUP: chooseTheCorrectAnswerGroupRegistration,
|
|
52
56
|
TRUE_FALSE_GROUP: trueFalseGroupRegistration,
|
|
57
|
+
TRUE_FALSE_CORRECT_GROUP: trueFalseCorrectGroupRegistration,
|
|
58
|
+
FILL_IN_BLANK_GROUP: fillInBlankGroupRegistration,
|
|
53
59
|
CROSS_OUT_WORD_GROUP: crossOutWordGroupRegistration,
|
|
54
60
|
WORD_ORDER_AND_MATCH_GROUP: wordOrderAndMatchGroupRegistration,
|
|
55
61
|
FILL_IN_BLANK: fillInBlankRegistration,
|
|
@@ -72,6 +78,8 @@ export const questionTypeRegistry = {
|
|
|
72
78
|
WORD_ORDERING: wordOrderingRegistration,
|
|
73
79
|
WORD_FILL_PARAGRAPH: wordFillParagraphRegistration, // Will need to handle viewMode mapping either here or in wrapper
|
|
74
80
|
MATCH_BY_WRITING_ANSWER: matchByWritingAnswerRegistration,
|
|
81
|
+
MATCH_WORD_TO_PICTURE: matchWordToPictureRegistration,
|
|
82
|
+
MATCHING_WITH_LINES_GROUP: matchingWithLinesGroupRegistration,
|
|
75
83
|
WRITE_SENTENCES: writeSentencesRegistration,
|
|
76
84
|
LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP: listenAndChooseFromAnswerGroupRegistration,
|
|
77
85
|
LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE: listenAndChooseObjectsInSceneRegistration,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function ChooseAnswerGroupImageUploadItem({ index, itemIndex, imageUrl, onChange, onRemove, }: {
|
|
2
|
+
index: number;
|
|
3
|
+
itemIndex: number;
|
|
4
|
+
imageUrl: string;
|
|
5
|
+
onChange: (url: string) => void;
|
|
6
|
+
onRemove: () => void;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
export declare function ChooseAnswerGroupOptionImageField({ itemIndex, optionIndex, imageUrl, onChange, }: {
|
|
9
|
+
itemIndex: number;
|
|
10
|
+
optionIndex: number;
|
|
11
|
+
imageUrl: string;
|
|
12
|
+
onChange: (url: string) => void;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Button } from '../../../../components/ui/button';
|
|
5
|
+
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
6
|
+
import { ImagePreview } from '../../../../components/ui/image-preview';
|
|
7
|
+
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
8
|
+
import { Trash2 } from 'lucide-react';
|
|
9
|
+
function resolveDisplayUrl(imageUrl, previewOverride, fetchedPreviewUrl) {
|
|
10
|
+
if (previewOverride) {
|
|
11
|
+
return previewOverride;
|
|
12
|
+
}
|
|
13
|
+
if (fetchedPreviewUrl) {
|
|
14
|
+
return fetchedPreviewUrl;
|
|
15
|
+
}
|
|
16
|
+
if (imageUrl.startsWith('http') || imageUrl.startsWith('blob:')) {
|
|
17
|
+
return imageUrl;
|
|
18
|
+
}
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
export function ChooseAnswerGroupImageUploadItem({ index, itemIndex, imageUrl, onChange, onRemove, }) {
|
|
22
|
+
const [previewOverride, setPreviewOverride] = useState('');
|
|
23
|
+
const { previewUrl: fetchedPreviewUrl } = usePresignedFileUrl(imageUrl);
|
|
24
|
+
const displayUrl = resolveDisplayUrl(imageUrl, previewOverride, fetchedPreviewUrl);
|
|
25
|
+
return (_jsxs("div", { className: "space-y-2 rounded-lg border border-gray-200 bg-gray-50/40 p-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-teal-100 text-xs font-semibold text-teal-700", children: index + 1 }), _jsx("div", { className: "flex-1", children: _jsx(FileUpload, { id: `choose-answer-group-image-${itemIndex}-${index}`, label: "", accept: "image/*", value: imageUrl, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: 5, placeholder: "Upload \u1EA3nh ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }) }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: onRemove, title: "Xo\u00E1 \u1EA3nh n\u00E0y", className: "h-8 w-8 text-gray-400 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), displayUrl && (_jsx("div", { className: "pl-8", children: _jsx(ImagePreview, { src: displayUrl, alt: `Hình ảnh ${index + 1}`, className: "h-40 w-full max-w-sm rounded-lg border border-gray-200 bg-white object-contain" }) }))] }));
|
|
26
|
+
}
|
|
27
|
+
export function ChooseAnswerGroupOptionImageField({ itemIndex, optionIndex, imageUrl, onChange, }) {
|
|
28
|
+
const [previewOverride, setPreviewOverride] = useState('');
|
|
29
|
+
const { previewUrl: fetchedPreviewUrl } = usePresignedFileUrl(imageUrl);
|
|
30
|
+
const displayUrl = resolveDisplayUrl(imageUrl, previewOverride, fetchedPreviewUrl);
|
|
31
|
+
return (_jsxs("div", { className: "flex-1 space-y-2", children: [_jsx(FileUpload, { id: `choose-answer-group-option-${itemIndex}-${optionIndex}`, label: "", accept: "image/*", value: imageUrl, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: 5, placeholder: `Upload ảnh đáp án ${optionIndex + 1}`, autoUpload: true, prefix: "questions/options" }), displayUrl && (_jsx(ImagePreview, { src: displayUrl, alt: `Đáp án ${optionIndex + 1}`, className: "h-28 w-28 rounded-lg border border-gray-200 bg-white object-contain" }))] }));
|
|
32
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ChooseTheCorrectAnswerGroupClientProps } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
2
|
-
export declare function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode, userAnswers, }: ChooseTheCorrectAnswerGroupClientProps): import("react").JSX.Element;
|
|
2
|
+
export declare function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: ChooseTheCorrectAnswerGroupClientProps): import("react").JSX.Element;
|
|
@@ -2,21 +2,57 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { BookOpen, Check, CircleHelp, Lightbulb, Star } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
|
+
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
|
+
import { getChooseAnswerGroupOptionImageSrc, toChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
7
|
+
function ClientImageItem({ url, alt }) {
|
|
8
|
+
const { previewUrl, isLoading } = usePresignedFileUrl(url);
|
|
9
|
+
const src = previewUrl ||
|
|
10
|
+
(typeof url === 'string' && (url.startsWith('http') || url.startsWith('blob:')) ? url : '');
|
|
11
|
+
if (!src && !isLoading) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return (_jsx("div", { className: "max-w-sm overflow-hidden rounded-lg border border-gray-200 bg-gray-50", children: isLoading ? (_jsx("div", { className: "flex h-36 w-48 items-center justify-center", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-teal-500" }) })) : (_jsx("img", { src: src, alt: alt || 'Image', className: "max-h-56 w-auto object-contain p-2" })) }));
|
|
15
|
+
}
|
|
5
16
|
const OPTION_LABELS = ['A', 'B', 'C', 'D', 'E', 'F'];
|
|
6
|
-
export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
|
|
17
|
+
export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
7
18
|
const items = questionData.items || [];
|
|
19
|
+
const handleSelect = (itemIndex, optionId) => {
|
|
20
|
+
const item = items[itemIndex];
|
|
21
|
+
if (isReviewMode || item?.isExample || !onAnswerChange) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const next = items.map((_, index) => userAnswers[index] ?? '');
|
|
25
|
+
next[itemIndex] = optionId;
|
|
26
|
+
onAnswerChange(next);
|
|
27
|
+
};
|
|
8
28
|
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 text-gray-800 leading-relaxed", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
|
|
9
29
|
const selectedId = userAnswers[itemIndex];
|
|
10
|
-
|
|
30
|
+
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
31
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), toChooseAnswerGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), item.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: item.options.map((option, optionIndex) => {
|
|
32
|
+
const isCorrect = option.id === item.correctAnswer;
|
|
33
|
+
const isSelected = option.id === selectedId;
|
|
34
|
+
const optionImageSrc = getChooseAnswerGroupOptionImageSrc(option);
|
|
35
|
+
return (_jsxs("button", { type: "button", disabled: !canSelect, onClick: () => handleSelect(itemIndex, option.id), className: cn('flex flex-col items-center gap-2 rounded-lg border-2 px-3 py-2.5 text-sm text-left', isReviewMode && isCorrect
|
|
36
|
+
? 'border-green-400 bg-green-50'
|
|
37
|
+
: isSelected
|
|
38
|
+
? 'border-blue-400 bg-blue-50'
|
|
39
|
+
: 'border-gray-200 bg-white', canSelect && 'cursor-pointer hover:border-blue-300', !canSelect && 'cursor-default'), children: [_jsx("span", { className: cn('flex h-7 w-7 items-center justify-center rounded-md text-xs font-bold', isReviewMode && isCorrect
|
|
40
|
+
? 'bg-green-500 text-white'
|
|
41
|
+
: isSelected
|
|
42
|
+
? 'bg-blue-500 text-white'
|
|
43
|
+
: 'bg-gray-100 text-gray-600'), children: OPTION_LABELS[optionIndex] || optionIndex + 1 }), optionImageSrc ? (_jsx(ClientImageItem, { url: optionImageSrc, alt: `Đáp án ${OPTION_LABELS[optionIndex] || optionIndex + 1}` })) : (_jsx("span", { className: "text-xs text-gray-500", children: `Đáp án ${OPTION_LABELS[optionIndex] || optionIndex + 1}` })), isReviewMode && isCorrect && _jsx(Check, { className: "h-4 w-4 text-green-600" })] }, option.id || optionIndex));
|
|
44
|
+
}) })) : (_jsx("div", { className: "space-y-2", children: item.options.map((option, optionIndex) => {
|
|
11
45
|
const isCorrect = option.id === item.correctAnswer;
|
|
12
46
|
const isSelected = option.id === selectedId;
|
|
13
|
-
return (_jsxs("
|
|
47
|
+
return (_jsxs("button", { type: "button", disabled: !canSelect, onClick: () => handleSelect(itemIndex, option.id), className: cn('flex w-full items-center gap-3 rounded-lg border-2 px-3 py-2.5 text-sm text-left', isReviewMode && isCorrect
|
|
14
48
|
? 'border-green-400 bg-green-50'
|
|
15
49
|
: isSelected
|
|
16
50
|
? 'border-blue-400 bg-blue-50'
|
|
17
|
-
: 'border-gray-200 bg-white'), children: [_jsx("span", { className: cn('flex h-7 w-7 items-center justify-center rounded-md text-xs font-bold', isReviewMode && isCorrect
|
|
51
|
+
: 'border-gray-200 bg-white', canSelect && 'cursor-pointer hover:border-blue-300', !canSelect && 'cursor-default'), children: [_jsx("span", { className: cn('flex h-7 w-7 items-center justify-center rounded-md text-xs font-bold', isReviewMode && isCorrect
|
|
18
52
|
? 'bg-green-500 text-white'
|
|
19
|
-
:
|
|
20
|
-
|
|
53
|
+
: isSelected
|
|
54
|
+
? 'bg-blue-500 text-white'
|
|
55
|
+
: 'bg-gray-100 text-gray-600'), children: OPTION_LABELS[optionIndex] || optionIndex + 1 }), _jsx("span", { className: "flex-1 text-gray-800", children: option.text || `Đáp án ${OPTION_LABELS[optionIndex]}` }), isReviewMode && isCorrect && _jsx(Check, { className: "h-4 w-4 text-green-600" })] }, option.id || optionIndex));
|
|
56
|
+
}) }))] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
21
57
|
}), 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] }))] }));
|
|
22
58
|
}
|
|
@@ -10,8 +10,10 @@ import { Card, CardContent, CardHeader, CardTitle } from '../../../../components
|
|
|
10
10
|
import { Switch } from '../../../../components/ui/switch';
|
|
11
11
|
import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
12
12
|
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
13
|
-
import { Eye, Pencil, Plus, Star, Trash2 } from 'lucide-react';
|
|
13
|
+
import { Eye, ImageIcon, Pencil, Plus, Star, Trash2, Type } from 'lucide-react';
|
|
14
14
|
import { ChooseTheCorrectAnswerGroupClient } from './ChooseTheCorrectAnswerGroupClient';
|
|
15
|
+
import { ChooseAnswerGroupImageUploadItem, ChooseAnswerGroupOptionImageField, } from './ChooseAnswerGroupImageUpload';
|
|
16
|
+
import { fromChooseAnswerGroupImageUrls, getChooseAnswerGroupOptionImageSrc, isChooseAnswerGroupOptionFilled, toChooseAnswerGroupImageUrls, toEditableChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
15
17
|
const OPTION_LABELS = ['A', 'B', 'C', 'D', 'E', 'F'];
|
|
16
18
|
function createEmptyItem(questionNumber) {
|
|
17
19
|
return {
|
|
@@ -32,20 +34,25 @@ function normalizeItems(rawItems) {
|
|
|
32
34
|
if (!Array.isArray(rawItems) || rawItems.length === 0) {
|
|
33
35
|
return [createEmptyItem(1)];
|
|
34
36
|
}
|
|
35
|
-
return rawItems.map((item, index) =>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
return rawItems.map((item, index) => {
|
|
38
|
+
const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(item.imageUrl));
|
|
39
|
+
return {
|
|
40
|
+
question: item.question || '',
|
|
41
|
+
optionType: item.optionType === 'image' ? 'image' : 'text',
|
|
42
|
+
options: Array.isArray(item.options) && item.options.length >= 2
|
|
43
|
+
? item.options.map((option, optionIndex) => ({
|
|
44
|
+
id: option.id || `opt-${optionIndex + 1}`,
|
|
45
|
+
text: option.text || '',
|
|
46
|
+
...(option.imageUrl ? { imageUrl: option.imageUrl } : {}),
|
|
47
|
+
}))
|
|
48
|
+
: createEmptyItem(index + 1).options,
|
|
49
|
+
correctAnswer: item.correctAnswer || 'opt-1',
|
|
50
|
+
isExample: item.isExample === true,
|
|
51
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
52
|
+
questionNumber: item.questionNumber || index + 1,
|
|
53
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
49
56
|
}
|
|
50
57
|
function sumPoints(items) {
|
|
51
58
|
return items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0);
|
|
@@ -102,11 +109,11 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
102
109
|
if (!item.question.trim()) {
|
|
103
110
|
nextErrors.push(`Câu ${index + 1}: nội dung câu hỏi là bắt buộc.`);
|
|
104
111
|
}
|
|
105
|
-
const filledOptions = item.options.filter((option) => option.
|
|
112
|
+
const filledOptions = item.options.filter((option) => (isChooseAnswerGroupOptionFilled(option, item.optionType)));
|
|
106
113
|
if (filledOptions.length < 2) {
|
|
107
114
|
nextErrors.push(`Câu ${index + 1}: cần ít nhất 2 đáp án.`);
|
|
108
115
|
}
|
|
109
|
-
if (!item.options.some((option) => option.id === item.correctAnswer && option.
|
|
116
|
+
if (!item.options.some((option) => (option.id === item.correctAnswer && isChooseAnswerGroupOptionFilled(option, item.optionType)))) {
|
|
110
117
|
nextErrors.push(`Câu ${index + 1}: hãy chọn đáp án đúng.`);
|
|
111
118
|
}
|
|
112
119
|
});
|
|
@@ -128,12 +135,12 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
128
135
|
const updateItem = (index, patch) => {
|
|
129
136
|
setItems((current) => current.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)));
|
|
130
137
|
};
|
|
131
|
-
const
|
|
138
|
+
const updateOption = (itemIndex, optionIndex, patch) => {
|
|
132
139
|
setItems((current) => current.map((item, index) => {
|
|
133
140
|
if (index !== itemIndex) {
|
|
134
141
|
return item;
|
|
135
142
|
}
|
|
136
|
-
const options = item.options.map((option, currentOptionIndex) => (currentOptionIndex === optionIndex ? { ...option,
|
|
143
|
+
const options = item.options.map((option, currentOptionIndex) => (currentOptionIndex === optionIndex ? { ...option, ...patch } : option));
|
|
137
144
|
return { ...item, options };
|
|
138
145
|
}));
|
|
139
146
|
};
|
|
@@ -157,7 +164,20 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
157
164
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(true), children: [_jsx(Eye, { className: "mr-2 h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] }) }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Nh\u00F3m ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "group-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "group-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: "Choose the correct answer." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage - Tu\u1EF3 ch\u1ECDn)" }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] }), _jsxs("div", { className: "rounded-xl border border-gray-200 bg-gray-50 p-4", children: [_jsx(PointsInput, { value: sumPoints(items), allowZero: true, readOnly: true }), _jsx("p", { className: "mt-2 text-xs text-gray-500", children: "T\u1ED5ng \u0111i\u1EC3m \u0111\u01B0\u1EE3c c\u1ED9ng t\u1EEB c\u00E1c c\u00E2u kh\u00F4ng ph\u1EA3i example." })] })] })] }), items.map((item, itemIndex) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0", children: [_jsxs(CardTitle, { className: "text-base", children: ["C\u00E2u ", itemIndex + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeItem(itemIndex), disabled: items.length <= 1, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg border border-amber-100 bg-amber-50 px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-amber-800", children: [_jsx(Star, { className: "h-4 w-4" }), "C\u00E2u v\u00ED d\u1EE5"] }), _jsx(Switch, { checked: item.isExample === true, onCheckedChange: (checked) => updateItem(itemIndex, {
|
|
158
165
|
isExample: checked,
|
|
159
166
|
points: checked ? 0 : (item.points || 1),
|
|
160
|
-
}) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "C\u00E2u h\u1ECFi" }), _jsx(Textarea, { value: item.question, onChange: (event) => updateItem(itemIndex, { question: event.target.value }), rows: 2, placeholder: "A place for someone to stay or live is called" })] }),
|
|
167
|
+
}) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "C\u00E2u h\u1ECFi" }), _jsx(Textarea, { value: item.question, onChange: (event) => updateItem(itemIndex, { question: event.target.value }), rows: 2, placeholder: "A place for someone to stay or live is called" })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { className: "flex items-center gap-2", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-teal-500" }), "H\u00ECnh \u1EA3nh (tu\u1EF3 ch\u1ECDn)"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
|
|
168
|
+
const nextUrls = [...toEditableChooseAnswerGroupImageUrls(item.imageUrl), ''];
|
|
169
|
+
updateItem(itemIndex, { imageUrl: nextUrls });
|
|
170
|
+
}, className: "gap-1.5 border-teal-200 text-teal-600 hover:bg-teal-50 hover:text-teal-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm h\u00ECnh \u1EA3nh"] })] }), toEditableChooseAnswerGroupImageUrls(item.imageUrl).length === 0 ? (_jsx("div", { className: "rounded-lg border border-dashed border-gray-200 bg-gray-50/50 p-3 text-xs text-gray-500", children: "Ch\u01B0a c\u00F3 h\u00ECnh \u1EA3nh. Nh\u1EA5n \"Th\u00EAm h\u00ECnh \u1EA3nh\" \u0111\u1EC3 upload \u1EA3nh minh ho\u1EA1 cho c\u00E2u h\u1ECFi." })) : (_jsx("div", { className: "space-y-3", children: toEditableChooseAnswerGroupImageUrls(item.imageUrl).map((imageUrl, imageIndex) => (_jsx(ChooseAnswerGroupImageUploadItem, { index: imageIndex, itemIndex: itemIndex, imageUrl: imageUrl, onChange: (url) => {
|
|
171
|
+
const nextUrls = toEditableChooseAnswerGroupImageUrls(item.imageUrl);
|
|
172
|
+
nextUrls[imageIndex] = url;
|
|
173
|
+
updateItem(itemIndex, { imageUrl: nextUrls });
|
|
174
|
+
}, onRemove: () => {
|
|
175
|
+
const nextUrls = toEditableChooseAnswerGroupImageUrls(item.imageUrl)
|
|
176
|
+
.filter((_, idx) => idx !== imageIndex);
|
|
177
|
+
updateItem(itemIndex, { imageUrl: fromChooseAnswerGroupImageUrls(nextUrls) });
|
|
178
|
+
} }, `${itemIndex}-${imageIndex}`))) }))] }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { children: "\u0110\u00E1p \u00E1n" }), _jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-1.5", children: [_jsx(Type, { className: `h-3.5 w-3.5 ${item.optionType === 'text' ? 'text-indigo-600' : 'text-gray-400'}` }), _jsx("span", { className: `text-xs font-medium ${item.optionType === 'text' ? 'text-indigo-600' : 'text-gray-400'}`, children: "Text" }), _jsx(Switch, { checked: item.optionType === 'image', onCheckedChange: (checked) => updateItem(itemIndex, {
|
|
179
|
+
optionType: checked ? 'image' : 'text',
|
|
180
|
+
}) }), _jsx(ImageIcon, { className: `h-3.5 w-3.5 ${item.optionType === 'image' ? 'text-indigo-600' : 'text-gray-400'}` }), _jsx("span", { className: `text-xs font-medium ${item.optionType === 'image' ? 'text-indigo-600' : 'text-gray-400'}`, children: "\u1EA2nh" })] })] }), item.options.map((option, optionIndex) => (_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("input", { type: "radio", name: `correct-${itemIndex}`, checked: item.correctAnswer === option.id, onChange: () => updateItem(itemIndex, { correctAnswer: option.id }), className: "mt-2" }), _jsx("span", { className: "mt-2 w-6 text-sm font-semibold text-gray-600", children: OPTION_LABELS[optionIndex] || optionIndex + 1 }), item.optionType === 'image' ? (_jsx(ChooseAnswerGroupOptionImageField, { itemIndex: itemIndex, optionIndex: optionIndex, imageUrl: getChooseAnswerGroupOptionImageSrc(option), onChange: (url) => updateOption(itemIndex, optionIndex, { imageUrl: url, text: '' }) })) : (_jsx(Input, { value: option.text, onChange: (event) => updateOption(itemIndex, optionIndex, { text: event.target.value }), placeholder: `Đáp án ${OPTION_LABELS[optionIndex]}` }))] }, option.id)))] }), !item.isExample && (_jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(itemIndex, {
|
|
161
181
|
points: parsePointsValue(event.target.value, true),
|
|
162
182
|
}) }))] })] }, `item-${itemIndex}`))), _jsxs(Button, { type: "button", variant: "outline", onClick: addItem, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm c\u00E2u h\u1ECFi"] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gi\u1EA3i th\u00EDch" }) }), _jsx(CardContent, { children: _jsx(Textarea, { id: "explanation", value: explanation, onChange: (event) => setExplanation(event.target.value), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", rows: 3 }) })] }), allErrors.length > 0 && (_jsx("div", { className: "rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700", children: allErrors.map((error) => (_jsx("p", { children: error }, error))) }))] }));
|
|
163
183
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ChooseTheCorrectAnswerGroupData, type ChooseTheCorrectAnswerGroupItemData } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
2
2
|
export declare function createEmptyChooseAnswerGroupItem(questionNumber: number): ChooseTheCorrectAnswerGroupItemData;
|
|
3
3
|
export declare function mapChooseAnswerGroupItem(item: unknown, index: number, fallbackAnswer?: string): ChooseTheCorrectAnswerGroupItemData;
|
|
4
4
|
export declare function mapQuestionToChooseAnswerGroupData(question: Record<string, unknown> | null | undefined): ChooseTheCorrectAnswerGroupData;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fromChooseAnswerGroupImageUrls, toChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
1
2
|
function isRecord(value) {
|
|
2
3
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
4
|
}
|
|
@@ -36,9 +37,11 @@ export function mapChooseAnswerGroupItem(item, index, fallbackAnswer) {
|
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
const optionRecord = asRecord(option);
|
|
40
|
+
const imageUrl = asString(optionRecord.imageUrl);
|
|
39
41
|
return {
|
|
40
42
|
id: asString(optionRecord.id, `opt-${optionIndex + 1}`),
|
|
41
43
|
text: asString(optionRecord.text),
|
|
44
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
42
45
|
};
|
|
43
46
|
})
|
|
44
47
|
: [
|
|
@@ -47,6 +50,7 @@ export function mapChooseAnswerGroupItem(item, index, fallbackAnswer) {
|
|
|
47
50
|
{ id: 'opt-3', text: '' },
|
|
48
51
|
];
|
|
49
52
|
const isExample = record.isExample === true;
|
|
53
|
+
const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(content.imageUrl));
|
|
50
54
|
return {
|
|
51
55
|
question: asString(content.question) || asString(record.question),
|
|
52
56
|
optionType: asString(content.optionType) === 'image' ? 'image' : 'text',
|
|
@@ -55,6 +59,7 @@ export function mapChooseAnswerGroupItem(item, index, fallbackAnswer) {
|
|
|
55
59
|
isExample,
|
|
56
60
|
points: isExample ? 0 : typeof record.points === 'number' ? record.points : 1,
|
|
57
61
|
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
62
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
58
63
|
};
|
|
59
64
|
}
|
|
60
65
|
export function mapQuestionToChooseAnswerGroupData(question) {
|