@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.
- package/dist/api/exam-taking.types.d.ts +1 -0
- package/dist/components/exams/ExamPreviewDialog.js +4 -2
- package/dist/components/exams/take/components/QuestionRenderer.js +2 -2
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +3 -1
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +5 -2
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +13 -20
- package/dist/components/exams/take/exam-taking.utils.d.ts +3 -2
- package/dist/components/exams/take/exam-taking.utils.js +3 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +11 -3
- package/dist/components/exams/take/utils/question-transformers.js +45 -2
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.d.ts +6 -2
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +26 -5
- package/dist/components/questions/_shared/types/fill-in-blank.type.d.ts +9 -0
- package/dist/components/questions/_shared/types/question-group.type.d.ts +11 -0
- package/dist/components/questions/_shared/types/question-group.type.js +4 -1
- package/dist/components/questions/_shared/types/spontaneous-qa.type.d.ts +7 -0
- package/dist/components/questions/_shared/types/word-fill-structured-form.type.d.ts +1 -1
- package/dist/components/questions/groups/ImageLabelGroupCard.d.ts +15 -0
- package/dist/components/questions/groups/ImageLabelGroupCard.js +42 -0
- package/dist/components/questions/groups/ImageLabelWordBank.d.ts +10 -0
- package/dist/components/questions/groups/ImageLabelWordBank.js +17 -0
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +33 -9
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +6 -2
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +28 -5
- package/dist/components/questions/types/fill-in-blank/register.js +5 -0
- package/dist/components/questions/types/fill-in-blank/transform.js +4 -1
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.d.ts +11 -0
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +27 -0
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQAClient.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQAClient.js +15 -0
- package/dist/components/questions/types/spontaneous-qa/map-spontaneous-qa-data.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa/map-spontaneous-qa-data.js +33 -0
- package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.d.ts +2 -2
- package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.js +15 -5
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +39 -15
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +35 -12
- package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +2 -10
- package/dist/components/questions/types/word-fill-structured-form/transform.js +2 -1
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +51 -33
- package/dist/components/questions/viewer/QuestionViewer.js +12 -68
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +3 -1
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +4 -2
- package/dist/components/results/renderers/review-question-body-dedicated.js +2 -2
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +12 -3
- package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +13 -20
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +8 -0
- package/dist/shared/constants/routes.d.ts +2 -2
- package/dist/shared/constants/routes.js +3 -3
- package/dist/shared/lib/rich-text.d.ts +4 -0
- package/dist/shared/lib/rich-text.js +25 -0
- package/dist/shared/lib/stores/examQuestionStore.d.ts +4 -0
- package/dist/shared/lib/utils/question-transform-types.d.ts +4 -0
- package/dist/shared/lib/utils/question-transform.js +1 -1
- package/dist/shared/lib/utils/wfsf-blank-match.d.ts +9 -0
- package/dist/shared/lib/utils/wfsf-blank-match.js +60 -0
- package/package.json +1 -1
- package/dist/components/themes/cambridge-yle/assets/images/bg-title-old.png +0 -0
- package/dist/components/themes/cambridge-yle/assets/images/border-old.png +0 -0
- package/dist/components/themes/cambridge-yle/assets/images/close_old.png +0 -0
- 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
|
+
}
|
package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js
CHANGED
|
@@ -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"] })
|
|
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(
|
|
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
|
-
},
|
|
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(
|
|
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
|
-
},
|
|
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') }) }))] })] }))
|
|
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
|
|
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
|
|
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
|
|
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,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,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;
|