@tinyweb_dev/oe-exam-sdk 0.2.7 → 0.2.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 (25) hide show
  1. package/dist/components/exams/take/components/QuestionRenderer.js +1 -1
  2. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -4
  3. package/dist/components/exams/take/utils/question-transformers.d.ts +1 -0
  4. package/dist/components/exams/take/utils/question-transformers.js +10 -0
  5. package/dist/components/index.d.ts +1 -0
  6. package/dist/components/index.js +1 -0
  7. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +2 -0
  8. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +6 -4
  9. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +16 -4
  10. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +12 -3
  11. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +8 -4
  12. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +13 -0
  13. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +5 -0
  14. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +3 -2
  15. package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
  16. package/dist/components/shared/RichTextEditor.d.ts +4 -2
  17. package/dist/components/shared/RichTextEditor.js +34 -11
  18. package/dist/components/shared/RichTextHtml.d.ts +8 -0
  19. package/dist/components/shared/RichTextHtml.js +9 -0
  20. package/dist/shared/lib/rich-text.d.ts +3 -0
  21. package/dist/shared/lib/rich-text.js +24 -0
  22. package/dist/shared/lib/utils/question-reverse-transform.js +4 -0
  23. package/dist/shared/lib/validation/choose-correct-answer.validation.js +4 -2
  24. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +2 -0
  25. package/package.json +1 -1
@@ -55,7 +55,7 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
55
55
  }
56
56
  case 'CHOOSE_THE_CORRECT_ANSWER_GROUP': {
57
57
  const data = transformChooseTheCorrectAnswerGroup(questions);
58
- return (_jsx(MoversChooseBestAnswerRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, example: data.example, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
58
+ return (_jsx(MoversChooseBestAnswerRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl || part.audioUrl, example: data.example, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
59
59
  }
60
60
  case 'CHOOSE_CORRECT_ADJECTIVE': {
61
61
  const data = transformChooseCorrectAdjective(questions);
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cn } from '../../../../../shared/lib/utils';
4
4
  import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
5
+ import { RichTextHtml } from '../../../../../components/shared/RichTextHtml';
5
6
  import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
6
7
  import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
7
8
  import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
@@ -42,14 +43,14 @@ function normalizeRenderableText(value) {
42
43
  }
43
44
  // Example Section Component
44
45
  function ExampleSection({ example }) {
45
- return (_jsxs("div", { className: "rounded-lg border-2 border-orange-300 bg-white p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-gray-700", children: "Example:" }), _jsxs("p", { className: "mb-3 text-[19px] font-bold text-[#001590]", children: [example.questionNumber, ". ", normalizeRenderableText(example.questionText)] }), _jsx(ChooseBestAnswerImages, { imageUrl: example.imageUrl, className: "mb-3" }), example.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${example.options.length}, minmax(0, 1fr))` }, children: example.options.map((option) => {
46
+ return (_jsxs("div", { className: "rounded-lg border-2 border-orange-300 bg-white p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-gray-700", children: "Example:" }), _jsxs("div", { className: "mb-3 flex items-start gap-1.5 text-[19px] font-bold text-[#001590]", children: [_jsxs("span", { children: [example.questionNumber, "."] }), _jsx(RichTextHtml, { html: normalizeRenderableText(example.questionText), className: "flex-1 font-bold text-[#001590]" })] }), example.audioUrl && (_jsx("div", { className: "mb-3", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: example.audioUrl, compact: true, dense: true }) })), _jsx(ChooseBestAnswerImages, { imageUrl: example.imageUrl, className: "mb-3" }), example.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${example.options.length}, minmax(0, 1fr))` }, children: example.options.map((option) => {
46
47
  const isCorrect = option.id === example.correctOptionId;
47
48
  return (_jsxs("div", { className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${isCorrect ? 'border-green-500 bg-green-50' : 'border-blue-200 bg-white'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "w-40 rounded object-contain mb-2" }), _jsx("div", { className: cn("w-7 h-7 rounded-full flex items-center justify-center border-[3px] transition-all mt-auto", isCorrect
48
49
  ? "bg-green-500 border-green-500 text-white"
49
50
  : "border-gray-300 bg-white text-transparent"), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }) })] }, option.id));
50
51
  }) })) : (_jsx("div", { className: "flex flex-col gap-2 pl-2", children: example.options.map((option) => {
51
52
  const isCorrect = option.id === example.correctOptionId;
52
- return (_jsxs("div", { className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 text-left transition-colors ${isCorrect ? 'border-green-500 bg-green-50' : 'border-blue-200 bg-white'}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx("span", { className: "flex-1 text-sm text-gray-700", children: normalizeRenderableText(option.text) }), _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 rounded-full ${isCorrect ? 'border-green-500 bg-green-500' : 'border-gray-300 bg-white'}`, children: isCorrect && (_jsx("div", { className: "h-2 w-2 rounded-full bg-white" })) })] }, option.id));
53
+ return (_jsxs("div", { className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 text-left transition-colors ${isCorrect ? 'border-green-500 bg-green-50' : 'border-blue-200 bg-white'}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx(RichTextHtml, { html: normalizeRenderableText(option.text), className: "flex-1 text-sm text-gray-700" }), _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 rounded-full ${isCorrect ? 'border-green-500 bg-green-500' : 'border-gray-300 bg-white'}`, children: isCorrect && (_jsx("div", { className: "h-2 w-2 rounded-full bg-white" })) })] }, option.id));
53
54
  }) }))] }));
54
55
  }
55
56
  // Single Question Component
@@ -99,9 +100,9 @@ function QuestionItem({ question, selectedOptionIds, onSelect, isReviewMode, id,
99
100
  }
100
101
  return selectedOptionIds.includes(optionId) || correctOptionIds.includes(optionId);
101
102
  };
102
- return (_jsxs("div", { id: id, className: "mb-6", children: [_jsxs("p", { className: "mb-3 text-[19px] font-bold text-[#001590]", children: [question.questionNumber, ". ", normalizeRenderableText(question.questionText)] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })), _jsx(ChooseBestAnswerImages, { imageUrl: question.imageUrl, className: "mb-3" }), question.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "w-35 rounded object-contain mb-2" }), _jsx("div", { className: cn("w-7 h-7 rounded-full flex items-center justify-center border-[3px] transition-all mt-auto", showIndicatorMark(option.id)
103
+ return (_jsxs("div", { id: id, className: "mb-6", children: [_jsxs("div", { className: "mb-3 flex items-start gap-1.5 text-[19px] font-bold text-[#001590]", children: [_jsxs("span", { children: [question.questionNumber, "."] }), _jsx(RichTextHtml, { html: normalizeRenderableText(question.questionText), className: "flex-1 font-bold text-[#001590]" })] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })), question.audioUrl && (_jsx("div", { className: "mb-3", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: question.audioUrl, compact: true, dense: true }) })), _jsx(ChooseBestAnswerImages, { imageUrl: question.imageUrl, className: "mb-3" }), question.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "w-35 rounded object-contain mb-2" }), _jsx("div", { className: cn("w-7 h-7 rounded-full flex items-center justify-center border-[3px] transition-all mt-auto", showIndicatorMark(option.id)
103
104
  ? "bg-blue-600 border-blue-600 text-white"
104
- : "border-gray-300 bg-white text-transparent"), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }) })] }, option.id))) })) : (_jsx("div", { className: "flex flex-col gap-2 pl-2", children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 text-left transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx("span", { className: "flex-1 text-sm text-gray-700", children: normalizeRenderableText(option.text) }), _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 ${isMultipleAnswers ? 'rounded-md' : 'rounded-full'} ${getIndicatorStyle(option.id)}`, children: showIndicatorMark(option.id) && (_jsx("div", { className: `${isMultipleAnswers ? 'h-2.5 w-2.5 rounded-sm' : 'h-2 w-2 rounded-full'} bg-white` })) })] }, option.id))) }))] }));
105
+ : "border-gray-300 bg-white text-transparent"), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }) })] }, option.id))) })) : (_jsx("div", { className: "flex flex-col gap-2 pl-2", children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 text-left transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx(RichTextHtml, { html: normalizeRenderableText(option.text), className: "flex-1 text-sm text-gray-700" }), _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 ${isMultipleAnswers ? 'rounded-md' : 'rounded-full'} ${getIndicatorStyle(option.id)}`, children: showIndicatorMark(option.id) && (_jsx("div", { className: `${isMultipleAnswers ? 'h-2.5 w-2.5 rounded-sm' : 'h-2 w-2 rounded-full'} bg-white` })) })] }, option.id))) }))] }));
105
106
  }
106
107
  // Article Card for ARTICLES groupType (e.g., GN QE RWL Part 2)
107
108
  function ArticleCard({ article, label, }) {
@@ -161,6 +161,7 @@ export declare function transformChooseTheCorrectAnswerGroup(questions: ApiQuest
161
161
  questions: ChooseBestAnswerQuestion[];
162
162
  example?: import('../types').ChooseBestAnswerExample;
163
163
  instruction: string;
164
+ audioUrl?: string;
164
165
  };
165
166
  export interface ChooseAdjectiveOption {
166
167
  id: string;
@@ -411,12 +411,18 @@ export function transformChooseTheCorrectAnswerGroup(questions) {
411
411
  }
412
412
  return urls.length === 1 ? urls[0] : urls;
413
413
  };
414
+ const toAudioUrl = (value) => {
415
+ const trimmed = typeof value === 'string' ? value.trim() : '';
416
+ return trimmed !== '' ? trimmed : undefined;
417
+ };
418
+ const groupAudioUrl = toAudioUrl(content?.meta?.audioUrl);
414
419
  let example;
415
420
  if (exampleItem) {
416
421
  const exampleIndex = items.indexOf(exampleItem);
417
422
  const exOptions = mapOptions(exampleItem.content?.options, `${parent.id}-ex`);
418
423
  const exCorrect = exampleItem.correctAnswer?.answer || parentCorrect[exampleIndex] || exOptions[0]?.id || '';
419
424
  const exampleImageUrl = toQuestionImageUrl(exampleItem.content?.imageUrl);
425
+ const exampleAudioUrl = toAudioUrl(exampleItem.content?.audioUrl);
420
426
  example = {
421
427
  questionNumber: exampleItem.questionNumber || exampleIndex + 1,
422
428
  questionText: exampleItem.content?.question || '',
@@ -424,6 +430,7 @@ export function transformChooseTheCorrectAnswerGroup(questions) {
424
430
  correctOptionId: exCorrect,
425
431
  optionType: (exampleItem.content?.optionType || 'text'),
426
432
  ...(exampleImageUrl !== undefined ? { imageUrl: exampleImageUrl } : {}),
433
+ ...(exampleAudioUrl ? { audioUrl: exampleAudioUrl } : {}),
427
434
  };
428
435
  }
429
436
  const transformed = regularItems.map((item) => {
@@ -431,6 +438,7 @@ export function transformChooseTheCorrectAnswerGroup(questions) {
431
438
  const options = mapOptions(item.content?.options, `${parent.id}-item-${itemIndex}`);
432
439
  const correctId = item.correctAnswer?.answer || parentCorrect[itemIndex] || '';
433
440
  const itemImageUrl = toQuestionImageUrl(item.content?.imageUrl);
441
+ const itemAudioUrl = toAudioUrl(item.content?.audioUrl);
434
442
  return {
435
443
  id: `${parent.id}-item-${itemIndex}`,
436
444
  questionNumber: item.questionNumber || itemIndex + 1,
@@ -440,12 +448,14 @@ export function transformChooseTheCorrectAnswerGroup(questions) {
440
448
  correctOptionIds: correctId ? [correctId] : [],
441
449
  optionType: (item.content?.optionType || 'text'),
442
450
  ...(itemImageUrl !== undefined ? { imageUrl: itemImageUrl } : {}),
451
+ ...(itemAudioUrl ? { audioUrl: itemAudioUrl } : {}),
443
452
  };
444
453
  });
445
454
  return {
446
455
  questions: transformed,
447
456
  example,
448
457
  instruction: content?.meta?.instruction || 'Choose the correct answer.',
458
+ ...(groupAudioUrl ? { audioUrl: groupAudioUrl } : {}),
449
459
  };
450
460
  }
451
461
  export function transformChooseCorrectAdjective(questions) {
@@ -1,5 +1,6 @@
1
1
  export * from './CueCardEditor';
2
2
  export * from './shared/RichTextEditor';
3
+ export * from './shared/RichTextHtml';
3
4
  export * from './exams';
4
5
  export * from './themes';
5
6
  export * from './results';
@@ -1,5 +1,6 @@
1
1
  export * from './CueCardEditor';
2
2
  export * from './shared/RichTextEditor';
3
+ export * from './shared/RichTextHtml';
3
4
  export * from './exams';
4
5
  export * from './themes';
5
6
  export * from './results';
@@ -18,10 +18,12 @@ export interface ChooseTheCorrectAnswerGroupItemData {
18
18
  points: number;
19
19
  questionNumber: number;
20
20
  imageUrl?: ChooseTheCorrectAnswerGroupImageUrl;
21
+ audioUrl?: string;
21
22
  }
22
23
  export interface ChooseTheCorrectAnswerGroupData {
23
24
  instruction: string;
24
25
  passage?: string;
26
+ audioUrl?: string;
25
27
  items: ChooseTheCorrectAnswerGroupItemData[];
26
28
  explanation?: string;
27
29
  points?: number;
@@ -4,6 +4,8 @@ import { useState, useEffect, useMemo } from 'react';
4
4
  import { Check, X, CircleHelp, Lightbulb, ClipboardList, Users, BookOpen } from 'lucide-react';
5
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
6
  import { cn } from '../../../../shared/lib/utils';
7
+ import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
8
+ import { isRichTextEmpty, richTextToPlainText } from '../../../../shared/lib/rich-text';
7
9
  import { normalizeChooseAnswerCorrectAnswer, normalizeChooseAnswerOptions, optionToString } from './normalize';
8
10
  // Helper component to resolve presigned URL for each image option
9
11
  function OptionImage({ src, alt, className }) {
@@ -82,7 +84,7 @@ export function ChooseTheCorrectAnswerClient({ questionData, onSubmit, isReviewM
82
84
  const optText = optionToString(opt);
83
85
  return questionData.optionType === 'image'
84
86
  ? `Đáp án ${answerLetter}`
85
- : `${answerLetter}. ${optText}`;
87
+ : `${answerLetter}. ${richTextToPlainText(optText)}`;
86
88
  }).join(', ');
87
89
  };
88
90
  // Visibility toggles based on questionConfig (from template)
@@ -109,7 +111,7 @@ export function ChooseTheCorrectAnswerClient({ questionData, onSubmit, isReviewM
109
111
  .split('\n')
110
112
  .map((line) => `<p class="last:mb-0">${line}</p>`)
111
113
  .join(''),
112
- } })), questionData.groupImageUrl && (_jsxs("div", { className: "mt-5 relative overflow-hidden rounded-xl border border-slate-200/60 bg-slate-50 shadow-sm transition-all hover:shadow-md", children: [_jsx("div", { className: "flex items-center justify-between border-b border-slate-200/60 bg-slate-100/50 px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("span", { className: "ml-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: "T\u00E0i li\u1EC7u \u0111\u00EDnh k\u00E8m" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4 min-h-[160px]", children: _jsx(OptionImage, { src: questionData.groupImageUrl, alt: "Group illustration", className: "max-h-64 w-auto rounded-lg object-contain" }) })] }))] }))] })] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xs", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3.5", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-xs", children: _jsx(CircleHelp, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: isMultipleAnswers ? 'Câu hỏi nhiều đáp án' : 'Chọn đáp án đúng' })] }), questionData.points !== undefined && (_jsx("div", { className: "flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: _jsxs("span", { children: [questionData.points, " \u0111i\u1EC3m"] }) }))] }), _jsxs("div", { className: "p-5 space-y-4", children: [(questionData.askerName || questionData.answererName) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-gray-50 p-2.5 text-xs text-gray-600", children: [_jsx(Users, { className: "h-4 w-4 text-gray-400" }), questionData.askerName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi h\u1ECFi:" }), " ", questionData.askerName] })), questionData.askerName && questionData.answererName && _jsx("span", { children: "\u2022" }), questionData.answererName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi:" }), " ", questionData.answererName] }))] })), showQuestion && (_jsx("div", { className: "rounded-lg bg-gradient-to-r from-blue-50 to-indigo-50 p-4 border border-blue-100", children: _jsx("h3", { className: "text-base font-semibold text-gray-800 leading-relaxed", children: questionData.question || 'Câu hỏi chưa được nhập' }) })), showImage && (imagePreviewUrl || cleanImageUrl) && (_jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2 w-2 rounded-full bg-red-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-green-400" }), _jsx("span", { className: "ml-2 text-xs font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4", children: isLoading ? (_jsx("div", { className: "flex h-48 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })) : (
114
+ } })), questionData.groupImageUrl && (_jsxs("div", { className: "mt-5 relative overflow-hidden rounded-xl border border-slate-200/60 bg-slate-50 shadow-sm transition-all hover:shadow-md", children: [_jsx("div", { className: "flex items-center justify-between border-b border-slate-200/60 bg-slate-100/50 px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("span", { className: "ml-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: "T\u00E0i li\u1EC7u \u0111\u00EDnh k\u00E8m" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4 min-h-[160px]", children: _jsx(OptionImage, { src: questionData.groupImageUrl, alt: "Group illustration", className: "max-h-64 w-auto rounded-lg object-contain" }) })] }))] }))] })] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xs", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3.5", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-xs", children: _jsx(CircleHelp, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: isMultipleAnswers ? 'Câu hỏi nhiều đáp án' : 'Chọn đáp án đúng' })] }), questionData.points !== undefined && (_jsx("div", { className: "flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: _jsxs("span", { children: [questionData.points, " \u0111i\u1EC3m"] }) }))] }), _jsxs("div", { className: "p-5 space-y-4", children: [(questionData.askerName || questionData.answererName) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-gray-50 p-2.5 text-xs text-gray-600", children: [_jsx(Users, { className: "h-4 w-4 text-gray-400" }), questionData.askerName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi h\u1ECFi:" }), " ", questionData.askerName] })), questionData.askerName && questionData.answererName && _jsx("span", { children: "\u2022" }), questionData.answererName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi:" }), " ", questionData.answererName] }))] })), showQuestion && (_jsx("div", { className: "rounded-lg bg-gradient-to-r from-blue-50 to-indigo-50 p-4 border border-blue-100", children: isRichTextEmpty(questionData.question) ? (_jsx("h3", { className: "text-base font-semibold text-gray-800 leading-relaxed", children: "C\u00E2u h\u1ECFi ch\u01B0a \u0111\u01B0\u1EE3c nh\u1EADp" })) : (_jsx(RichTextHtml, { html: questionData.question, className: "text-base font-semibold text-gray-800 leading-relaxed" })) })), showImage && (imagePreviewUrl || cleanImageUrl) && (_jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2 w-2 rounded-full bg-red-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-green-400" }), _jsx("span", { className: "ml-2 text-xs font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4", children: isLoading ? (_jsx("div", { className: "flex h-48 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })) : (
113
115
  /* eslint-disable-next-line @next/next/no-img-element */
114
116
  _jsx("img", { src: imagePreviewUrl || cleanImageUrl, alt: "Question illustration", className: "max-h-56 w-auto object-contain rounded-md", onError: (e) => {
115
117
  const target = e.target;
@@ -136,11 +138,11 @@ export function ChooseTheCorrectAnswerClient({ questionData, onSubmit, isReviewM
136
138
  : 'bg-gray-200 text-gray-600'
137
139
  : isSelected
138
140
  ? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm'
139
- : 'bg-gray-100 text-gray-600 group-hover:bg-gray-200'), children: letter }), questionData.optionType === 'image' ? (_jsx("div", { className: "flex-1 flex items-center justify-center py-1", children: _jsx(OptionImage, { src: optionText, alt: `Đáp án ${letter}` }) })) : (_jsx("span", { className: cn('flex-1 text-sm font-medium', isReviewMode && isCorrectAnswer
141
+ : 'bg-gray-100 text-gray-600 group-hover:bg-gray-200'), children: letter }), questionData.optionType === 'image' ? (_jsx("div", { className: "flex-1 flex items-center justify-center py-1", children: _jsx(OptionImage, { src: optionText, alt: `Đáp án ${letter}` }) })) : (_jsx(RichTextHtml, { html: optionText, className: cn('flex-1 text-sm font-medium', isReviewMode && isCorrectAnswer
140
142
  ? 'text-green-800'
141
143
  : isReviewMode && isSelected && !isCorrectAnswer
142
144
  ? 'text-red-800'
143
- : 'text-gray-700'), children: optionText })), isReviewMode && (isCorrectAnswer || isSelected) && (_jsx("div", { className: cn('flex h-6 w-6 items-center justify-center rounded-full', isCorrectAnswer ? 'bg-green-500' : 'bg-red-500'), children: isCorrectAnswer
145
+ : 'text-gray-700') })), isReviewMode && (isCorrectAnswer || isSelected) && (_jsx("div", { className: cn('flex h-6 w-6 items-center justify-center rounded-full', isCorrectAnswer ? 'bg-green-500' : 'bg-red-500'), children: isCorrectAnswer
144
146
  ? _jsx(Check, { className: "h-4 w-4 text-white" })
145
147
  : _jsx(X, { className: "h-4 w-4 text-white" }) }))] }, index));
146
148
  })] })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-xs font-bold", children: "!" }) }), _jsx("span", { className: "text-sm text-amber-800", children: isMultipleAnswers ? 'Vui lòng chọn ít nhất một đáp án' : 'Vui lòng chọn một đáp án' })] }))] })] }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50 shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-blue-100 bg-white/50 px-4 py-3", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 text-white shadow-sm", children: _jsx(Lightbulb, { className: "h-4 w-4" }) }), _jsx("h4", { className: "text-sm font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("div", { className: "p-4", children: _jsx("p", { className: "text-sm text-gray-700 leading-relaxed", children: questionData.explanation }) })] })), isReviewMode && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4 py-3", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-slate-500 to-gray-600 text-white shadow-sm", children: _jsx(ClipboardList, { className: "h-4 w-4" }) }), _jsx("h4", { className: "text-sm font-semibold text-gray-800", children: "T\u1ED5ng k\u1EBFt" })] }), _jsxs("div", { className: "p-4 space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg bg-gray-50 p-3", children: [_jsx("span", { className: "text-sm text-gray-600", children: "\u0110\u00E1p \u00E1n c\u1EE7a b\u1EA1n:" }), _jsx("span", { className: cn('text-sm font-semibold', isAnswerCorrect ? 'text-green-600' : hasAnswered ? 'text-red-600' : 'text-gray-500'), children: formatAnswerLabel(selectedAnswer) })] }), _jsxs("div", { className: "flex items-center justify-between rounded-lg bg-green-50 p-3 border border-green-100", children: [_jsx("span", { className: "text-sm text-gray-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng:" }), _jsx("span", { className: "text-sm font-semibold text-green-600", children: formatAnswerLabel(normalizedCorrectAnswers) })] }), _jsxs("div", { className: cn('flex items-center justify-between rounded-lg p-3 border', isAnswerCorrect
@@ -14,6 +14,8 @@ import { usePresignedFileUrl, useDebouncedCallback } from '../../../../shared/li
14
14
  import { useReactHookForm } from '../../../../shared/lib/hooks/useReactHookForm';
15
15
  import { Pencil, Eye, Plus, Trash2, HelpCircle, CheckCircle2, Lightbulb, MessageSquare, ImageIcon, Type, Users, BookOpen, Star, ListChecks } from 'lucide-react';
16
16
  import { FileUpload } from '../../../../components/ui/file-upload';
17
+ import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
18
+ import { isRichTextEmpty } from '../../../../shared/lib/rich-text';
17
19
  import { ChooseTheCorrectAnswerClient } from './ChooseTheCorrectAnswerClient';
18
20
  import { normalizeChooseAnswerCorrectAnswer, normalizeChooseAnswerOptions, pickRawChooseAnswer, } from './normalize';
19
21
  import { chooseCorrectAnswerFormSchema, } from '../../../../shared/lib/validation/choose-correct-answer.validation';
@@ -410,7 +412,9 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
410
412
  : undefined })] }));
411
413
  }
412
414
  // Creator UI
413
- return (_jsxs("div", { className: "space-y-4", children: [isGrouped && effectiveGroupType === 'ARTICLES' && (_jsx(ArticlesGroupCard, { groupNumber: currentGroupNumber, articleCount: articleCount, articles: articles, onArticlesChange: setArticles, title: groupTitle, onTitleChange: setGroupTitle, isReadOnly: !isFirstInGroup, uploadIdPrefix: `choose-correct-answer-${partId ?? 'articles'}` })), isGrouped && effectiveGroupType === 'BASIC' && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `choose-correct-answer-${partId ?? 'group'}` })), isGrouped && effectiveGroupType === 'DOCUMENT' && (_jsx(DocumentGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `choose-correct-answer-${partId ?? 'document'}` })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100", children: _jsx(HelpCircle, { className: "h-4 w-4 text-gray-600" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "Ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("p", { className: "text-xs text-gray-500", children: "T\u1EA1o c\u00E2u h\u1ECFi tr\u1EAFc nghi\u1EC7m v\u1EDBi nhi\u1EC1u l\u1EF1a ch\u1ECDn" })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [headerExtra, _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: watchedOptions.filter((o) => typeof o === 'string' && o.trim()).length < 2, className: "gap-2 border-indigo-200 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(Eye, { className: "h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] })] })] }) }), _jsxs(CardContent, { className: "space-y-3 px-4 pb-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-y-2", children: [_jsx(PointsInput, { ...form.register('points', { valueAsNumber: true }), error: form.hasError('points') ? form.getFieldError('points') : undefined }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("button", { type: "button", style: { paddingBlock: 'calc(var(--spacing) * 1)' }, onClick: () => form.setValue('isExample', !watchedIsExample), className: `inline-flex items-center gap-2 rounded-md border px-3.5 text-sm font-medium transition-colors select-none ${watchedIsExample
415
+ return (_jsxs("div", { className: "space-y-4", children: [isGrouped && effectiveGroupType === 'ARTICLES' && (_jsx(ArticlesGroupCard, { groupNumber: currentGroupNumber, articleCount: articleCount, articles: articles, onArticlesChange: setArticles, title: groupTitle, onTitleChange: setGroupTitle, isReadOnly: !isFirstInGroup, uploadIdPrefix: `choose-correct-answer-${partId ?? 'articles'}` })), isGrouped && effectiveGroupType === 'BASIC' && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `choose-correct-answer-${partId ?? 'group'}` })), isGrouped && effectiveGroupType === 'DOCUMENT' && (_jsx(DocumentGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `choose-correct-answer-${partId ?? 'document'}` })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100", children: _jsx(HelpCircle, { className: "h-4 w-4 text-gray-600" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "Ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("p", { className: "text-xs text-gray-500", children: "T\u1EA1o c\u00E2u h\u1ECFi tr\u1EAFc nghi\u1EC7m v\u1EDBi nhi\u1EC1u l\u1EF1a ch\u1ECDn" })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [headerExtra, _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: watchedOptions.filter((o) => watchedOptionType === 'image'
416
+ ? typeof o === 'string' && o.trim()
417
+ : !isRichTextEmpty(o)).length < 2, className: "gap-2 border-indigo-200 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(Eye, { className: "h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] })] })] }) }), _jsxs(CardContent, { className: "space-y-3 px-4 pb-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-y-2", children: [_jsx(PointsInput, { ...form.register('points', { valueAsNumber: true }), error: form.hasError('points') ? form.getFieldError('points') : undefined }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("button", { type: "button", style: { paddingBlock: 'calc(var(--spacing) * 1)' }, onClick: () => form.setValue('isExample', !watchedIsExample), className: `inline-flex items-center gap-2 rounded-md border px-3.5 text-sm font-medium transition-colors select-none ${watchedIsExample
414
418
  ? 'border-amber-300 bg-amber-50 text-amber-700'
415
419
  : 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'}`, children: [_jsx(Star, { className: `h-4 w-4 ${watchedIsExample ? 'fill-amber-400 text-amber-400' : ''}` }), "Example"] }), _jsxs("button", { type: "button", style: { paddingBlock: 'calc(var(--spacing) * 1)' }, disabled: Boolean(multipleAnswers), onClick: () => {
416
420
  const enabled = !watchedMultipleAnswers;
@@ -422,7 +426,7 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
422
426
  form.setValue('correctAnswer', enabled ? normalized : (normalized[0] ?? -1), { shouldValidate: true });
423
427
  }, className: `inline-flex items-center gap-2 rounded-md border px-3.5 text-sm font-medium transition-colors select-none ${watchedMultipleAnswers
424
428
  ? 'border-violet-300 bg-violet-50 text-violet-700'
425
- : 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'} ${Boolean(multipleAnswers) ? 'cursor-not-allowed opacity-50' : ''}`, children: [_jsx(ListChecks, { className: "h-4 w-4" }), "Nhi\u1EC1u \u0111\u00E1p \u00E1n"] })] })] }), viewMode === 'CONVERSATION' && (_jsxs("div", { className: "space-y-3", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Users, { className: "h-4 w-4 text-indigo-500" }), "H\u1ED9i tho\u1EA1i ", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn - d\u00F9ng cho c\u00E2u h\u1ECFi d\u1EA1ng h\u1ED9i tho\u1EA1i)" })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4 rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: "askerName", className: "flex items-center gap-1 text-sm font-medium text-gray-600", children: ["T\u00EAn ng\u01B0\u1EDDi h\u1ECFi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "askerName", ...form.register('askerName'), placeholder: "VD: John, Mary, Teacher...", className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('askerName') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` }), form.hasError('askerName') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('askerName') }))] }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: "answererName", className: "flex items-center gap-1 text-sm font-medium text-gray-600", children: ["T\u00EAn ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "answererName", ...form.register('answererName'), placeholder: "VD: Tom, Anna, Student...", className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('answererName') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` }), form.hasError('answererName') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('answererName') }))] })] })] })), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('question')) && (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "question", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-indigo-500" }), _jsx("span", { className: "flex items-center gap-1", children: "C\u00E2u h\u1ECFi" }), _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(Textarea, { id: "question", ...form.register('question'), placeholder: "Nh\u1EADp c\u00E2u h\u1ECFi...", className: `min-h-[100px] border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('question') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}`, rows: 3 }), form.hasError('question') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('question') }))] })), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('imageUrl') || questionConfig.includes('imageUrl ')) && (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-indigo-500" }), "H\u00ECnh \u1EA3nh ", _jsx("span", { className: "text-sm font-normal text-gray-500" })] }), _jsx(FileUpload, { id: "question-image", label: "", accept: "image/*", value: watchedImageUrl || '', onChange: (url) => {
429
+ : 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'} ${Boolean(multipleAnswers) ? 'cursor-not-allowed opacity-50' : ''}`, children: [_jsx(ListChecks, { className: "h-4 w-4" }), "Nhi\u1EC1u \u0111\u00E1p \u00E1n"] })] })] }), viewMode === 'CONVERSATION' && (_jsxs("div", { className: "space-y-3", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Users, { className: "h-4 w-4 text-indigo-500" }), "H\u1ED9i tho\u1EA1i ", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn - d\u00F9ng cho c\u00E2u h\u1ECFi d\u1EA1ng h\u1ED9i tho\u1EA1i)" })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4 rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: "askerName", className: "flex items-center gap-1 text-sm font-medium text-gray-600", children: ["T\u00EAn ng\u01B0\u1EDDi h\u1ECFi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "askerName", ...form.register('askerName'), placeholder: "VD: John, Mary, Teacher...", className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('askerName') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` }), form.hasError('askerName') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('askerName') }))] }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: "answererName", className: "flex items-center gap-1 text-sm font-medium text-gray-600", children: ["T\u00EAn ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: "answererName", ...form.register('answererName'), placeholder: "VD: Tom, Anna, Student...", className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${form.hasError('answererName') ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` }), form.hasError('answererName') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('answererName') }))] })] })] })), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('question')) && (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "question", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-indigo-500" }), _jsx("span", { className: "flex items-center gap-1", children: "C\u00E2u h\u1ECFi" }), _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(RichTextEditor, { value: watchedQuestion, onChange: (html) => form.setValue('question', html, { shouldValidate: true }), variant: "compact", minHeightClassName: "min-h-[100px]" }), form.hasError('question') && (_jsx("p", { className: "text-sm text-red-600", children: form.getFieldError('question') }))] })), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('imageUrl') || questionConfig.includes('imageUrl ')) && (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-indigo-500" }), "H\u00ECnh \u1EA3nh ", _jsx("span", { className: "text-sm font-normal text-gray-500" })] }), _jsx(FileUpload, { id: "question-image", label: "", accept: "image/*", value: watchedImageUrl || '', onChange: (url) => {
426
430
  form.setValue('imageUrl', url, { shouldValidate: true });
427
431
  }, onPresignedUrlChange: setImagePreviewUrl, maxSize: 5, placeholder: "Upload \u1EA3nh ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }), form.hasError('imageUrl') && (_jsx("p", { className: "mt-1 text-sm text-red-600", children: form.getFieldError('imageUrl') })), displayPreviewUrl && (_jsx(ImagePreview, { src: displayPreviewUrl, alt: "Preview", className: "mt-2 h-48 w-full max-w-md rounded-lg border border-gray-200" }))] })), externalErrors && externalErrors.length > 0 && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-red-200 bg-gradient-to-r from-red-50 to-rose-50 p-4", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-red-100", children: _jsx("span", { className: "text-lg", children: "\u274C" }) }), _jsxs("div", { className: "text-sm", children: [_jsx("p", { className: "font-semibold text-red-800", children: "L\u1ED7i:" }), _jsx("ul", { className: "mt-1 list-inside list-disc text-red-700", children: externalErrors.map((error, index) => (_jsx("li", { children: error }, index))) })] })] })), form.formState.errors.options?.message && (_jsx("div", { className: "rounded-xl border border-red-200 bg-red-50 p-4", children: _jsx("p", { className: "text-sm text-red-600", children: form.formState.errors.options.message }) }))] })] }), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('options')) && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-indigo-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-indigo-100", children: _jsx(Lightbulb, { className: "h-3.5 w-3.5 text-indigo-600" }) }), _jsxs(CardTitle, { className: "text-base font-semibold text-gray-800", children: ["C\u00E1c l\u1EF1a ch\u1ECDn ", _jsxs("span", { className: "text-xs font-normal text-gray-500", children: ["(", watchedOptions.length, " \u0111\u00E1p \u00E1n)"] })] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_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 ${watchedOptionType === 'text' ? 'text-indigo-600' : 'text-gray-400'}` }), _jsx("span", { className: `text-xs font-medium ${watchedOptionType === 'text' ? 'text-indigo-600' : 'text-gray-400'}`, children: "Text" }), _jsx(Switch, { checked: watchedOptionType === 'image', disabled: true, className: watchedOptionType === 'image' ? '!bg-indigo-600 !opacity-100' : '!bg-indigo-600 !opacity-100', onCheckedChange: (checked) => {
428
432
  const newType = checked ? 'image' : 'text';
@@ -488,7 +492,11 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
488
492
  setOptionPreviewUrls((prev) => ({ ...prev, [index]: presignedUrl }));
489
493
  }, maxSize: 5, placeholder: `Upload ảnh đáp án ${optionLetter}...`, autoUpload: true, prefix: "questions/options" }), (optionPreviewUrls[index] || option) && (_jsx(OptionImagePreview, { src: optionPreviewUrls[index] || option, alt: `Đáp án ${optionLetter}` }))] })) : (
490
494
  /* Text Input Mode */
491
- _jsx(Input, { id: `option-input-${index}`, ...form.register(`options.${index}`), placeholder: `Nhập đáp án ${optionLetter}...`, className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${optionError ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` })), optionError && (_jsx("p", { className: "text-sm text-red-600", children: optionError.message })), isCorrect && (_jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-xs font-semibold text-green-700", children: [_jsx(CheckCircle2, { className: "h-3.5 w-3.5" }), watchedMultipleAnswers ? 'Đáp án đúng đã chọn' : 'Đáp án đúng'] }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
495
+ _jsx(RichTextEditor, { value: option, onChange: (html) => {
496
+ const newOptions = [...watchedOptions];
497
+ newOptions[index] = html;
498
+ form.setValue('options', newOptions, { shouldValidate: true });
499
+ }, variant: "compact", minHeightClassName: "min-h-[72px]" })), optionError && (_jsx("p", { className: "text-sm text-red-600", children: optionError.message })), isCorrect && (_jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-xs font-semibold text-green-700", children: [_jsx(CheckCircle2, { className: "h-3.5 w-3.5" }), watchedMultipleAnswers ? 'Đáp án đúng đã chọn' : 'Đáp án đúng'] }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
492
500
  if (watchedMultipleAnswers) {
493
501
  const currentAnswers = Array.isArray(form.getValues('correctAnswer'))
494
502
  ? form.getValues('correctAnswer')
@@ -543,7 +551,11 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
543
551
  setOptionPreviewUrls((prev) => ({ ...prev, [index]: presignedUrl }));
544
552
  }, maxSize: 5, placeholder: `Upload ảnh đáp án ${optionLetter}...`, autoUpload: true, prefix: "questions/options" }), (optionPreviewUrls[index] || option) && (_jsx(OptionImagePreview, { src: optionPreviewUrls[index] || option, alt: `Đáp án ${optionLetter}` }))] })) : (
545
553
  /* Text Input Mode */
546
- _jsx(Input, { id: `option-input-${index}`, ...form.register(`options.${index}`), placeholder: `Nhập đáp án ${optionLetter}...`, className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${optionError ? 'border-red-400 focus:border-red-500 focus:ring-red-200' : ''}` })), optionError && (_jsx("p", { className: "text-sm text-red-600", children: optionError.message })), isCorrect && (_jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-xs font-semibold text-green-700", children: [_jsx(CheckCircle2, { className: "h-3.5 w-3.5" }), "\u0110\u00E1p \u00E1n \u0111\u00FAng"] }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
554
+ _jsx(RichTextEditor, { value: option, onChange: (html) => {
555
+ const newOptions = [...watchedOptions];
556
+ newOptions[index] = html;
557
+ form.setValue('options', newOptions, { shouldValidate: true });
558
+ }, variant: "compact", minHeightClassName: "min-h-[72px]" })), optionError && (_jsx("p", { className: "text-sm text-red-600", children: optionError.message })), isCorrect && (_jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-xs font-semibold text-green-700", children: [_jsx(CheckCircle2, { className: "h-3.5 w-3.5" }), "\u0110\u00E1p \u00E1n \u0111\u00FAng"] }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
547
559
  form.setValue('correctAnswer', index, { shouldValidate: true });
548
560
  }, className: isCorrect ? 'text-green-600 hover:text-green-700' : 'text-gray-400 hover:text-green-600', title: "Ch\u1ECDn l\u00E0m \u0111\u00E1p \u00E1n \u0111\u00FAng", children: _jsx(CheckCircle2, { className: "h-4 w-4" }) }), watchedOptions.length > 2 && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
549
561
  const currentOptions = form.getValues('options');
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { BookOpen, Check, CircleHelp, Lightbulb, Star } from 'lucide-react';
3
+ import { BookOpen, Check, CircleHelp, Lightbulb, Star, Volume2 } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
6
  import { getChooseAnswerGroupOptionImageSrc, toChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
@@ -13,6 +13,15 @@ function ClientImageItem({ url, alt }) {
13
13
  }
14
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
15
  }
16
+ function ClientAudio({ url }) {
17
+ const { previewUrl, isLoading } = usePresignedFileUrl(url);
18
+ const src = previewUrl ||
19
+ (typeof url === 'string' && (url.startsWith('http') || url.startsWith('blob:')) ? url : '');
20
+ if (!src && !isLoading) {
21
+ return null;
22
+ }
23
+ 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
+ }
16
25
  const OPTION_LABELS = ['A', 'B', 'C', 'D', 'E', 'F'];
17
26
  export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
18
27
  const items = questionData.items || [];
@@ -25,10 +34,10 @@ export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode =
25
34
  next[itemIndex] = optionId;
26
35
  onAnswerChange(next);
27
36
  };
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) => {
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 }), 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) => {
29
38
  const selectedId = userAnswers[itemIndex];
30
39
  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) => {
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-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' }), 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) => {
32
41
  const isCorrect = option.id === item.correctAnswer;
33
42
  const isSelected = option.id === selectedId;
34
43
  const optionImageSrc = getChooseAnswerGroupOptionImageSrc(option);
@@ -8,6 +8,7 @@ import { Textarea } from '../../../../components/ui/textarea';
8
8
  import { PointsInput } from '../../../../components/ui/points-input';
9
9
  import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
10
10
  import { Switch } from '../../../../components/ui/switch';
11
+ import { FileUpload } from '../../../../components/ui/file-upload';
11
12
  import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
12
13
  import { useDebouncedCallback } from '../../../../shared/lib/hooks';
13
14
  import { Eye, ImageIcon, Pencil, Plus, Star, Trash2, Type } from 'lucide-react';
@@ -51,6 +52,7 @@ function normalizeItems(rawItems) {
51
52
  points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
52
53
  questionNumber: item.questionNumber || index + 1,
53
54
  ...(imageUrl !== undefined ? { imageUrl } : {}),
55
+ ...(item.audioUrl ? { audioUrl: item.audioUrl } : {}),
54
56
  };
55
57
  });
56
58
  }
@@ -67,6 +69,7 @@ function parsePointsValue(raw, allowZero) {
67
69
  export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
68
70
  const [instruction, setInstruction] = useState(initialData?.instruction || 'Choose the correct answer.');
69
71
  const [passage, setPassage] = useState(initialData?.passage || '');
72
+ const [audioUrl, setAudioUrl] = useState(initialData?.audioUrl || '');
70
73
  const [items, setItems] = useState(() => normalizeItems(initialData?.items));
71
74
  const [explanation, setExplanation] = useState(initialData?.explanation || '');
72
75
  const [isClientMode, setIsClientMode] = useState(false);
@@ -77,9 +80,10 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
77
80
  onChangeRef.current = onChange;
78
81
  }, [onChange]);
79
82
  const isContentEmpty = (html) => !html || html === '<p></p>' || html === '<p><br></p>' || html.trim() === '';
80
- const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation) => ({
83
+ const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation, nextAudioUrl = audioUrl) => ({
81
84
  instruction: nextInstruction,
82
85
  ...(!isContentEmpty(nextPassage) ? { passage: nextPassage.trim() } : {}),
86
+ ...(nextAudioUrl.trim() ? { audioUrl: nextAudioUrl.trim() } : {}),
83
87
  items: nextItems,
84
88
  explanation: nextExplanation,
85
89
  points: sumPoints(nextItems),
@@ -96,7 +100,7 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
96
100
  previousPayloadRef.current = serialized;
97
101
  onUnsavedChangesChange?.(true);
98
102
  debouncedOnChange(payload);
99
- }, [instruction, passage, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
103
+ }, [instruction, passage, audioUrl, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
100
104
  const validate = () => {
101
105
  const nextErrors = [];
102
106
  if (!instruction.trim()) {
@@ -161,10 +165,10 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
161
165
  return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Ch\u1EC9nh s\u1EEDa"] }) }), _jsx(ChooseTheCorrectAnswerGroupClient, { questionData: buildPayload(), isReviewMode: true })] }));
162
166
  }
163
167
  const allErrors = [...errors, ...(externalErrors || [])];
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, {
168
+ 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." })] }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: "choose-answer-group-audio", label: "Audio chung (tu\u1EF3 ch\u1ECDn)", accept: "audio/*", value: audioUrl, onChange: setAudioUrl, maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) }), _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, {
165
169
  isExample: checked,
166
170
  points: checked ? 0 : (item.points || 1),
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: () => {
171
+ }) })] }), _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" })] }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: `choose-answer-item-audio-${itemIndex}`, label: "Audio c\u00E2u h\u1ECFi (tu\u1EF3 ch\u1ECDn)", accept: "audio/*", value: item.audioUrl || '', onChange: (url) => updateItem(itemIndex, { audioUrl: url }), maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) }), _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
172
  const nextUrls = [...toEditableChooseAnswerGroupImageUrls(item.imageUrl), ''];
169
173
  updateItem(itemIndex, { imageUrl: nextUrls });
170
174
  }, 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) => {
@@ -8,6 +8,13 @@ function asRecord(value) {
8
8
  function asString(value, fallback = '') {
9
9
  return typeof value === 'string' ? value : fallback;
10
10
  }
11
+ function asOptionalUrl(value) {
12
+ if (typeof value !== 'string') {
13
+ return undefined;
14
+ }
15
+ const trimmed = value.trim();
16
+ return trimmed !== '' ? trimmed : undefined;
17
+ }
11
18
  export function createEmptyChooseAnswerGroupItem(questionNumber) {
12
19
  return {
13
20
  question: '',
@@ -51,6 +58,7 @@ export function mapChooseAnswerGroupItem(item, index, fallbackAnswer) {
51
58
  ];
52
59
  const isExample = record.isExample === true;
53
60
  const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(content.imageUrl));
61
+ const audioUrl = asOptionalUrl(content.audioUrl);
54
62
  return {
55
63
  question: asString(content.question) || asString(record.question),
56
64
  optionType: asString(content.optionType) === 'image' ? 'image' : 'text',
@@ -60,6 +68,7 @@ export function mapChooseAnswerGroupItem(item, index, fallbackAnswer) {
60
68
  points: isExample ? 0 : typeof record.points === 'number' ? record.points : 1,
61
69
  questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
62
70
  ...(imageUrl !== undefined ? { imageUrl } : {}),
71
+ ...(audioUrl ? { audioUrl } : {}),
63
72
  };
64
73
  }
65
74
  export function mapQuestionToChooseAnswerGroupData(question) {
@@ -73,18 +82,22 @@ export function mapQuestionToChooseAnswerGroupData(question) {
73
82
  : [];
74
83
  if (Array.isArray(content.items)) {
75
84
  const items = content.items.map((item, index) => mapChooseAnswerGroupItem(item, index, fallbackAnswers[index]));
85
+ const audioUrl = asOptionalUrl(asRecord(content.meta).audioUrl);
76
86
  return {
77
87
  instruction: asString(asRecord(content.meta).instruction) || 'Choose the correct answer.',
78
88
  passage: asString(asRecord(content.meta).passage),
89
+ ...(audioUrl ? { audioUrl } : {}),
79
90
  items,
80
91
  explanation,
81
92
  points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
82
93
  };
83
94
  }
84
95
  if (Array.isArray(answer.items)) {
96
+ const audioUrl = asOptionalUrl(answer.audioUrl);
85
97
  return {
86
98
  instruction: asString(answer.instruction, 'Choose the correct answer.'),
87
99
  passage: asString(answer.passage),
100
+ ...(audioUrl ? { audioUrl } : {}),
88
101
  items: answer.items.map((item, index) => mapChooseAnswerGroupItem(item, index, fallbackAnswers[index])),
89
102
  explanation,
90
103
  points: typeof answer.points === 'number' ? answer.points : undefined,
@@ -26,6 +26,7 @@ export const transformChooseTheCorrectAnswerGroup = (question) => {
26
26
  }));
27
27
  const correctAnswer = item.correctAnswer || options[0]?.id || 'opt-1';
28
28
  const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(item.imageUrl));
29
+ const audioUrl = typeof item.audioUrl === 'string' ? item.audioUrl.trim() : '';
29
30
  return {
30
31
  questionType: CHILD_QUESTION_TYPE,
31
32
  ...(item.isExample ? { isExample: true } : {}),
@@ -34,6 +35,7 @@ export const transformChooseTheCorrectAnswerGroup = (question) => {
34
35
  options,
35
36
  optionType: item.optionType || 'text',
36
37
  ...(imageUrl !== undefined ? { imageUrl } : {}),
38
+ ...(audioUrl ? { audioUrl } : {}),
37
39
  },
38
40
  correctAnswer: { answer: correctAnswer },
39
41
  points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
@@ -46,6 +48,9 @@ export const transformChooseTheCorrectAnswerGroup = (question) => {
46
48
  if (answerData?.passage && answerData.passage.trim()) {
47
49
  meta.passage = answerData.passage.trim();
48
50
  }
51
+ if (answerData?.audioUrl && answerData.audioUrl.trim()) {
52
+ meta.audioUrl = answerData.audioUrl.trim();
53
+ }
49
54
  return {
50
55
  apiContent: {
51
56
  meta,
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Check, X } from 'lucide-react';
4
4
  import { ResolvedImage } from '../../../components/common/ResolvedImage';
5
+ import { RichTextHtml } from '../../../components/shared/RichTextHtml';
5
6
  import CambridgeYlePartBanner from '../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
6
7
  import CambridgeYleInstructionBanner from '../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
7
8
  import CambridgeYleAudioPlayer from '../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
@@ -43,9 +44,9 @@ function ReviewQuestionItem({ question, selectedOptionIds, isCorrect, correctOpt
43
44
  }
44
45
  return _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 border-gray-300 bg-white ${shapeClass}` });
45
46
  };
46
- return (_jsxs("div", { className: "mb-4", children: [_jsxs("p", { className: "mb-2 text-lg font-bold text-gray-900", children: [question.questionNumber, ". ", question.questionText] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })), _jsx(ChooseBestAnswerImages, { imageUrl: question.imageUrl, className: "mb-3" }), isImageOptions ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("div", { className: `relative flex flex-col items-center gap-1 rounded-lg border-2 p-2 cursor-default ${getOptionStyle(option.id)}`, children: [_jsx("span", { className: "text-xs font-medium text-gray-600", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "h-28 w-28 rounded object-contain" }), renderIndicator(option.id)] }, option.id))) })) : (
47
+ return (_jsxs("div", { className: "mb-4", children: [_jsxs("div", { className: "mb-2 flex items-start gap-1.5 text-lg font-bold text-gray-900", children: [_jsxs("span", { children: [question.questionNumber, "."] }), _jsx(RichTextHtml, { html: question.questionText, className: "flex-1 font-bold text-gray-900" })] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })), question.audioUrl && (_jsx("div", { className: "mb-3", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: question.audioUrl, compact: true }) })), _jsx(ChooseBestAnswerImages, { imageUrl: question.imageUrl, className: "mb-3" }), isImageOptions ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("div", { className: `relative flex flex-col items-center gap-1 rounded-lg border-2 p-2 cursor-default ${getOptionStyle(option.id)}`, children: [_jsx("span", { className: "text-xs font-medium text-gray-600", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "h-28 w-28 rounded object-contain" }), renderIndicator(option.id)] }, option.id))) })) : (
47
48
  /* Options — text variant */
48
- _jsx("div", { className: "flex flex-col gap-2 pl-2", children: question.options.map((option) => (_jsxs("div", { className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 cursor-default ${getOptionStyle(option.id)}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx("span", { className: "flex-1 text-sm text-gray-700", children: option.text }), renderIndicator(option.id)] }, option.id))) }))] }));
49
+ _jsx("div", { className: "flex flex-col gap-2 pl-2", children: question.options.map((option) => (_jsxs("div", { className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 cursor-default ${getOptionStyle(option.id)}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx(RichTextHtml, { html: option.text, className: "flex-1 text-sm text-gray-700" }), renderIndicator(option.id)] }, option.id))) }))] }));
49
50
  }
50
51
  // Article Card for ARTICLES groupType (e.g., GN QE RWL Part 2)
51
52
  function ArticleCard({ article, label, }) {
@@ -40,7 +40,7 @@ export function renderDedicatedReviewBody(ctx) {
40
40
  }
41
41
  case 'CHOOSE_THE_CORRECT_ANSWER_GROUP': {
42
42
  const data = transformChooseTheCorrectAnswerGroup(apiQuestions);
43
- return (_jsx(ReviewChooseBestAnswerRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, instruction: part.instructions || data.instruction, questions: data.questions, answers: answers, isCorrectMap: isCorrectMap, correctOptionIdMap: correctOptionIdMap, example: data.example }));
43
+ return (_jsx(ReviewChooseBestAnswerRenderer, { partNumber: partNo, questionCount: questionCount, partName: part.name, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl, questions: data.questions, answers: answers, isCorrectMap: isCorrectMap, correctOptionIdMap: correctOptionIdMap, example: data.example }));
44
44
  }
45
45
  case 'CHOOSE_CORRECT_ADJECTIVE': {
46
46
  const data = transformChooseCorrectAdjective(apiQuestions);
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
- export interface RichTextEditorProps {
2
+ interface RichTextEditorProps {
3
3
  value?: string;
4
4
  onChange: (html: string) => void;
5
5
  label?: React.ReactNode;
6
6
  disabled?: boolean;
7
7
  minHeightClassName?: string;
8
+ variant?: 'default' | 'compact';
8
9
  }
9
- export declare function RichTextEditor({ value, onChange, label, disabled, minHeightClassName, }: RichTextEditorProps): React.JSX.Element;
10
+ export declare function RichTextEditor({ value, onChange, label, disabled, minHeightClassName, variant, }: RichTextEditorProps): React.JSX.Element;
11
+ export {};
@@ -3,16 +3,43 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from 'react';
4
4
  import { useEditor, EditorContent } from '@tiptap/react';
5
5
  import StarterKit from '@tiptap/starter-kit';
6
+ import Underline from '@tiptap/extension-underline';
7
+ import TextAlign from '@tiptap/extension-text-align';
8
+ import { Table, TableRow, TableCell, TableHeader } from '@tiptap/extension-table';
9
+ import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, Code, Columns3, Italic, List, ListOrdered, Minus, Quote, Redo2, RemoveFormatting, Rows3, SquareCode, Strikethrough, TableIcon, Trash2, Underline as UnderlineIcon, Undo2, } from 'lucide-react';
6
10
  const EMPTY_CONTENT = '<p></p>';
7
- const HEADING_LEVELS = [1, 2, 3];
8
- function getToolbarButtonClass(isActive) {
9
- return `rounded px-3 py-1 text-sm ${isActive ? 'bg-blue-500 text-white' : 'border bg-white text-gray-700 hover:bg-gray-100'}`;
11
+ const HEADING_LEVELS = [1, 2, 3, 4];
12
+ const EDITOR_EXTENSIONS = [
13
+ StarterKit.configure({
14
+ heading: { levels: [...HEADING_LEVELS] },
15
+ }),
16
+ Underline,
17
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
18
+ Table.configure({ resizable: false }),
19
+ TableRow,
20
+ TableHeader,
21
+ TableCell,
22
+ ];
23
+ const EDITOR_TABLE_CLASS = '[&_table]:my-2 [&_table]:w-full [&_table]:border-collapse [&_th]:border [&_td]:border [&_th]:border-slate-300 [&_td]:border-slate-300 [&_th]:bg-slate-50 [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1';
24
+ function toolbarButtonClass(isActive) {
25
+ return `inline-flex h-7 min-w-7 items-center justify-center rounded-md px-1.5 transition-colors disabled:opacity-30 ${isActive ? 'bg-indigo-600 text-white' : 'text-slate-600 hover:bg-slate-100'}`;
10
26
  }
11
- export function RichTextEditor({ value, onChange, label, disabled = false, minHeightClassName = 'min-h-[250px]', }) {
27
+ function ToolbarGroup({ children }) {
28
+ return (_jsx("div", { className: "flex items-center gap-0.5 rounded-md bg-white px-0.5 py-0.5 shadow-sm ring-1 ring-slate-200/80", children: children }));
29
+ }
30
+ function ToolbarButton({ editor, disabled, active, title, onClick, children, }) {
31
+ return (_jsx("button", { type: "button", title: title, disabled: disabled || !editor, onClick: onClick, className: toolbarButtonClass(!!active), children: children }));
32
+ }
33
+ function EditorToolbar({ editor, disabled, }) {
34
+ const iconClass = 'h-4 w-4';
35
+ return (_jsxs("div", { className: "relative flex flex-wrap items-center gap-1 rounded-t-md border-b border-slate-200 bg-slate-100 px-1.5 py-1", children: [_jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { editor: editor, disabled: disabled, title: "Ho\u00E0n t\u00E1c", onClick: () => editor?.chain().focus().undo().run(), children: _jsx(Undo2, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, title: "L\u00E0m l\u1EA1i", onClick: () => editor?.chain().focus().redo().run(), children: _jsx(Redo2, { className: iconClass, strokeWidth: 2 }) })] }), _jsx(ToolbarGroup, { children: HEADING_LEVELS.map((level) => (_jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('heading', { level }), title: `Heading ${level}`, onClick: () => editor?.chain().focus().toggleHeading({ level }).run(), children: _jsxs("span", { className: "text-[11px] font-bold", children: ["H", level] }) }, level))) }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('bold'), title: "\u0110\u1EADm", onClick: () => editor?.chain().focus().toggleBold().run(), children: _jsx(Bold, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('italic'), title: "Nghi\u00EAng", onClick: () => editor?.chain().focus().toggleItalic().run(), children: _jsx(Italic, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('underline'), title: "G\u1EA1ch ch\u00E2n", onClick: () => editor?.chain().focus().toggleUnderline().run(), children: _jsx(UnderlineIcon, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('strike'), title: "G\u1EA1ch ngang", onClick: () => editor?.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('code'), title: "Code", onClick: () => editor?.chain().focus().toggleCode().run(), children: _jsx(Code, { className: iconClass, strokeWidth: 2 }) })] }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('bulletList'), title: "Danh s\u00E1ch", onClick: () => editor?.chain().focus().toggleBulletList().run(), children: _jsx(List, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('orderedList'), title: "Danh s\u00E1ch s\u1ED1", onClick: () => editor?.chain().focus().toggleOrderedList().run(), children: _jsx(ListOrdered, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('blockquote'), title: "Tr\u00EDch d\u1EABn", onClick: () => editor?.chain().focus().toggleBlockquote().run(), children: _jsx(Quote, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive('codeBlock'), title: "Kh\u1ED1i code", onClick: () => editor?.chain().focus().toggleCodeBlock().run(), children: _jsx(SquareCode, { className: iconClass, strokeWidth: 2 }) })] }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive({ textAlign: 'left' }), title: "C\u0103n tr\u00E1i", onClick: () => editor?.chain().focus().setTextAlign('left').run(), children: _jsx(AlignLeft, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive({ textAlign: 'center' }), title: "C\u0103n gi\u1EEFa", onClick: () => editor?.chain().focus().setTextAlign('center').run(), children: _jsx(AlignCenter, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive({ textAlign: 'right' }), title: "C\u0103n ph\u1EA3i", onClick: () => editor?.chain().focus().setTextAlign('right').run(), children: _jsx(AlignRight, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, active: editor?.isActive({ textAlign: 'justify' }), title: "C\u0103n \u0111\u1EC1u", onClick: () => editor?.chain().focus().setTextAlign('justify').run(), children: _jsx(AlignJustify, { className: iconClass, strokeWidth: 2 }) })] }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { editor: editor, disabled: disabled, title: "\u0110\u01B0\u1EDDng k\u1EBB ngang", onClick: () => editor?.chain().focus().setHorizontalRule().run(), children: _jsx(Minus, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, title: "Ch\u00E8n b\u1EA3ng", onClick: () => editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), children: _jsx(TableIcon, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled || !editor?.can().addRowAfter(), title: "Th\u00EAm h\u00E0ng", onClick: () => editor?.chain().focus().addRowAfter().run(), children: _jsx(Rows3, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled || !editor?.can().addColumnAfter(), title: "Th\u00EAm c\u1ED9t", onClick: () => editor?.chain().focus().addColumnAfter().run(), children: _jsx(Columns3, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled || !editor?.can().deleteTable(), title: "X\u00F3a b\u1EA3ng", onClick: () => editor?.chain().focus().deleteTable().run(), children: _jsx(Trash2, { className: iconClass, strokeWidth: 2 }) }), _jsx(ToolbarButton, { editor: editor, disabled: disabled, title: "X\u00F3a \u0111\u1ECBnh d\u1EA1ng", onClick: () => editor?.chain().focus().unsetAllMarks().clearNodes().run(), children: _jsx(RemoveFormatting, { className: iconClass, strokeWidth: 2 }) })] })] }));
36
+ }
37
+ export function RichTextEditor({ value, onChange, label, disabled = false, minHeightClassName = 'min-h-[250px]', variant = 'default', }) {
12
38
  const [mounted, setMounted] = useState(false);
39
+ const isCompact = variant === 'compact';
13
40
  const editor = useEditor({
14
41
  immediatelyRender: false,
15
- extensions: [StarterKit],
42
+ extensions: EDITOR_EXTENSIONS,
16
43
  editable: !disabled,
17
44
  content: value || EMPTY_CONTENT,
18
45
  onUpdate: ({ editor: currentEditor }) => {
@@ -20,7 +47,7 @@ export function RichTextEditor({ value, onChange, label, disabled = false, minHe
20
47
  },
21
48
  editorProps: {
22
49
  attributes: {
23
- class: `tiptap-editor focus:outline-none ${minHeightClassName} p-4 bg-white border border-gray-200 rounded-b-md prose prose-sm max-w-none`,
50
+ class: `tiptap-editor focus:outline-none ${minHeightClassName} ${isCompact ? 'p-2' : 'p-4'} bg-white rounded-b-md prose prose-sm max-w-none ${EDITOR_TABLE_CLASS}`,
24
51
  },
25
52
  },
26
53
  });
@@ -39,9 +66,5 @@ export function RichTextEditor({ value, onChange, label, disabled = false, minHe
39
66
  if (!mounted) {
40
67
  return null;
41
68
  }
42
- return (_jsxs("div", { className: "flex w-full flex-col gap-4", children: [label ? _jsx("div", { children: label }) : null, _jsxs("div", { className: "flex flex-col rounded-md border border-gray-300 shadow-sm", children: [_jsxs("div", { className: "relative flex flex-wrap gap-2 rounded-t-md border-b border-gray-300 bg-gray-50 p-2", children: [_jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleBold().run(), disabled: disabled || !editor?.can().chain().focus().toggleBold().run(), className: `${getToolbarButtonClass(!!editor?.isActive('bold'))} font-bold`, children: "B" }), _jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleItalic().run(), disabled: disabled || !editor?.can().chain().focus().toggleItalic().run(), className: `${getToolbarButtonClass(!!editor?.isActive('italic'))} italic`, children: "I" }), _jsx("div", { className: "mx-1 h-6 w-px self-center bg-gray-300" }), HEADING_LEVELS.map((level) => (_jsxs("button", { type: "button", onClick: () => editor
43
- ?.chain()
44
- .focus()
45
- .toggleHeading({ level })
46
- .run(), disabled: disabled, className: `${getToolbarButtonClass(!!editor?.isActive('heading', { level }))} font-bold`, children: ["H", level] }, level))), _jsx("div", { className: "mx-1 h-6 w-px self-center bg-gray-300" }), _jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleBulletList().run(), disabled: disabled, className: getToolbarButtonClass(!!editor?.isActive('bulletList')), children: "\u2022 List" }), _jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleOrderedList().run(), disabled: disabled, className: getToolbarButtonClass(!!editor?.isActive('orderedList')), children: "1. List" })] }), _jsx(EditorContent, { editor: editor })] })] }));
69
+ return (_jsxs("div", { className: `flex w-full flex-col ${isCompact ? 'gap-0' : 'gap-4'}`, children: [label ? _jsx("div", { children: label }) : null, _jsxs("div", { className: "flex flex-col overflow-hidden rounded-md border border-gray-300 shadow-sm", children: [_jsx(EditorToolbar, { editor: editor, disabled: disabled }), _jsx(EditorContent, { editor: editor })] })] }));
47
70
  }
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from 'react';
2
+ interface RichTextHtmlProps {
3
+ html?: string | null;
4
+ className?: string;
5
+ fallback?: ReactNode;
6
+ }
7
+ export declare function RichTextHtml({ html, className, fallback }: RichTextHtmlProps): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,9 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from '../../shared/lib/utils';
3
+ const RICH_TEXT_CLASS = 'rich-text-html max-w-none [&_p]:m-0 [&_p+p]:mt-1 [&_h1]:mb-1 [&_h1]:mt-0 [&_h1]:text-xl [&_h1]:font-bold [&_h2]:mb-1 [&_h2]:mt-0 [&_h2]:text-lg [&_h2]:font-bold [&_h3]:mb-1 [&_h3]:mt-0 [&_h3]:text-base [&_h3]:font-bold [&_h4]:mb-1 [&_h4]:mt-0 [&_h4]:text-sm [&_h4]:font-bold [&_ul]:my-1 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-1 [&_ol]:list-decimal [&_ol]:pl-5 [&_blockquote]:my-1 [&_blockquote]:border-l-2 [&_blockquote]:border-slate-300 [&_blockquote]:pl-3 [&_pre]:my-1 [&_pre]:overflow-x-auto [&_pre]:rounded [&_pre]:bg-slate-100 [&_pre]:p-2 [&_code]:rounded [&_code]:bg-slate-100 [&_code]:px-1 [&_u]:underline [&_s]:line-through [&_table]:my-2 [&_table]:w-full [&_table]:border-collapse [&_th]:border [&_td]:border [&_th]:border-slate-300 [&_td]:border-slate-300 [&_th]:bg-slate-50 [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1 [&_hr]:my-2';
4
+ export function RichTextHtml({ html, className, fallback = null }) {
5
+ if (!html) {
6
+ return fallback ? _jsx(_Fragment, { children: fallback }) : null;
7
+ }
8
+ return (_jsx("div", { className: cn(RICH_TEXT_CLASS, className), dangerouslySetInnerHTML: { __html: html } }));
9
+ }
@@ -0,0 +1,3 @@
1
+ /** Strip HTML tags/entities so empty TipTap documents (`<p></p>`) count as blank. */
2
+ export declare function richTextToPlainText(value: string | null | undefined): string;
3
+ export declare function isRichTextEmpty(value: string | null | undefined): boolean;
@@ -0,0 +1,24 @@
1
+ const HTML_TAG_RE = /<[^>]*>/g;
2
+ const HTML_ENTITIES = {
3
+ '&nbsp;': ' ',
4
+ '&amp;': '&',
5
+ '&lt;': '<',
6
+ '&gt;': '>',
7
+ '&quot;': '"',
8
+ '&#39;': "'",
9
+ };
10
+ /** Strip HTML tags/entities so empty TipTap documents (`<p></p>`) count as blank. */
11
+ export function richTextToPlainText(value) {
12
+ if (!value)
13
+ return '';
14
+ return value
15
+ .replace(HTML_TAG_RE, ' ')
16
+ .replace(/&nbsp;|&amp;|&lt;|&gt;|&quot;|&#39;/gi, (entity) => {
17
+ return HTML_ENTITIES[entity.toLowerCase()] ?? entity;
18
+ })
19
+ .replace(/\s+/g, ' ')
20
+ .trim();
21
+ }
22
+ export function isRichTextEmpty(value) {
23
+ return richTextToPlainText(value) === '';
24
+ }
@@ -242,6 +242,7 @@ const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
242
242
  });
243
243
  const isExample = item.isExample === true;
244
244
  const imageUrl = normalizeChooseAnswerGroupImageUrl(itemContent.imageUrl);
245
+ const audioUrl = asString(itemContent.audioUrl).trim();
245
246
  return {
246
247
  question: asString(itemContent.question),
247
248
  optionType: asString(itemContent.optionType, 'text') === 'image' ? 'image' : 'text',
@@ -255,13 +256,16 @@ const reverseChooseTheCorrectAnswerGroup = (apiQuestion) => {
255
256
  points: isExample ? 0 : asNumber(item.points, 1),
256
257
  questionNumber: asNumber(item.questionNumber, index + 1),
257
258
  ...(imageUrl !== undefined ? { imageUrl } : {}),
259
+ ...(audioUrl ? { audioUrl } : {}),
258
260
  };
259
261
  });
262
+ const audioUrl = asString(asRecord(content.meta).audioUrl).trim();
260
263
  return {
261
264
  content: '',
262
265
  answer: {
263
266
  instruction: asString(asRecord(content.meta).instruction, 'Choose the correct answer.'),
264
267
  passage: asString(asRecord(content.meta).passage),
268
+ ...(audioUrl ? { audioUrl } : {}),
265
269
  items,
266
270
  explanation: asString(apiQuestion.explanation),
267
271
  points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { isRichTextEmpty } from '../../lib/rich-text';
2
3
  // Validation schema for Choose the Correct Answer form
3
4
  export const chooseCorrectAnswerFormSchema = z.object({
4
5
  question: z.string().optional(),
@@ -35,7 +36,8 @@ export const chooseCorrectAnswerFormSchema = z.object({
35
36
  })
36
37
  .pipe(z.number().min(1, 'Điểm số phải >= 1')),
37
38
  }).superRefine((data, ctx) => {
38
- const filledOptions = data.options.filter((opt) => opt.trim() !== '');
39
+ const isOptionFilled = (opt) => data.optionType === 'image' ? opt.trim() !== '' : !isRichTextEmpty(opt);
40
+ const filledOptions = data.options.filter((opt) => isOptionFilled(opt));
39
41
  if (filledOptions.length < 2) {
40
42
  ctx.addIssue({
41
43
  code: z.ZodIssueCode.custom,
@@ -47,7 +49,7 @@ export const chooseCorrectAnswerFormSchema = z.object({
47
49
  }
48
50
  // Validate each option based on type
49
51
  data.options.forEach((opt, index) => {
50
- if (opt.trim() === '') {
52
+ if (!isOptionFilled(opt)) {
51
53
  ctx.addIssue({
52
54
  code: z.ZodIssueCode.custom,
53
55
  message: data.optionType === 'image'
@@ -8,6 +8,7 @@ export interface ChooseTheCorrectAnswerGroupItemContent {
8
8
  options: ChooseTheCorrectAnswerGroupOption[];
9
9
  optionType?: 'text' | 'image';
10
10
  imageUrl?: string | string[];
11
+ audioUrl?: string;
11
12
  }
12
13
  export interface ChooseTheCorrectAnswerGroupItem {
13
14
  questionType?: 'CHOOSE_THE_CORRECT_ANSWER';
@@ -23,6 +24,7 @@ export interface ChooseTheCorrectAnswerGroupContent {
23
24
  meta?: {
24
25
  instruction?: string;
25
26
  passage?: string;
27
+ audioUrl?: string;
26
28
  };
27
29
  items: ChooseTheCorrectAnswerGroupItem[];
28
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyweb_dev/oe-exam-sdk",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Reusable OceanEdu question components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",