@tinyweb_dev/oe-exam-sdk 1.0.7 → 1.0.9

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.
Files changed (65) hide show
  1. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
  2. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
  3. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +2 -2
  4. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +5 -1
  5. package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
  6. package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
  7. package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
  8. package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
  9. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
  10. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -15
  11. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +2 -0
  12. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
  13. package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
  14. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +7 -0
  15. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +4 -1
  16. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +24 -22
  17. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +45 -58
  18. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupItemEditor.d.ts +6 -0
  19. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupItemEditor.js +145 -0
  20. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +6 -15
  21. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +5 -4
  22. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
  23. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +75 -11
  24. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
  25. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
  26. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
  27. package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
  28. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
  29. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +54 -3
  30. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +1 -1
  31. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +15 -5
  32. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +12 -2
  33. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +144 -45
  34. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
  35. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +79 -3
  36. package/dist/components/questions/types/spontaneous-qa-group/index.d.ts +1 -0
  37. package/dist/components/questions/types/spontaneous-qa-group/index.js +1 -0
  38. package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.d.ts +2 -0
  39. package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +74 -0
  40. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +1 -1
  41. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +26 -4
  42. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
  43. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +13 -3
  44. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +3 -2
  45. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
  46. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
  47. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
  48. package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
  49. package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +15 -2
  50. package/dist/components/questions/viewer/QuestionViewer.d.ts +9 -1
  51. package/dist/components/questions/viewer/QuestionViewer.js +22 -9
  52. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +4 -79
  53. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
  54. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
  55. package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
  56. package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
  57. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
  58. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
  59. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
  60. package/dist/shared/lib/utils/question-reverse-transform.js +12 -4
  61. package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
  62. package/dist/shared/types/questions/answer-the-question-group.js +5 -0
  63. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -1
  64. package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
  65. package/package.json +1 -1
@@ -13,7 +13,7 @@ export function ChooseThenAnswerGroupRenderer({ partNumber, partName, questionCo
13
13
  });
14
14
  return (_jsxs("div", { className: "w-full space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx("div", { className: "mt-4", children: _jsx(ChooseThenAnswerGroupClient, { questionData: {
15
15
  title: data.title,
16
- instruction: data.instruction,
16
+ instruction: '',
17
17
  items: data.questions.map((item) => ({
18
18
  question: item.question,
19
19
  options: item.options,
@@ -19,6 +19,8 @@ export interface AnswerTheQuestionGroupData {
19
19
  items: AnswerTheQuestionGroupItemData[];
20
20
  explanation?: string;
21
21
  points?: number;
22
+ /** Preserved from content.meta; AI | MAPPING. */
23
+ gradingType?: 'AI' | 'MAPPING';
22
24
  }
23
25
  export interface AnswerTheQuestionGroupCreatorProps {
24
26
  initialData?: AnswerTheQuestionGroupData;
@@ -36,4 +38,5 @@ export interface AnswerTheQuestionGroupClientProps {
36
38
  questionData: AnswerTheQuestionGroupData;
37
39
  isReviewMode?: boolean;
38
40
  userAnswers?: string[];
41
+ onAnswerChange?: (answers: string[]) => void;
39
42
  }
@@ -18,12 +18,12 @@ export interface ChooseTheCorrectAnswerGroupItemData {
18
18
  points: number;
19
19
  questionNumber: number;
20
20
  imageUrl?: ChooseTheCorrectAnswerGroupImageUrl;
21
- audioUrl?: string;
21
+ audioUrl?: ChooseTheCorrectAnswerGroupImageUrl;
22
22
  }
23
23
  export interface ChooseTheCorrectAnswerGroupData {
24
24
  instruction: string;
25
25
  passage?: string;
26
- audioUrl?: string;
26
+ audioUrl?: ChooseTheCorrectAnswerGroupImageUrl;
27
27
  imageUrl?: ChooseTheCorrectAnswerGroupImageUrl;
28
28
  items: ChooseTheCorrectAnswerGroupItemData[];
29
29
  explanation?: string;
@@ -11,7 +11,11 @@ export function isChooseAnswerGroupOptionFilled(option, optionType = 'text') {
11
11
  if (optionType === 'image') {
12
12
  return getChooseAnswerGroupOptionImageSrc(option) !== '';
13
13
  }
14
- return typeof option.text === 'string' && option.text.trim() !== '';
14
+ if (typeof option.text !== 'string') {
15
+ return false;
16
+ }
17
+ const text = option.text.trim();
18
+ return text !== '' && text !== '<p></p>' && text !== '<p><br></p>';
15
19
  }
16
20
  export function toChooseAnswerGroupImageUrls(value) {
17
21
  if (Array.isArray(value)) {
@@ -7,6 +7,7 @@ export interface ChooseThenAnswerOption {
7
7
  }
8
8
  export interface ChooseThenAnswerItemAnswer {
9
9
  optionId: string;
10
+ expectedAnswers?: string[];
10
11
  }
11
12
  export interface ChooseThenAnswerStudentItemAnswer {
12
13
  optionId: string;
@@ -16,6 +17,7 @@ export interface ChooseThenAnswerGroupItemData {
16
17
  question: string;
17
18
  options: ChooseThenAnswerOption[];
18
19
  optionId: string;
20
+ expectedAnswers?: string[];
19
21
  isExample?: boolean;
20
22
  points: number;
21
23
  questionNumber: number;
@@ -39,9 +41,24 @@ export interface ChooseThenAnswerGroupCreatorProps {
39
41
  getFormData?: () => ChooseThenAnswerGroupData;
40
42
  } | null>;
41
43
  }
44
+ export interface ChooseThenAnswerItemResult {
45
+ mappingCorrect?: boolean;
46
+ aiCorrect?: boolean;
47
+ partialScore?: number;
48
+ feedback?: string;
49
+ }
50
+ export interface ChooseThenAnswerGradeDetail {
51
+ itemId: string;
52
+ isCorrect?: boolean;
53
+ userAnswer?: unknown;
54
+ correctAnswer?: unknown;
55
+ partialScore?: number;
56
+ feedback?: string;
57
+ }
42
58
  export interface ChooseThenAnswerGroupClientProps {
43
59
  questionData: ChooseThenAnswerGroupData;
44
60
  isReviewMode?: boolean;
45
61
  userAnswers?: ChooseThenAnswerStudentItemAnswer[];
62
+ itemResults?: ChooseThenAnswerItemResult[];
46
63
  onAnswerChange?: (index: number, next: ChooseThenAnswerStudentItemAnswer) => void;
47
64
  }
@@ -39,4 +39,5 @@ export interface CrossOutWordGroupClientProps {
39
39
  questionData: CrossOutWordGroupData;
40
40
  isReviewMode?: boolean;
41
41
  userAnswers?: Array<CrossOutWordGroupItemAnswer | null | undefined>;
42
+ onAnswerChange?: (answers: Array<CrossOutWordGroupItemAnswer | null>) => void;
42
43
  }
@@ -39,4 +39,5 @@ export interface TrueFalseCorrectGroupClientProps {
39
39
  questionData: TrueFalseCorrectGroupData;
40
40
  isReviewMode?: boolean;
41
41
  userAnswers?: TrueFalseCorrectStudentItemAnswer[];
42
+ onAnswerChange?: (answers: TrueFalseCorrectStudentItemAnswer[]) => void;
42
43
  }
@@ -28,4 +28,5 @@ export interface TrueFalseGroupClientProps {
28
28
  questionData: TrueFalseGroupData;
29
29
  isReviewMode?: boolean;
30
30
  userAnswers?: boolean[];
31
+ onAnswerChange?: (answers: boolean[]) => void;
31
32
  }
@@ -1,2 +1,2 @@
1
1
  import type { AnswerTheQuestionGroupClientProps } from '../../_shared/types/answer-the-question-group.type';
2
- export declare function AnswerTheQuestionGroupClient({ questionData, isReviewMode, userAnswers, }: AnswerTheQuestionGroupClientProps): import("react").JSX.Element;
2
+ export declare function AnswerTheQuestionGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: AnswerTheQuestionGroupClientProps): import("react").JSX.Element;
@@ -3,36 +3,30 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { BookOpen, CircleHelp, Lightbulb, Sparkles, Star, Volume2 } from 'lucide-react';
4
4
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
5
5
  import { toAnswerTheQuestionGroupImageUrls } from '../../_shared/types/answer-the-question-group.type';
6
- function 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) {
6
+ function isAbsoluteMediaUrl(value) {
14
7
  return (value.startsWith('http://')
15
8
  || value.startsWith('https://')
16
9
  || value.startsWith('blob:')
17
10
  || value.startsWith('data:'));
18
11
  }
12
+ function isPublicAssetPath(value) {
13
+ return value.startsWith('/images/') || value.startsWith('/_next/');
14
+ }
19
15
  function toPresignKey(value) {
20
16
  const trimmed = value.trim();
21
- if (isPlayableAudioUrl(trimmed)) {
17
+ if (isAbsoluteMediaUrl(trimmed) || isPublicAssetPath(trimmed)) {
22
18
  return trimmed;
23
19
  }
24
20
  return trimmed.replace(/^\/+/, '');
25
21
  }
26
22
  function MediaPreview({ src, kind = 'image', alt }) {
27
- const fileKey = kind === 'audio' ? toPresignKey(src) : src;
23
+ const fileKey = toPresignKey(src);
28
24
  const { previewUrl, isLoading } = usePresignedFileUrl(fileKey);
29
25
  if (isLoading) {
30
26
  return _jsx("div", { className: "h-16 w-16 animate-pulse rounded-md bg-slate-100" });
31
27
  }
32
28
  const url = previewUrl
33
- || (kind === 'audio'
34
- ? (isPlayableAudioUrl(fileKey) ? fileKey : '')
35
- : (isDisplayableMediaUrl(src) ? src : ''));
29
+ || (isAbsoluteMediaUrl(fileKey) || isPublicAssetPath(fileKey) ? fileKey : '');
36
30
  if (!url) {
37
31
  return null;
38
32
  }
@@ -45,13 +39,17 @@ function MediaPreview({ src, kind = 'image', alt }) {
45
39
  target.src = '/images/placeholder-image.svg';
46
40
  } }) }));
47
41
  }
48
- export function AnswerTheQuestionGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
42
+ export function AnswerTheQuestionGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
49
43
  const items = questionData.items || [];
50
44
  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) => {
51
45
  const userValue = userAnswers[itemIndex] || '';
52
46
  const expectedAnswers = Array.isArray(item.expectedAnswers)
53
47
  ? item.expectedAnswers.filter((answer) => answer.trim() !== '')
54
48
  : [];
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-3 py-2", 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-3", 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}`));
49
+ 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-3 py-2", 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-3", 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' }), isReviewMode || item.isExample || !onAnswerChange ? (userValue ? (_jsx("div", { className: "rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-700", children: userValue })) : null) : (_jsx("textarea", { value: userValue, onChange: (event) => {
50
+ const next = items.map((_, index) => userAnswers[index] || '');
51
+ next[itemIndex] = event.target.value;
52
+ onAnswerChange(next);
53
+ }, placeholder: "Nh\u1EADp c\u00E2u tr\u1EA3 l\u1EDDi...", rows: 2, className: "w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-800 outline-none ring-blue-200 focus:border-blue-400 focus:ring-2" })), 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" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })) : isReviewMode && !userValue ? (_jsx("div", { className: "rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-500", children: "Ch\u01B0a c\u00F3 \u0111\u00E1p \u00E1n m\u1EABu" })) : null] })] }, `${item.questionNumber}-${itemIndex}`));
56
54
  }), 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] }))] }));
57
55
  }
@@ -60,6 +60,7 @@ export function AnswerTheQuestionGroupCreator({ initialData, onChange, externalE
60
60
  const [errors, setErrors] = useState([]);
61
61
  const onChangeRef = useRef(onChange);
62
62
  const previousPayloadRef = useRef('');
63
+ const gradingTypeRef = useRef(initialData?.gradingType);
63
64
  useEffect(() => {
64
65
  onChangeRef.current = onChange;
65
66
  }, [onChange]);
@@ -71,6 +72,7 @@ export function AnswerTheQuestionGroupCreator({ initialData, onChange, externalE
71
72
  items: nextItems,
72
73
  explanation: nextExplanation,
73
74
  points: sumPoints(nextItems),
75
+ ...(gradingTypeRef.current ? { gradingType: gradingTypeRef.current } : {}),
74
76
  });
75
77
  const debouncedOnChange = useDebouncedCallback((payload) => {
76
78
  onChangeRef.current?.(payload);
@@ -77,6 +77,7 @@ export function mapQuestionToAnswerTheQuestionGroupData(question) {
77
77
  items,
78
78
  explanation,
79
79
  points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
80
+ ...(meta.gradingType === 'MAPPING' ? { gradingType: 'MAPPING' } : {}),
80
81
  };
81
82
  }
82
83
  if (Array.isArray(answer.items)) {
@@ -90,6 +91,7 @@ export function mapQuestionToAnswerTheQuestionGroupData(question) {
90
91
  items: answer.items,
91
92
  explanation,
92
93
  points: typeof answer.points === 'number' ? answer.points : 1,
94
+ ...(answer.gradingType === 'MAPPING' ? { gradingType: 'MAPPING' } : {}),
93
95
  };
94
96
  }
95
97
  return {
@@ -1,7 +1,12 @@
1
1
  import { fromAnswerTheQuestionGroupImageUrls, toAnswerTheQuestionGroupImageUrls, } from '../../_shared/types/answer-the-question-group.type';
2
+ import { resolveAnswerTheQuestionGroupGradingType } from '../../../../shared/types/questions/answer-the-question-group';
2
3
  const CHILD_QUESTION_TYPE = 'ANSWER_THE_QUESTION';
3
- const GRADING_TYPE = 'AI';
4
4
  const DEFAULT_INSTRUCTION = 'Read the text again and answer the questions.';
5
+ function readGradingType(question) {
6
+ const answer = question.answer;
7
+ const content = question.content;
8
+ return resolveAnswerTheQuestionGroupGradingType(answer?.gradingType ?? content?.meta?.gradingType);
9
+ }
5
10
  function normalizeExpectedAnswers(value) {
6
11
  if (!Array.isArray(value)) {
7
12
  return [];
@@ -26,6 +31,8 @@ function isContentEmpty(html) {
26
31
  export const transformAnswerTheQuestionGroup = (question) => {
27
32
  const answerData = question.answer;
28
33
  const items = Array.isArray(answerData?.items) ? answerData.items : [];
34
+ const gradingType = readGradingType(question);
35
+ const isAiGraded = gradingType === 'AI';
29
36
  if (items.length === 0) {
30
37
  const emptyAudioUrl = typeof answerData?.audioUrl === 'string' ? answerData.audioUrl.trim() : '';
31
38
  return {
@@ -33,8 +40,8 @@ export const transformAnswerTheQuestionGroup = (question) => {
33
40
  meta: {
34
41
  instruction: answerData?.instruction || DEFAULT_INSTRUCTION,
35
42
  ...(emptyAudioUrl ? { audioUrl: emptyAudioUrl } : {}),
36
- gradingType: GRADING_TYPE,
37
- isAiGraded: true,
43
+ gradingType,
44
+ isAiGraded,
38
45
  },
39
46
  items: [],
40
47
  },
@@ -66,8 +73,8 @@ export const transformAnswerTheQuestionGroup = (question) => {
66
73
  ...(!isContentEmpty(passage) ? { passage } : {}),
67
74
  ...(audioUrl ? { audioUrl } : {}),
68
75
  ...(imageUrl.length ? { imageUrl } : {}),
69
- gradingType: GRADING_TYPE,
70
- isAiGraded: true,
76
+ gradingType,
77
+ isAiGraded,
71
78
  },
72
79
  items: apiItems,
73
80
  },
@@ -5,6 +5,13 @@ export declare function ChooseAnswerGroupImageUploadItem({ index, itemIndex, ima
5
5
  onChange: (url: string) => void;
6
6
  onRemove: () => void;
7
7
  }): import("react").JSX.Element;
8
+ export declare function ChooseAnswerGroupAudioUploadItem({ index, itemIndex, audioUrl, onChange, onRemove, }: {
9
+ index: number;
10
+ itemIndex: number;
11
+ audioUrl: string;
12
+ onChange: (url: string) => void;
13
+ onRemove: () => void;
14
+ }): import("react").JSX.Element;
8
15
  export declare function ChooseAnswerGroupOptionImageField({ itemIndex, optionIndex, imageUrl, onChange, }: {
9
16
  itemIndex: number;
10
17
  optionIndex: number;
@@ -5,7 +5,7 @@ import { Button } from '../../../../components/ui/button';
5
5
  import { FileUpload } from '../../../../components/ui/file-upload';
6
6
  import { ImagePreview } from '../../../../components/ui/image-preview';
7
7
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
8
- import { ImageIcon, Trash2 } from 'lucide-react';
8
+ import { ImageIcon, Trash2, Volume2 } from 'lucide-react';
9
9
  function resolveDisplayUrl(imageUrl, previewOverride, fetchedPreviewUrl) {
10
10
  if (previewOverride) {
11
11
  return previewOverride;
@@ -24,6 +24,9 @@ export function ChooseAnswerGroupImageUploadItem({ index, itemIndex, imageUrl, o
24
24
  const displayUrl = resolveDisplayUrl(imageUrl, previewOverride, fetchedPreviewUrl);
25
25
  return (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-gray-200 bg-white p-1.5", children: [_jsx("div", { className: "flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-md border border-gray-100 bg-gray-50", children: displayUrl ? (_jsx(ImagePreview, { src: displayUrl, alt: `Hình ảnh ${index + 1}`, className: "h-12 w-12 rounded-md object-contain" })) : (_jsx(ImageIcon, { className: "h-4 w-4 text-gray-300" })) }), _jsx("div", { className: "min-w-0 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-7 w-7 shrink-0 text-gray-400 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }));
26
26
  }
27
+ export function ChooseAnswerGroupAudioUploadItem({ index, itemIndex, audioUrl, onChange, onRemove, }) {
28
+ return (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-gray-200 bg-white p-1.5", children: [_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-indigo-50", children: _jsx(Volume2, { className: "h-3.5 w-3.5 text-indigo-500" }) }), _jsx("div", { className: "min-w-0 flex-1", children: _jsx(FileUpload, { id: `choose-answer-group-audio-${itemIndex}-${index}`, label: "", accept: "audio/*", value: audioUrl, onChange: onChange, maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: onRemove, title: "Xo\u00E1 audio n\u00E0y", className: "h-7 w-7 shrink-0 text-gray-400 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }));
29
+ }
27
30
  export function ChooseAnswerGroupOptionImageField({ itemIndex, optionIndex, imageUrl, onChange, }) {
28
31
  const [previewOverride, setPreviewOverride] = useState('');
29
32
  const { previewUrl: fetchedPreviewUrl } = usePresignedFileUrl(imageUrl);
@@ -1,8 +1,10 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { BookOpen, Check, CircleHelp, Lightbulb, Star, Volume2 } from 'lucide-react';
3
+ import { BookOpen, Check, CircleHelp, Lightbulb, Star, Volume2, X } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
+ import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
7
+ import { isRichTextEmpty } from '../../../../shared/lib/rich-text';
6
8
  import { getChooseAnswerGroupOptionImageSrc, toChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
7
9
  function ClientImageItem({ url, alt }) {
8
10
  const { previewUrl, isLoading } = usePresignedFileUrl(url);
@@ -22,7 +24,21 @@ function ClientAudio({ url }) {
22
24
  }
23
25
  return (_jsxs("div", { className: "flex items-center gap-3 rounded-xl border border-indigo-100 bg-indigo-50/70 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 shrink-0 text-indigo-600" }), isLoading ? (_jsx("div", { className: "h-8 flex-1 animate-pulse rounded-md bg-indigo-100" })) : (_jsx("audio", { controls: true, className: "h-9 w-full", src: src, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." }))] }));
24
26
  }
25
- const OPTION_LABELS = ['A', 'B', 'C', 'D', 'E', 'F'];
27
+ function optionLetter(index) {
28
+ return String.fromCharCode(65 + index);
29
+ }
30
+ function reviewOptionTone(isReviewMode, isCorrect, isSelected) {
31
+ if (isReviewMode && isCorrect) {
32
+ return { border: 'border-green-400 bg-green-50', badge: 'bg-green-500 text-white' };
33
+ }
34
+ if (isReviewMode && isSelected) {
35
+ return { border: 'border-red-400 bg-red-50', badge: 'bg-red-500 text-white' };
36
+ }
37
+ if (isSelected) {
38
+ return { border: 'border-blue-400 bg-blue-50', badge: 'bg-blue-500 text-white' };
39
+ }
40
+ return { border: 'border-gray-200 bg-white', badge: 'bg-gray-100 text-gray-600' };
41
+ }
26
42
  export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
27
43
  const items = questionData.items || [];
28
44
  const handleSelect = (itemIndex, optionId) => {
@@ -34,34 +50,20 @@ export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode =
34
50
  next[itemIndex] = optionId;
35
51
  onAnswerChange(next);
36
52
  };
37
- return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), questionData.audioUrl && _jsx(ClientAudio, { url: questionData.audioUrl }), toChooseAnswerGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh nhóm (${imageIndex + 1})` }, `group-image-${imageIndex}`))) })), questionData.passage && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none text-gray-800 leading-relaxed", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
53
+ 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 })), toChooseAnswerGroupImageUrls(questionData.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `group-audio-${audioIndex}`))), toChooseAnswerGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh nhóm (${imageIndex + 1})` }, `group-image-${imageIndex}`))) })), questionData.passage && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "overflow-x-auto", children: _jsx(RichTextHtml, { html: questionData.passage, className: "text-gray-800 leading-relaxed" }) })] })), items.map((item, itemIndex) => {
38
54
  const selectedId = userAnswers[itemIndex];
39
55
  const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
40
- return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", 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-3", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), item.audioUrl && _jsx(ClientAudio, { url: item.audioUrl }), toChooseAnswerGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), item.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: item.options.map((option, optionIndex) => {
56
+ 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-3 py-2", 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-3", children: [!isRichTextEmpty(item.question) && (_jsx(RichTextHtml, { html: item.question, className: "text-sm font-semibold leading-relaxed text-gray-800" })), toChooseAnswerGroupImageUrls(item.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `${item.questionNumber}-audio-${audioIndex}`))), 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) => {
41
57
  const isCorrect = option.id === item.correctAnswer;
42
58
  const isSelected = option.id === selectedId;
59
+ const tone = reviewOptionTone(isReviewMode, isCorrect, isSelected);
43
60
  const optionImageSrc = getChooseAnswerGroupOptionImageSrc(option);
44
- 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
45
- ? 'border-green-400 bg-green-50'
46
- : isSelected
47
- ? 'border-blue-400 bg-blue-50'
48
- : '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
49
- ? 'bg-green-500 text-white'
50
- : isSelected
51
- ? 'bg-blue-500 text-white'
52
- : '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));
61
+ 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', tone.border, 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', tone.badge), children: optionLetter(optionIndex) }), optionImageSrc ? (_jsx(ClientImageItem, { url: optionImageSrc, alt: `Đáp án ${optionLetter(optionIndex)}` })) : (_jsx("span", { className: "text-xs text-gray-500", children: `Đáp án ${optionLetter(optionIndex)}` })), isReviewMode && isCorrect ? (_jsx(Check, { className: "h-4 w-4 text-green-600" })) : isReviewMode && isSelected ? (_jsx(X, { className: "h-4 w-4 text-red-600" })) : null] }, option.id || optionIndex));
53
62
  }) })) : (_jsx("div", { className: "space-y-2", children: item.options.map((option, optionIndex) => {
54
63
  const isCorrect = option.id === item.correctAnswer;
55
64
  const isSelected = option.id === selectedId;
56
- 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
57
- ? 'border-green-400 bg-green-50'
58
- : isSelected
59
- ? 'border-blue-400 bg-blue-50'
60
- : '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
61
- ? 'bg-green-500 text-white'
62
- : isSelected
63
- ? 'bg-blue-500 text-white'
64
- : '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));
65
+ const tone = reviewOptionTone(isReviewMode, isCorrect, isSelected);
66
+ 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', tone.border, 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', tone.badge), children: optionLetter(optionIndex) }), isRichTextEmpty(option.text) ? (_jsx("span", { className: "flex-1 text-gray-800", children: `Đáp án ${optionLetter(optionIndex)}` })) : (_jsx(RichTextHtml, { html: option.text, className: "flex-1 text-gray-800" })), isReviewMode && isCorrect ? (_jsx(Check, { className: "h-4 w-4 text-green-600" })) : isReviewMode && isSelected ? (_jsx(X, { className: "h-4 w-4 text-red-600" })) : null] }, option.id || optionIndex));
65
67
  }) }))] })] }, `${item.questionNumber}-${itemIndex}`));
66
68
  }), 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] }))] }));
67
69
  }