@tinyweb_dev/oe-exam-sdk 1.0.0 → 1.0.2

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 (60) hide show
  1. package/dist/api/exam-taking.types.d.ts +1 -0
  2. package/dist/components/exams/ExamPreviewDialog.js +4 -2
  3. package/dist/components/exams/take/components/QuestionRenderer.js +2 -2
  4. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +3 -1
  5. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +5 -2
  6. package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +13 -20
  7. package/dist/components/exams/take/exam-taking.utils.d.ts +3 -2
  8. package/dist/components/exams/take/exam-taking.utils.js +3 -0
  9. package/dist/components/exams/take/utils/question-transformers.d.ts +11 -3
  10. package/dist/components/exams/take/utils/question-transformers.js +45 -2
  11. package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.d.ts +6 -2
  12. package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +26 -5
  13. package/dist/components/questions/_shared/types/fill-in-blank.type.d.ts +9 -0
  14. package/dist/components/questions/_shared/types/question-group.type.d.ts +11 -0
  15. package/dist/components/questions/_shared/types/question-group.type.js +4 -1
  16. package/dist/components/questions/_shared/types/spontaneous-qa.type.d.ts +7 -0
  17. package/dist/components/questions/_shared/types/word-fill-structured-form.type.d.ts +1 -1
  18. package/dist/components/questions/groups/ImageLabelGroupCard.d.ts +15 -0
  19. package/dist/components/questions/groups/ImageLabelGroupCard.js +42 -0
  20. package/dist/components/questions/groups/ImageLabelWordBank.d.ts +10 -0
  21. package/dist/components/questions/groups/ImageLabelWordBank.js +17 -0
  22. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +33 -9
  23. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +6 -2
  24. package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +28 -5
  25. package/dist/components/questions/types/fill-in-blank/register.js +5 -0
  26. package/dist/components/questions/types/fill-in-blank/transform.js +4 -1
  27. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.d.ts +11 -0
  28. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +27 -0
  29. package/dist/components/questions/types/spontaneous-qa/SpontaneousQAClient.d.ts +2 -0
  30. package/dist/components/questions/types/spontaneous-qa/SpontaneousQAClient.js +15 -0
  31. package/dist/components/questions/types/spontaneous-qa/map-spontaneous-qa-data.d.ts +2 -0
  32. package/dist/components/questions/types/spontaneous-qa/map-spontaneous-qa-data.js +33 -0
  33. package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.d.ts +2 -2
  34. package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.js +15 -5
  35. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +39 -15
  36. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +35 -12
  37. package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +2 -10
  38. package/dist/components/questions/types/word-fill-structured-form/transform.js +2 -1
  39. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +51 -33
  40. package/dist/components/questions/viewer/QuestionViewer.js +12 -68
  41. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +3 -1
  42. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +4 -2
  43. package/dist/components/results/renderers/review-question-body-dedicated.js +2 -2
  44. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +12 -3
  45. package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +13 -20
  46. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +8 -0
  47. package/dist/shared/constants/routes.d.ts +2 -2
  48. package/dist/shared/constants/routes.js +3 -3
  49. package/dist/shared/lib/rich-text.d.ts +4 -0
  50. package/dist/shared/lib/rich-text.js +25 -0
  51. package/dist/shared/lib/stores/examQuestionStore.d.ts +4 -0
  52. package/dist/shared/lib/utils/question-transform-types.d.ts +4 -0
  53. package/dist/shared/lib/utils/question-transform.js +1 -1
  54. package/dist/shared/lib/utils/wfsf-blank-match.d.ts +9 -0
  55. package/dist/shared/lib/utils/wfsf-blank-match.js +60 -0
  56. package/package.json +1 -1
  57. package/dist/components/themes/cambridge-yle/assets/images/bg-title-old.png +0 -0
  58. package/dist/components/themes/cambridge-yle/assets/images/border-old.png +0 -0
  59. package/dist/components/themes/cambridge-yle/assets/images/close_old.png +0 -0
  60. package/dist/components/themes/cambridge-yle/assets/images/frame-old.png +0 -0
@@ -22,7 +22,7 @@ export interface WordFillStructuredFormData {
22
22
  audioUrl?: string;
23
23
  audioUrls?: string[];
24
24
  content: string;
25
- answers: Record<string, string>;
25
+ answers: Record<string, string[]>;
26
26
  /**
27
27
  * letter = square 1-char box; word = single-word underline; phrase = multi-word; translate = LLM grading.
28
28
  * Default when omitted: word.
@@ -0,0 +1,15 @@
1
+ import type { ImageLabelData } from '../_shared/types/question-group.type';
2
+ export interface ImageLabelGroupCardProps {
3
+ groupNumber: number;
4
+ imageLabels: ImageLabelData[];
5
+ onImageLabelsChange: (items: ImageLabelData[]) => void;
6
+ title?: string;
7
+ onTitleChange?: (value: string) => void;
8
+ isReadOnly?: boolean;
9
+ uploadIdPrefix?: string;
10
+ }
11
+ /**
12
+ * Editor for groupType IMAGE_LABEL: repeatable image + label items
13
+ * that render as a horizontal word bank for students.
14
+ */
15
+ export declare function ImageLabelGroupCard({ groupNumber, imageLabels, onImageLabelsChange, title, onTitleChange, isReadOnly, uploadIdPrefix, }: ImageLabelGroupCardProps): import("react").JSX.Element;
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ImagePreview } from '../../../components/ui/image-preview';
4
+ import { Input } from '../../../components/ui/input';
5
+ import { Label } from '../../../components/ui/label';
6
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../components/ui/card';
7
+ import { FileUpload } from '../../../components/ui/file-upload';
8
+ import { Button } from '../../../components/ui/button';
9
+ import { ImageIcon, Layers, PlusCircle, Trash2, Type } from 'lucide-react';
10
+ import { usePresignedFileUrl } from '../../../shared/lib/hooks';
11
+ import { ImageLabelWordBank } from './ImageLabelWordBank';
12
+ function ItemImagePreview({ src, alt }) {
13
+ const { previewUrl } = usePresignedFileUrl(src);
14
+ if (!previewUrl)
15
+ return null;
16
+ return (_jsx(ImagePreview, { src: previewUrl, alt: alt, className: "mt-2 h-28 w-full rounded-lg border border-gray-200 object-contain" }));
17
+ }
18
+ /**
19
+ * Editor for groupType IMAGE_LABEL: repeatable image + label items
20
+ * that render as a horizontal word bank for students.
21
+ */
22
+ export function ImageLabelGroupCard({ groupNumber, imageLabels, onImageLabelsChange, title = '', onTitleChange, isReadOnly = false, uploadIdPrefix = 'image-label-group', }) {
23
+ const displayItems = imageLabels.length > 0
24
+ ? imageLabels
25
+ : [{ imageUrl: '', label: '' }];
26
+ const handleFieldChange = (index, field, value) => {
27
+ if (isReadOnly)
28
+ return;
29
+ onImageLabelsChange(displayItems.map((item, i) => (i === index ? { ...item, [field]: value } : item)));
30
+ };
31
+ const handleAdd = () => {
32
+ if (isReadOnly)
33
+ return;
34
+ onImageLabelsChange([...displayItems, { imageUrl: '', label: '' }]);
35
+ };
36
+ const handleDelete = (index) => {
37
+ if (isReadOnly)
38
+ return;
39
+ onImageLabelsChange(displayItems.filter((_, i) => i !== index));
40
+ };
41
+ return (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-indigo-100/50", children: [_jsx("div", { className: "h-1.5 bg-gradient-to-r from-indigo-500 via-blue-500 to-sky-500" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-500 to-blue-600 shadow-md", children: _jsx(Layers, { className: "h-5 w-5 text-white" }) }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(CardTitle, { className: "text-lg font-bold text-gray-900", children: "Ng\u00E2n h\u00E0ng \u1EA3nh + t\u1EEB" }), _jsxs("span", { className: "inline-flex items-center rounded-full border border-indigo-300 bg-indigo-100 px-2.5 py-0.5 text-xs font-semibold text-indigo-700", children: ["Nh\u00F3m ", groupNumber] }), _jsxs("span", { className: "inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-blue-600", children: [displayItems.length, " m\u1EE5c"] })] }), _jsx("p", { className: "text-sm text-gray-500", children: "M\u1ED7i m\u1EE5c g\u1ED3m 1 h\u00ECnh v\u00E0 1 nh\u00E3n \u2014 h\u1ECDc sinh th\u1EA5y h\u00E0ng \u1EA3nh + t\u1EEB ph\u00EDa tr\u00EAn c\u00E2u h\u1ECFi." })] })] }) }), _jsxs(CardContent, { className: "space-y-5", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: `${uploadIdPrefix}-title`, className: "flex items-center gap-2 text-sm font-semibold text-gray-800", children: [_jsx(Type, { className: "h-4 w-4 text-indigo-500" }), "Ti\u00EAu \u0111\u1EC1 nh\u00F3m", ' ', _jsx("span", { className: "text-xs font-normal text-gray-500", children: "(T\u00F9y ch\u1ECDn)" })] }), _jsx(Input, { id: `${uploadIdPrefix}-title`, value: title, onChange: (e) => onTitleChange?.(e.target.value), placeholder: "VD: Words from the box", readOnly: isReadOnly, className: `border-gray-200 bg-white transition-all focus:border-indigo-300 focus:ring-indigo-200 ${isReadOnly ? 'cursor-not-allowed border-transparent bg-gray-50 text-gray-500 focus:border-transparent focus:ring-0' : ''}` })] }), _jsx("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-3", children: displayItems.map((item, index) => (_jsxs("div", { className: "rounded-xl border-2 border-gray-200 bg-gradient-to-b from-slate-50 to-white p-3 transition-all hover:border-indigo-200 hover:shadow-sm", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "text-xs font-semibold text-gray-600", children: ["M\u1EE5c ", index + 1] }), displayItems.length > 1 && !isReadOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => handleDelete(index), className: "h-7 w-7 p-0 text-red-500 hover:bg-red-50 hover:text-red-600", title: "X\u00F3a m\u1EE5c", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }), _jsxs("div", { className: "space-y-2.5", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: `${uploadIdPrefix}-label-${index}`, className: "flex items-center gap-1.5 text-xs font-medium text-gray-700", children: [_jsx(Type, { className: "h-3.5 w-3.5 text-indigo-500" }), "Nh\u00E3n"] }), _jsx(Input, { id: `${uploadIdPrefix}-label-${index}`, value: item.label, onChange: (e) => handleFieldChange(index, 'label', e.target.value), placeholder: "VD: swimming", readOnly: isReadOnly, className: `border-gray-200 bg-white text-sm focus:border-indigo-300 focus:ring-indigo-200 ${isReadOnly ? 'cursor-not-allowed bg-gray-50 text-gray-500' : ''}` })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { className: "flex items-center gap-1.5 text-xs font-medium text-gray-700", children: [_jsx(ImageIcon, { className: "h-3.5 w-3.5 text-indigo-500" }), "H\u00ECnh \u1EA3nh"] }), !isReadOnly && (_jsx(FileUpload, { id: `${uploadIdPrefix}-image-${index}`, label: "", accept: "image/*", value: item.imageUrl, onChange: (url) => handleFieldChange(index, 'imageUrl', url), maxSize: 5, placeholder: "Upload \u1EA3nh", autoUpload: true, prefix: "questions" })), item.imageUrl && (_jsx(ItemImagePreview, { src: item.imageUrl, alt: item.label || `Item ${index + 1}` }))] })] })] }, index))) }), !isReadOnly && (_jsxs(Button, { type: "button", variant: "outline", className: "w-full border-dashed border-indigo-300 bg-indigo-50/50 text-indigo-700 hover:border-indigo-400 hover:bg-indigo-50 hover:text-indigo-800", onClick: handleAdd, children: [_jsx(PlusCircle, { className: "mr-2 h-4 w-4" }), "Th\u00EAm m\u1EE5c \u1EA3nh + t\u1EEB"] })), _jsxs("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50/40 p-4", children: [_jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-indigo-600", children: "Xem tr\u01B0\u1EDBc h\u00E0ng t\u1EEB" }), _jsx(ImageLabelWordBank, { items: displayItems })] })] })] }));
42
+ }
@@ -0,0 +1,10 @@
1
+ import { type ImageLabelData } from '../_shared/types/question-group.type';
2
+ export interface ImageLabelWordBankProps {
3
+ items?: ImageLabelData[];
4
+ title?: string;
5
+ className?: string;
6
+ }
7
+ /**
8
+ * Horizontal picture + label bank shown to students (and creator preview).
9
+ */
10
+ export declare function ImageLabelWordBank({ items, title, className }: ImageLabelWordBankProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ResolvedImage } from '../../../components/common/ResolvedImage';
4
+ import { hasImageLabels } from '../_shared/types/question-group.type';
5
+ /**
6
+ * Horizontal picture + label bank shown to students (and creator preview).
7
+ */
8
+ export function ImageLabelWordBank({ items, title, className }) {
9
+ if (!hasImageLabels(items))
10
+ return null;
11
+ const visible = items.filter((item) => item.imageUrl?.trim() || item.label?.trim());
12
+ const hasAnyImage = visible.some((item) => Boolean(item.imageUrl?.trim()));
13
+ if (!hasAnyImage) {
14
+ return (_jsxs("div", { className: className, children: [title ? (_jsx("p", { className: "mb-3 text-center text-sm font-semibold text-slate-700", children: title })) : null, _jsx("div", { className: "flex flex-wrap items-center justify-center gap-x-4 gap-y-2 rounded-xl border-2 border-slate-300 bg-white px-4 py-3 sm:gap-x-8", children: visible.map((item, index) => (_jsx("span", { className: "text-sm font-semibold text-slate-800 sm:text-base", children: item.label }, `${item.label || 'item'}-${index}`))) })] }));
15
+ }
16
+ return (_jsxs("div", { className: className, children: [title ? (_jsx("p", { className: "mb-3 text-center text-sm font-semibold text-slate-700", children: title })) : null, _jsx("div", { className: "flex flex-wrap justify-center gap-3 sm:gap-4", children: visible.map((item, index) => (_jsxs("div", { className: "flex w-[5.5rem] flex-col items-center gap-1.5 sm:w-24", children: [_jsx("div", { className: "flex h-20 w-full items-center justify-center overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm sm:h-24", children: item.imageUrl ? (_jsx(ResolvedImage, { src: item.imageUrl, alt: item.label || `Item ${index + 1}`, className: "h-full w-full object-contain p-1" })) : (_jsx("span", { className: "px-1 text-center text-[10px] text-slate-400", children: "Ch\u01B0a c\u00F3 \u1EA3nh" })) }), item.label?.trim() ? (_jsx("span", { className: "text-center text-xs font-semibold leading-tight text-slate-800 sm:text-sm", children: item.label })) : null] }, `${item.label || 'item'}-${index}`))) })] }));
17
+ }
@@ -12,10 +12,10 @@ import { RadioGroup } from '../../../../components/ui/radio-group';
12
12
  import { Switch } from '../../../../components/ui/switch';
13
13
  import { usePresignedFileUrl, useDebouncedCallback } from '../../../../shared/lib/hooks';
14
14
  import { useReactHookForm } from '../../../../shared/lib/hooks/useReactHookForm';
15
- import { Pencil, Eye, Plus, Trash2, HelpCircle, CheckCircle2, Lightbulb, MessageSquare, ImageIcon, Type, Users, BookOpen, Star, ListChecks } from 'lucide-react';
15
+ import { Pencil, Eye, Plus, Trash2, HelpCircle, CheckCircle2, Lightbulb, MessageSquare, ImageIcon, Type, Users, BookOpen, Star, ListChecks, Pilcrow } from 'lucide-react';
16
16
  import { FileUpload } from '../../../../components/ui/file-upload';
17
17
  import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
18
- import { isRichTextEmpty } from '../../../../shared/lib/rich-text';
18
+ import { hasRichTextFormatting, htmlToPlainEditorText, isRichTextEmpty } from '../../../../shared/lib/rich-text';
19
19
  import { ChooseTheCorrectAnswerClient } from './ChooseTheCorrectAnswerClient';
20
20
  import { normalizeChooseAnswerCorrectAnswer, normalizeChooseAnswerOptions, pickRawChooseAnswer, } from './normalize';
21
21
  import { chooseCorrectAnswerFormSchema, } from '../../../../shared/lib/validation/choose-correct-answer.validation';
@@ -24,6 +24,24 @@ import { ArticlesGroupCard } from '../../groups/ArticlesGroupCard';
24
24
  import { DocumentGroupCard } from '../../groups/DocumentGroupCard';
25
25
  import { useBasicQuestionGroup } from '../../_shared/hooks/useBasicQuestionGroup';
26
26
  import { z } from 'zod';
27
+ function shouldStartInTipTapMode(apiData) {
28
+ const question = apiData?.content?.question || apiData?.question || '';
29
+ if (hasRichTextFormatting(question))
30
+ return true;
31
+ const rawOptions = apiData?.content?.options || apiData?.options;
32
+ if (!Array.isArray(rawOptions))
33
+ return false;
34
+ return rawOptions.some((option) => hasRichTextFormatting(typeof option === 'string' ? option : option?.text));
35
+ }
36
+ function ChooseAnswerTextInput({ value, onChange, isTipTapMode, placeholder, compact = false, }) {
37
+ if (isTipTapMode) {
38
+ return (_jsx(RichTextEditor, { value: value, onChange: onChange, variant: "compact", minHeightClassName: compact ? 'min-h-[72px]' : 'min-h-[100px]' }));
39
+ }
40
+ if (compact) {
41
+ return (_jsx(Input, { value: htmlToPlainEditorText(value), onChange: (event) => onChange(event.target.value), placeholder: placeholder, className: "border-gray-200 bg-white" }));
42
+ }
43
+ return (_jsx(Textarea, { value: htmlToPlainEditorText(value), onChange: (event) => onChange(event.target.value), placeholder: placeholder, rows: 3, className: "min-h-[72px] border-gray-200 bg-white" }));
44
+ }
27
45
  // Helper component to resolve presigned URL for option image preview (same pattern as Client's OptionImage)
28
46
  function OptionImagePreview({ src, alt }) {
29
47
  const { previewUrl, isLoading } = usePresignedFileUrl(src);
@@ -115,6 +133,9 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
115
133
  const [isClientMode, setIsClientMode] = useState(false);
116
134
  const [imagePreviewUrl, setImagePreviewUrl] = useState('');
117
135
  const [optionPreviewUrls, setOptionPreviewUrls] = useState({});
136
+ const [isImageInputVisible, setIsImageInputVisible] = useState(() => Boolean(apiData?.content?.imageUrl || apiData?.imageUrl));
137
+ const [isTipTapMode, setIsTipTapMode] = useState(() => shouldStartInTipTapMode(apiData));
138
+ const [isExplanationVisible, setIsExplanationVisible] = useState(() => Boolean(apiData?.explanation));
118
139
  // ============ GROUP (shared title / content / image) ============
119
140
  const { isGrouped, currentGroupNumber, isFirstInGroup, groupTitle, groupSubtitle, groupContent, imageUrl: groupImageUrl, articles, setGroupTitle, setGroupSubtitle, setGroupContent, setImageUrl: setGroupImageUrl, setArticles, } = useBasicQuestionGroup({
120
141
  partId,
@@ -426,9 +447,12 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
426
447
  form.setValue('correctAnswer', enabled ? normalized : (normalized[0] ?? -1), { shouldValidate: true });
427
448
  }, className: `inline-flex items-center gap-2 rounded-md border px-3.5 text-sm font-medium transition-colors select-none ${watchedMultipleAnswers
428
449
  ? 'border-violet-300 bg-violet-50 text-violet-700'
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) => {
450
+ : '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"] }), _jsxs("button", { type: "button", style: { paddingBlock: 'calc(var(--spacing) * 1)' }, onClick: () => setIsTipTapMode((enabled) => !enabled), title: isTipTapMode ? 'Chuyển về ô nhập thường' : 'Bật định dạng chữ (in đậm, danh sách, bảng)', className: `inline-flex items-center gap-2 rounded-md border px-3.5 text-sm font-medium transition-colors select-none ${isTipTapMode
451
+ ? 'border-indigo-300 bg-indigo-50 text-indigo-700'
452
+ : 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'}`, children: [_jsx(Pilcrow, { className: "h-4 w-4" }), "\u0110\u1ECBnh d\u1EA1ng"] })] })] }), 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(ChooseAnswerTextInput, { value: watchedQuestion, onChange: (html) => form.setValue('question', html, { shouldValidate: true }), isTipTapMode: isTipTapMode, placeholder: "Nh\u1EADp c\u00E2u h\u1ECFi..." }), 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 ')) && !isImageInputVisible
453
+ || !isExplanationVisible) && (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [(!questionConfig || questionConfig.length === 0 || questionConfig.includes('imageUrl') || questionConfig.includes('imageUrl ')) && !isImageInputVisible && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsImageInputVisible(true), className: "gap-2 border-indigo-200 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(ImageIcon, { className: "h-4 w-4" }), watchedImageUrl ? 'Hiện hình ảnh' : 'Thêm hình ảnh'] })), !isExplanationVisible && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsExplanationVisible(true), className: "gap-2 border-amber-200 text-amber-700 hover:bg-amber-50 hover:text-amber-800", children: [_jsx(BookOpen, { className: "h-4 w-4" }), watchedExplanation ? 'Hiện giải thích' : 'Thêm giải thích'] }))] })), (!questionConfig || questionConfig.length === 0 || questionConfig.includes('imageUrl') || questionConfig.includes('imageUrl ')) && isImageInputVisible && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-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(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsImageInputVisible(false), className: "h-8 text-gray-500 hover:text-gray-700", children: "\u1EA8n" })] }), _jsx(FileUpload, { id: "question-image", label: "", accept: "image/*", value: watchedImageUrl || '', onChange: (url) => {
430
454
  form.setValue('imageUrl', url, { shouldValidate: true });
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) => {
455
+ }, 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" }))] })), isExplanationVisible && (_jsxs("div", { className: "relative", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsExplanationVisible(false), className: "absolute right-3 top-3 z-10 h-8 text-gray-500 hover:text-gray-700", children: "\u1EA8n" }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", 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-amber-500 to-orange-500 shadow-sm", children: _jsx(BookOpen, { className: "h-3.5 w-3.5 text-white" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" }), _jsx("span", { className: "inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-600", children: "Kh\u00F4ng b\u1EAFt bu\u1ED9c" })] }), _jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: "Gi\u1EA3i th\u00EDch cho \u0111\u00E1p \u00E1n \u0111\u00FAng \u2014 hi\u1EC3n th\u1ECB sau khi h\u1ECDc sinh ho\u00E0n th\u00E0nh c\u00E2u h\u1ECFi" })] })] }) }), _jsx(CardContent, { className: "px-4 pb-4", children: _jsx(Textarea, { ...form.register('explanation'), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", rows: 3, className: "min-h-[80px] resize-none border-gray-200 bg-white transition-all focus:border-amber-300 focus:ring-amber-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) => {
432
456
  const newType = checked ? 'image' : 'text';
433
457
  const currentOptions = form.getValues('options');
434
458
  const currentCorrectAnswer = form.getValues('correctAnswer');
@@ -492,11 +516,11 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
492
516
  setOptionPreviewUrls((prev) => ({ ...prev, [index]: presignedUrl }));
493
517
  }, 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}` }))] })) : (
494
518
  /* Text Input Mode */
495
- _jsx(RichTextEditor, { value: option, onChange: (html) => {
519
+ _jsx(ChooseAnswerTextInput, { value: option, onChange: (html) => {
496
520
  const newOptions = [...watchedOptions];
497
521
  newOptions[index] = html;
498
522
  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: () => {
523
+ }, isTipTapMode: isTipTapMode, compact: true, placeholder: `Nhập đáp án ${optionLetter}...` })), 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: () => {
500
524
  if (watchedMultipleAnswers) {
501
525
  const currentAnswers = Array.isArray(form.getValues('correctAnswer'))
502
526
  ? form.getValues('correctAnswer')
@@ -551,11 +575,11 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
551
575
  setOptionPreviewUrls((prev) => ({ ...prev, [index]: presignedUrl }));
552
576
  }, 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}` }))] })) : (
553
577
  /* Text Input Mode */
554
- _jsx(RichTextEditor, { value: option, onChange: (html) => {
578
+ _jsx(ChooseAnswerTextInput, { value: option, onChange: (html) => {
555
579
  const newOptions = [...watchedOptions];
556
580
  newOptions[index] = html;
557
581
  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: () => {
582
+ }, isTipTapMode: isTipTapMode, compact: true, placeholder: `Nhập đáp án ${optionLetter}...` })), 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: () => {
559
583
  form.setValue('correctAnswer', index, { shouldValidate: true });
560
584
  }, 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: () => {
561
585
  const currentOptions = form.getValues('options');
@@ -573,7 +597,7 @@ function ChooseTheCorrectAnswerCreatorContent({ initialData, onSave, onCancel, o
573
597
  }) })), ((Array.isArray(watchedCorrectAnswer) && watchedCorrectAnswer.length === 0)
574
598
  || (!Array.isArray(watchedCorrectAnswer) && watchedCorrectAnswer === -1)) && (_jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-4", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-amber-100", children: _jsx("span", { className: "text-lg", children: "\u26A0\uFE0F" }) }), _jsxs("div", { className: "text-sm text-amber-800", children: [_jsx("p", { className: "font-semibold", children: "Ch\u01B0a ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("p", { className: "mt-0.5 text-amber-700", children: watchedMultipleAnswers
575
599
  ? 'Click vào ô chữ cái hoặc icon ✓ để chọn một hoặc nhiều đáp án đúng'
576
- : 'Click vào ô chữ cái hoặc icon ✓ để chọn đáp án đúng' })] })] })), form.hasError('correctAnswer') && (_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.getFieldError('correctAnswer') }) }))] })] })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", 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-amber-500 to-orange-500 shadow-sm", children: _jsx(BookOpen, { className: "h-3.5 w-3.5 text-white" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" }), _jsx("span", { className: "inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-600", children: "Kh\u00F4ng b\u1EAFt bu\u1ED9c" })] }), _jsx("p", { className: "text-xs text-gray-500 mt-0.5", children: "Gi\u1EA3i th\u00EDch cho \u0111\u00E1p \u00E1n \u0111\u00FAng \u2014 hi\u1EC3n th\u1ECB sau khi h\u1ECDc sinh ho\u00E0n th\u00E0nh c\u00E2u h\u1ECFi" })] })] }) }), _jsx(CardContent, { className: "px-4 pb-4", children: _jsx(Textarea, { ...form.register('explanation'), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", rows: 3, className: "min-h-[80px] border-gray-200 bg-white transition-all focus:border-amber-300 focus:ring-amber-200 resize-none" }) })] })] }));
600
+ : 'Click vào ô chữ cái hoặc icon ✓ để chọn đáp án đúng' })] })] })), form.hasError('correctAnswer') && (_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.getFieldError('correctAnswer') }) }))] })] }))] }));
577
601
  }
578
602
  export function ChooseTheCorrectAnswerCreator(props) {
579
603
  return _jsx(ChooseTheCorrectAnswerCreatorContent, { ...props });
@@ -3,6 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { useState, useEffect, useMemo } from 'react';
4
4
  import { Input } from '../../../../components/ui/input';
5
5
  import { Check, X, PenLine, AlertTriangle, ClipboardList, Lightbulb, BookOpen, Hash, Volume2, } from 'lucide-react';
6
+ import { ImageLabelWordBank } from '../../groups/ImageLabelWordBank';
7
+ import { hasImageLabels } from '../../_shared/types/question-group.type';
6
8
  import { extractPlaceholderIndices, formatBlankAnswersDisplay, getPrimaryBlankAnswer, isBlankInputCorrect, normalizeBlankAnswers, normalizeFillInBlankAnswers, } from './answer-utils';
7
9
  import { parseFillBlankSegments } from '../../../../shared/lib/utils/fill-in-blank';
8
10
  import { cn } from '../../../../shared/lib/utils';
@@ -31,7 +33,9 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
31
33
  const groupContent = questionData.groupContent || '';
32
34
  const groupImageUrl = questionData.groupImageUrl || '';
33
35
  const groupNumber = questionData.groupNumber;
34
- const hasGroupData = isGroup && (groupTitle || groupContent || groupImageUrl);
36
+ const imageLabels = questionData.imageLabels;
37
+ const hasImageLabelBank = hasImageLabels(imageLabels);
38
+ const hasGroupData = isGroup && (groupTitle || groupContent || groupImageUrl || hasImageLabelBank);
35
39
  // Custom hooks for presigned URL (question-level image, not group image)
36
40
  const { previewUrl: imagePreviewUrl, isLoading: isImageLoading } = usePresignedFileUrl(
37
41
  // Only use question-level imageUrl when NOT grouped (group image is handled separately)
@@ -128,7 +132,7 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
128
132
  const renderGroupBlock = () => {
129
133
  if (!hasGroupData)
130
134
  return null;
131
- return (_jsxs("div", { className: "relative overflow-hidden rounded-2xl border border-indigo-100 bg-white shadow-sm ring-1 ring-black/5 transition-all", children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-br from-indigo-50/80 via-white to-blue-50/30 opacity-70 pointer-events-none" }), _jsx("div", { className: "absolute left-0 top-0 bottom-0 w-1.5 bg-gradient-to-b from-indigo-500 to-blue-600 rounded-l-2xl" }), _jsxs("div", { className: "relative p-6 sm:p-7", children: [_jsxs("div", { className: "flex items-start gap-4 mb-5 border-b border-indigo-50/60 pb-4", children: [_jsx("div", { className: "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-100 to-blue-100 shadow-inner ring-1 ring-indigo-200/50", children: _jsx(BookOpen, { className: "h-5 w-5 text-indigo-600" }) }), _jsxs("div", { className: "flex-1 pt-1", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [groupNumber && (_jsxs("span", { className: "inline-flex items-center rounded-md bg-indigo-50 px-2 py-1 text-xs font-semibold text-indigo-700 ring-1 ring-inset ring-indigo-700/10", children: ["Nh\u00F3m ", groupNumber] })), _jsx("span", { className: "inline-flex items-center rounded-md bg-orange-50 px-2 py-1 text-xs font-semibold text-orange-700 ring-1 ring-inset ring-orange-700/10", children: "\u0110i\u1EC1n v\u00E0o ch\u1ED7 tr\u1ED1ng" })] }), groupTitle && (_jsx("h4", { className: "text-xl font-bold text-slate-800 leading-tight tracking-tight", children: groupTitle }))] })] }), _jsxs("div", { className: "space-y-6 text-slate-700", children: [groupContent && (_jsx("div", { className: "prose prose-slate prose-p:leading-relaxed prose-p:mb-3 max-w-none text-[15px]", children: groupContent.split('\n').map((line, i) => (_jsx("p", { className: "last:mb-0", children: line }, i))) })), 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(GroupImage, { src: groupImageUrl, alt: groupTitle || 'Hình ảnh nhóm' }) })] }))] })] })] }));
135
+ return (_jsxs("div", { className: "relative overflow-hidden rounded-2xl border border-indigo-100 bg-white shadow-sm ring-1 ring-black/5 transition-all", children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-br from-indigo-50/80 via-white to-blue-50/30 opacity-70 pointer-events-none" }), _jsx("div", { className: "absolute left-0 top-0 bottom-0 w-1.5 bg-gradient-to-b from-indigo-500 to-blue-600 rounded-l-2xl" }), _jsxs("div", { className: "relative p-6 sm:p-7", children: [_jsxs("div", { className: "flex items-start gap-4 mb-5 border-b border-indigo-50/60 pb-4", children: [_jsx("div", { className: "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-100 to-blue-100 shadow-inner ring-1 ring-indigo-200/50", children: _jsx(BookOpen, { className: "h-5 w-5 text-indigo-600" }) }), _jsxs("div", { className: "flex-1 pt-1", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [groupNumber && (_jsxs("span", { className: "inline-flex items-center rounded-md bg-indigo-50 px-2 py-1 text-xs font-semibold text-indigo-700 ring-1 ring-inset ring-indigo-700/10", children: ["Nh\u00F3m ", groupNumber] })), _jsx("span", { className: "inline-flex items-center rounded-md bg-orange-50 px-2 py-1 text-xs font-semibold text-orange-700 ring-1 ring-inset ring-orange-700/10", children: "\u0110i\u1EC1n v\u00E0o ch\u1ED7 tr\u1ED1ng" })] }), groupTitle && (_jsx("h4", { className: "text-xl font-bold text-slate-800 leading-tight tracking-tight", children: groupTitle }))] })] }), _jsxs("div", { className: "space-y-6 text-slate-700", children: [hasImageLabelBank && (_jsx(ImageLabelWordBank, { items: imageLabels })), groupContent && (_jsx("div", { className: "prose prose-slate prose-p:leading-relaxed prose-p:mb-3 max-w-none text-[15px]", children: groupContent.split('\n').map((line, i) => (_jsx("p", { className: "last:mb-0", children: line }, i))) })), 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(GroupImage, { src: groupImageUrl, alt: groupTitle || 'Hình ảnh nhóm' }) })] }))] })] })] }));
132
136
  };
133
137
  /** Render inline blanks within question text (new format) */
134
138
  const renderQuestionWithBlanks = () => {
@@ -15,13 +15,31 @@ import { blankHasAnswer, compactFillInBlankAnswers, formatBlankAnswersDisplay, e
15
15
  import { useDebouncedCallback, usePresignedFileUrl } from '../../../../shared/lib/hooks';
16
16
  import { FileUpload } from '../../../../components/ui/file-upload';
17
17
  import { BasicQuestionGroupCard } from '../../groups/BasicQuestionGroupCard';
18
+ import { ImageLabelGroupCard } from '../../groups/ImageLabelGroupCard';
18
19
  import { useBasicQuestionGroup } from '../../_shared/hooks/useBasicQuestionGroup';
19
- function FillInBlankCreatorContent({ initialData, onChange, externalErrors, validationRef, groups, questionIndexInPart, partId, questionConfig, headerExtra, }) {
20
+ function resolveInitialImageLabels(initialData, suggestedAnswers) {
21
+ const fromPayload = initialData?.imageLabels || initialData?.questionGroup?.payload?.imageLabels;
22
+ if (Array.isArray(fromPayload) && fromPayload.length > 0) {
23
+ return fromPayload.map((item) => ({
24
+ imageUrl: item?.imageUrl || '',
25
+ label: item?.label || '',
26
+ }));
27
+ }
28
+ if (suggestedAnswers?.length) {
29
+ return suggestedAnswers.map((label) => ({ imageUrl: '', label }));
30
+ }
31
+ return [];
32
+ }
33
+ function FillInBlankCreatorContent({ initialData, onChange, externalErrors, validationRef, groups, questionIndexInPart, partId, questionConfig, groupType, suggestedAnswers, headerExtra, }) {
34
+ const effectiveGroupType = (groupType || 'BASIC').toUpperCase();
35
+ const isImageLabelGroup = effectiveGroupType === 'IMAGE_LABEL';
36
+ const initialImageLabels = resolveInitialImageLabels(initialData, suggestedAnswers);
20
37
  // ============ GROUP (shared title / content / image) ============
21
- const { isGrouped, currentGroupNumber, groupTitle, groupSubtitle, groupContent, imageUrl: groupImageUrl, setGroupTitle, setGroupSubtitle, setGroupContent, setImageUrl: setGroupImageUrl, } = useBasicQuestionGroup({
38
+ const { isGrouped, currentGroupNumber, isFirstInGroup, groupTitle, groupSubtitle, groupContent, imageUrl: groupImageUrl, imageLabels, setGroupTitle, setGroupSubtitle, setGroupContent, setImageUrl: setGroupImageUrl, setImageLabels, } = useBasicQuestionGroup({
22
39
  partId,
23
40
  groups,
24
41
  questionIndexInPart,
42
+ imageLabelCount: isImageLabelGroup ? (initialImageLabels.length || 1) : 0,
25
43
  initialData: {
26
44
  title: (() => {
27
45
  if (initialData?.content?.passageTitle)
@@ -60,6 +78,7 @@ function FillInBlankCreatorContent({ initialData, onChange, externalErrors, vali
60
78
  return initialData.groupImageUrl;
61
79
  return '';
62
80
  })(),
81
+ imageLabels: initialImageLabels,
63
82
  },
64
83
  });
65
84
  // ============ STATE ============
@@ -232,10 +251,11 @@ function FillInBlankCreatorContent({ initialData, onChange, externalErrors, vali
232
251
  groupContent: isGrouped ? groupContent : '',
233
252
  groupNumber: currentGroupNumber,
234
253
  groups,
254
+ ...(isGrouped && isImageLabelGroup ? { imageLabels } : {}),
235
255
  }),
236
256
  };
237
257
  }
238
- }, [validationRef, question, answers, explanation, points, isGrouped, imageUrl, groupImageUrl, audioUrl, includesAudio, showHints, hints, groupTitle, groupSubtitle, groupContent, currentGroupNumber, groups, isExample, validateForm]);
258
+ }, [validationRef, question, answers, explanation, points, isGrouped, imageUrl, groupImageUrl, audioUrl, includesAudio, showHints, hints, groupTitle, groupSubtitle, groupContent, currentGroupNumber, groups, isExample, validateForm, isImageLabelGroup, imageLabels]);
239
259
  // ============ SYNC INITIAL DATA ============
240
260
  useEffect(() => {
241
261
  const initialDataChanged = JSON.stringify(previousInitialDataRef.current) !== JSON.stringify(initialData);
@@ -290,6 +310,7 @@ function FillInBlankCreatorContent({ initialData, onChange, externalErrors, vali
290
310
  groupContent,
291
311
  groupNumber: currentGroupNumber,
292
312
  groups,
313
+ ...(isImageLabelGroup ? { imageLabels } : {}),
293
314
  });
294
315
  previousPayloadRef.current = currentPayload;
295
316
  return;
@@ -310,13 +331,14 @@ function FillInBlankCreatorContent({ initialData, onChange, externalErrors, vali
310
331
  groupContent: isGrouped ? groupContent : '',
311
332
  groupNumber: currentGroupNumber,
312
333
  groups,
334
+ ...(isGrouped && isImageLabelGroup ? { imageLabels } : {}),
313
335
  };
314
336
  const payloadString = JSON.stringify(payload);
315
337
  if (previousPayloadRef.current !== payloadString) {
316
338
  previousPayloadRef.current = payloadString;
317
339
  debouncedOnChange(payload);
318
340
  }
319
- }, [question, answers, explanation, points, isGrouped, imageUrl, groupImageUrl, audioUrl, includesAudio, showHints, hints, isExample, groupTitle, groupSubtitle, groupContent, currentGroupNumber, groups, debouncedOnChange]);
341
+ }, [question, answers, explanation, points, isGrouped, imageUrl, groupImageUrl, audioUrl, includesAudio, showHints, hints, isExample, groupTitle, groupSubtitle, groupContent, currentGroupNumber, groups, debouncedOnChange, isImageLabelGroup, imageLabels]);
320
342
  // Ensure blank slot exists then update alternative at altIndex
321
343
  const updateBlankAnswer = (blankIndex, altIndex, value) => {
322
344
  setAnswers((prev) => {
@@ -394,11 +416,12 @@ function FillInBlankCreatorContent({ initialData, onChange, externalErrors, vali
394
416
  groupContent: isGrouped ? groupContent : '',
395
417
  groupImageUrl: isGrouped ? (groupDisplayPreviewUrl || groupImageUrl) : '',
396
418
  groupNumber: isGrouped ? currentGroupNumber : undefined,
419
+ ...(isGrouped && isImageLabelGroup ? { imageLabels } : {}),
397
420
  };
398
421
  return (_jsxs("div", { className: "space-y-4", children: [_jsx(Card, { children: _jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx(CardTitle, { children: "Fill In Blank (Preview)" }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Quay l\u1EA1i Edit Mode"] })] }) }) }), _jsx(FillInBlankClient, { questionData: questionData, questionConfig: questionConfig, autoFillCorrectAnswers: true, isReviewMode: false, explanation: explanation })] }));
399
422
  }
400
423
  // ============ CREATOR UI ============
401
- return (_jsxs("div", { className: "space-y-4", children: [isGrouped && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, uploadIdPrefix: `fill-in-blank-${partId ?? 'group'}` })), _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-gradient-to-br from-blue-500 to-indigo-600 shadow-sm", children: _jsx(Type, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n v\u00E0o ch\u1ED7 tr\u1ED1ng" }), _jsx("p", { className: "text-xs text-gray-500", children: "H\u1ED7 tr\u1EE3 nhi\u1EC1u ch\u1ED7 tr\u1ED1ng trong m\u1ED9t c\u00E2u h\u1ECFi" })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [headerExtra, _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: !question?.trim() || placeholderIndices.length === 0, 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-4 px-4 pb-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-6", children: [_jsx(PointsInput, { value: points, onChange: (e) => setPoints(parseFloat(e.target.value) || 1) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "show-suggested-words", checked: showHints, onCheckedChange: setShowHints }), _jsx(Label, { htmlFor: "show-suggested-words", className: "cursor-pointer text-sm font-medium text-gray-700", children: "Hi\u1EC3n th\u1ECB t\u1EEB g\u1EE3i \u00FD" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "isExample", checked: isExample, onCheckedChange: setIsExample }), _jsx(Label, { htmlFor: "isExample", className: "cursor-pointer text-sm font-medium text-gray-700", children: "\u0110\u00E1nh d\u1EA5u l\u00E0 c\u00E2u h\u1ECFi Example" })] })] }), _jsx("div", { className: "rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-5 shadow-sm", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100", children: _jsx(Lightbulb, { className: "h-4 w-4 text-blue-600" }) }), _jsxs("div", { className: "text-sm text-blue-900", children: [_jsx("p", { className: "font-semibold text-blue-800", children: "H\u01B0\u1EDBng d\u1EABn t\u1EA1o c\u00E2u h\u1ECFi" }), _jsxs("ul", { className: "mt-2 space-y-1.5 text-blue-700", children: [_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "S\u1EED d\u1EE5ng ", _jsx("code", { className: "mx-1 rounded bg-blue-100 px-1.5 py-0.5 font-mono text-blue-800", children: '{0}' }), ", ", _jsx("code", { className: "mx-1 rounded bg-blue-100 px-1.5 py-0.5 font-mono text-blue-800", children: '{1}' }), ", ", _jsx("code", { className: "mx-1 rounded bg-blue-100 px-1.5 py-0.5 font-mono text-blue-800", children: '{2}' }), "... \u0111\u1EC3 \u0111\u00E1nh d\u1EA5u c\u00E1c ch\u1ED7 tr\u1ED1ng"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "H\u1EC7 th\u1ED1ng s\u1EBD t\u1EF1 \u0111\u1ED9ng t\u1EA1o \u00F4 nh\u1EADp \u0111\u00E1p \u00E1n cho m\u1ED7i ch\u1ED7 tr\u1ED1ng"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "V\u00ED d\u1EE5: \u201CHe ", '{0}', " 10 years old and she ", '{1}', " 12.\u201D"] })] })] })] }) }), !isGrouped && (_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", children: "(T\u00F9y ch\u1ECDn)" })] }), _jsx(FileUpload, { id: "fill-in-blank-image", label: "", accept: "image/*", value: imageUrl || '', onChange: (url) => {
424
+ return (_jsxs("div", { className: "space-y-4", children: [isGrouped && isImageLabelGroup && (_jsx(ImageLabelGroupCard, { groupNumber: currentGroupNumber, imageLabels: imageLabels, onImageLabelsChange: setImageLabels, title: groupTitle, onTitleChange: setGroupTitle, isReadOnly: !isFirstInGroup, uploadIdPrefix: `fill-in-blank-${partId ?? 'image-label'}` })), isGrouped && !isImageLabelGroup && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, uploadIdPrefix: `fill-in-blank-${partId ?? 'group'}` })), _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-gradient-to-br from-blue-500 to-indigo-600 shadow-sm", children: _jsx(Type, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n v\u00E0o ch\u1ED7 tr\u1ED1ng" }), _jsx("p", { className: "text-xs text-gray-500", children: "H\u1ED7 tr\u1EE3 nhi\u1EC1u ch\u1ED7 tr\u1ED1ng trong m\u1ED9t c\u00E2u h\u1ECFi" })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [headerExtra, _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: !question?.trim() || placeholderIndices.length === 0, 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-4 px-4 pb-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-6", children: [_jsx(PointsInput, { value: points, onChange: (e) => setPoints(parseFloat(e.target.value) || 1) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "show-suggested-words", checked: showHints, onCheckedChange: setShowHints }), _jsx(Label, { htmlFor: "show-suggested-words", className: "cursor-pointer text-sm font-medium text-gray-700", children: "Hi\u1EC3n th\u1ECB t\u1EEB g\u1EE3i \u00FD" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "isExample", checked: isExample, onCheckedChange: setIsExample }), _jsx(Label, { htmlFor: "isExample", className: "cursor-pointer text-sm font-medium text-gray-700", children: "\u0110\u00E1nh d\u1EA5u l\u00E0 c\u00E2u h\u1ECFi Example" })] })] }), _jsx("div", { className: "rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-5 shadow-sm", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100", children: _jsx(Lightbulb, { className: "h-4 w-4 text-blue-600" }) }), _jsxs("div", { className: "text-sm text-blue-900", children: [_jsx("p", { className: "font-semibold text-blue-800", children: "H\u01B0\u1EDBng d\u1EABn t\u1EA1o c\u00E2u h\u1ECFi" }), _jsxs("ul", { className: "mt-2 space-y-1.5 text-blue-700", children: [_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "S\u1EED d\u1EE5ng ", _jsx("code", { className: "mx-1 rounded bg-blue-100 px-1.5 py-0.5 font-mono text-blue-800", children: '{0}' }), ", ", _jsx("code", { className: "mx-1 rounded bg-blue-100 px-1.5 py-0.5 font-mono text-blue-800", children: '{1}' }), ", ", _jsx("code", { className: "mx-1 rounded bg-blue-100 px-1.5 py-0.5 font-mono text-blue-800", children: '{2}' }), "... \u0111\u1EC3 \u0111\u00E1nh d\u1EA5u c\u00E1c ch\u1ED7 tr\u1ED1ng"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "H\u1EC7 th\u1ED1ng s\u1EBD t\u1EF1 \u0111\u1ED9ng t\u1EA1o \u00F4 nh\u1EADp \u0111\u00E1p \u00E1n cho m\u1ED7i ch\u1ED7 tr\u1ED1ng"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-blue-400" }), "V\u00ED d\u1EE5: \u201CHe ", '{0}', " 10 years old and she ", '{1}', " 12.\u201D"] })] })] })] }) }), !isGrouped && (_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", children: "(T\u00F9y ch\u1ECDn)" })] }), _jsx(FileUpload, { id: "fill-in-blank-image", label: "", accept: "image/*", value: imageUrl || '', onChange: (url) => {
402
425
  setImageUrl(url);
403
426
  }, onPresignedUrlChange: setImagePreviewUrl, maxSize: 5, placeholder: "Upload \u1EA3nh ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }), displayPreviewUrl && (_jsx(ImagePreview, { src: displayPreviewUrl, alt: "Preview", className: "mt-2 h-48 w-full max-w-md rounded-lg border border-gray-200" }))] })), includesAudio && (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Volume2, { className: "h-4 w-4 text-indigo-500" }), "Audio c\u00E2u h\u1ECFi"] }), _jsx(FileUpload, { id: "fill-in-blank-audio", label: "", accept: "audio/*", value: audioUrl || '', onChange: (url) => {
404
427
  setAudioUrl(url);
@@ -130,6 +130,9 @@ export const fillInBlankRegistration = {
130
130
  groups: answer?.groups || initialData?.groups,
131
131
  };
132
132
  }
133
+ if (creatorData) {
134
+ creatorData.imageLabels = groupPayload?.imageLabels || answer?.imageLabels || undefined;
135
+ }
133
136
  return creatorData;
134
137
  },
135
138
  getExtraProps: (ctx) => ({
@@ -139,6 +142,8 @@ export const fillInBlankRegistration = {
139
142
  questionIndexInPart: ctx.questionIndexInPart,
140
143
  partId: ctx.partId,
141
144
  questionConfig: ctx.questionConfig,
145
+ groupType: ctx.groupType,
146
+ suggestedAnswers: ctx.suggestedAnswers,
142
147
  }),
143
148
  wrapOnSave: (data, ctx) => ({
144
149
  type: ctx.questionType,
@@ -4,13 +4,16 @@ export const transformFillInBlank = (question) => {
4
4
  // Helper: build basicGroupData if any group field is present.
5
5
  // NOTE: only `groupImageUrl` belongs to the group card — the per-question `imageUrl`
6
6
  // goes into content (same split as CHOOSE_THE_CORRECT_ANSWER).
7
- const hasGroupData = !!(answerData?.title || answerData?.subtitle || answerData?.groupContent || answerData?.groupImageUrl);
7
+ const imageLabels = Array.isArray(answerData?.imageLabels) ? answerData.imageLabels : undefined;
8
+ const hasImageLabels = Array.isArray(imageLabels) && imageLabels.some((item) => Boolean(item?.imageUrl?.trim() || item?.label?.trim()));
9
+ const hasGroupData = !!(answerData?.title || answerData?.subtitle || answerData?.groupContent || answerData?.groupImageUrl || hasImageLabels);
8
10
  const basicGroupData = hasGroupData
9
11
  ? {
10
12
  title: answerData.title || '',
11
13
  subtitle: answerData.subtitle || '',
12
14
  content: answerData.groupContent || '',
13
15
  imageUrl: answerData.groupImageUrl || '',
16
+ ...(hasImageLabels ? { imageLabels } : {}),
14
17
  }
15
18
  : undefined;
16
19
  const groupNumber = answerData?.groupNumber || 1;
@@ -0,0 +1,11 @@
1
+ export interface SpeakingCueCardClientProps {
2
+ questionData: {
3
+ cueCardHtml: string;
4
+ isBackup?: boolean;
5
+ expectedAnswers?: string[];
6
+ explanation?: string;
7
+ };
8
+ isReviewMode?: boolean;
9
+ }
10
+ export declare function mapQuestionToSpeakingCueCardData(question: Record<string, unknown> | null | undefined): SpeakingCueCardClientProps['questionData'];
11
+ export declare function SpeakingCueCardClient({ questionData, isReviewMode, }: SpeakingCueCardClientProps): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Mic, CheckCircle2, Info, CreditCard } from 'lucide-react';
4
+ import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
5
+ function asRecord(value) {
6
+ return value && typeof value === 'object' && !Array.isArray(value)
7
+ ? value
8
+ : {};
9
+ }
10
+ export function mapQuestionToSpeakingCueCardData(question) {
11
+ const source = asRecord(question);
12
+ const content = asRecord(source.content);
13
+ const answer = asRecord(source.correctAnswer ?? source.correct_answer ?? source.answer);
14
+ const expectedAnswers = Array.isArray(answer.expectedAnswers)
15
+ ? answer.expectedAnswers.filter((item) => typeof item === 'string' && item.trim() !== '')
16
+ : [];
17
+ return {
18
+ cueCardHtml: typeof content.cueCardHtml === 'string' ? content.cueCardHtml : '',
19
+ isBackup: content.isBackup === true,
20
+ expectedAnswers,
21
+ explanation: typeof source.explanation === 'string' ? source.explanation : '',
22
+ };
23
+ }
24
+ export function SpeakingCueCardClient({ questionData, isReviewMode = false, }) {
25
+ const answers = questionData.expectedAnswers || [];
26
+ return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsx("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-purple-50 to-indigo-50 px-5 py-3", 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-purple-500 to-indigo-600 text-white shadow-sm", children: _jsx(CreditCard, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "Th\u1EBB g\u1EE3i \u00FD (Cue Card)" })] }) }), _jsxs("div", { className: "p-5 space-y-5", children: [_jsx("div", { className: "rounded-xl border border-purple-100 bg-purple-50/40 px-5 py-4", children: _jsx(RichTextHtml, { html: questionData.cueCardHtml, className: "text-sm text-slate-800", fallback: _jsx("p", { className: "text-sm text-gray-400", children: "Ch\u01B0a c\u00F3 n\u1ED9i dung cue card" }) }) }), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-purple-50 ring-2 ring-purple-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-purple-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh \u0111\u1ECDc th\u1EBB g\u1EE3i \u00FD, chu\u1EA9n b\u1ECB, r\u1ED3i tr\u1EA3 l\u1EDDi b\u1EB1ng gi\u1ECDng n\u00F3i" })] }), isReviewMode && answers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "\u0110\u00E1p \u00E1n m\u1EABu" })] }), _jsx("div", { className: "space-y-2", children: answers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, `${answer}-${index}`))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-amber-700 leading-relaxed whitespace-pre-wrap", children: questionData.explanation })] }))] })] })] }));
27
+ }
@@ -0,0 +1,2 @@
1
+ import type { SpontaneousQAClientProps } from '../../_shared/types/spontaneous-qa.type';
2
+ export declare function SpontaneousQAClient({ questionData, isReviewMode, }: SpontaneousQAClientProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Mic, CheckCircle2, Info, FileQuestion, Volume2 } from 'lucide-react';
4
+ import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
5
+ function sampleAnswers(questionData) {
6
+ const fromExpected = (questionData.expectedAnswers || []).filter((item) => item.trim() !== '');
7
+ if (fromExpected.length > 0)
8
+ return fromExpected;
9
+ return (questionData.sampleAnswers || []).filter((item) => item.trim() !== '');
10
+ }
11
+ export function SpontaneousQAClient({ questionData, isReviewMode = false, }) {
12
+ const { previewUrl: questionAudioUrl } = usePresignedFileUrl(questionData.audioUrl || '');
13
+ const answers = sampleAnswers(questionData);
14
+ return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsx("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-indigo-50 to-blue-50 px-5 py-3", 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-indigo-500 to-blue-600 text-white shadow-sm", children: _jsx(Mic, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "H\u1ECFi \u0111\u00E1p t\u1EF1 ph\u00E1t" })] }) }), _jsxs("div", { className: "p-5 space-y-5", children: [questionData.questionText && (_jsxs("div", { className: "flex gap-3", children: [_jsx(FileQuestion, { className: "h-5 w-5 text-indigo-500 shrink-0 mt-0.5" }), _jsx("div", { className: "text-base font-semibold text-gray-800", children: questionData.questionText })] })), questionAudioUrl ? (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-blue-100 bg-blue-50/60 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-blue-500 shrink-0" }), _jsx("audio", { controls: true, src: questionAudioUrl, className: "h-8 w-full max-w-md", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })] })) : questionData.audioUrl ? (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-xs text-gray-400", children: "\u0110ang t\u1EA3i audio c\u00E2u h\u1ECFi..." })] })) : (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 audio c\u00E2u h\u1ECFi" })] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh nghe c\u00E2u h\u1ECFi r\u1ED3i nh\u1EA5n micro \u0111\u1EC3 tr\u1EA3 l\u1EDDi b\u1EB1ng gi\u1ECDng n\u00F3i" })] }), isReviewMode && answers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "\u0110\u00E1p \u00E1n m\u1EABu" })] }), _jsx("div", { className: "space-y-2", children: answers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, `${answer}-${index}`))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-amber-700 leading-relaxed whitespace-pre-wrap", children: questionData.explanation })] }))] })] })] }));
15
+ }
@@ -0,0 +1,2 @@
1
+ import type { SpontaneousQAClientProps } from '../../_shared/types/spontaneous-qa.type';
2
+ export declare function mapQuestionToSpontaneousQAData(question: Record<string, unknown> | null | undefined): SpontaneousQAClientProps['questionData'];
@@ -0,0 +1,33 @@
1
+ function asRecord(value) {
2
+ return value && typeof value === 'object' && !Array.isArray(value)
3
+ ? value
4
+ : {};
5
+ }
6
+ function asStringList(value) {
7
+ if (Array.isArray(value)) {
8
+ return value.filter((item) => typeof item === 'string' && item.trim() !== '');
9
+ }
10
+ if (typeof value === 'string' && value.trim())
11
+ return [value];
12
+ return [];
13
+ }
14
+ export function mapQuestionToSpontaneousQAData(question) {
15
+ const source = asRecord(question);
16
+ const content = asRecord(source.content);
17
+ const answer = asRecord(source.correctAnswer ?? source.correct_answer ?? source.answer);
18
+ const expectedAnswers = asStringList(answer.expectedAnswers).length
19
+ ? asStringList(answer.expectedAnswers)
20
+ : asStringList(answer.sampleAnswers).length
21
+ ? asStringList(answer.sampleAnswers)
22
+ : asStringList(answer.sampleAnswer);
23
+ return {
24
+ inputType: content.inputType === 'AUDIO' ? 'AUDIO' : 'TEXT',
25
+ audioUrl: typeof content.audioUrl === 'string' ? content.audioUrl : '',
26
+ questionText: typeof content.questionText === 'string' ? content.questionText : '',
27
+ expectedAnswers,
28
+ sampleAnswers: expectedAnswers,
29
+ explanation: typeof source.explanation === 'string' ? source.explanation : '',
30
+ isBackup: content.isBackup === true,
31
+ points: typeof source.points === 'number' ? source.points : undefined,
32
+ };
33
+ }
@@ -8,7 +8,7 @@ export type WfsfWordBankEditorProps = {
8
8
  optionsBank: WfsfOptionsWordBankEntry[];
9
9
  onOptionsBankChange: (next: WfsfOptionsWordBankEntry[]) => void;
10
10
  blankKeys: string[];
11
- answers: Record<string, string>;
12
- onAnswerChange: (key: string, value: string) => void;
11
+ answers: Record<string, string[]>;
12
+ onAnswerChange: (key: string, value: string[]) => void;
13
13
  };
14
14
  export declare function WfsfWordBankEditor({ mode, onModeChange, sharedBank, onSharedBankChange, optionsBank, onOptionsBankChange, blankKeys, answers, onAnswerChange, }: WfsfWordBankEditorProps): import("react").JSX.Element;