@tinyweb_dev/oe-exam-sdk 0.2.10 → 0.2.12

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.
@@ -14,6 +14,7 @@ export function MoversWordOrderingGroupRenderer({ partNumber, partName, question
14
14
  items: data.questions.map((question) => ({
15
15
  words: question.words,
16
16
  answer: question.answer,
17
+ imageUrl: question.imageUrl,
17
18
  isExample: question.isExample,
18
19
  points: question.points,
19
20
  questionNumber: question.questionNumber,
@@ -556,6 +556,7 @@ export interface WordOrderingGroupQuestionItem {
556
556
  questionNumber: number;
557
557
  words: string[];
558
558
  answer: string[];
559
+ imageUrl?: string;
559
560
  isExample: boolean;
560
561
  points: number;
561
562
  }
@@ -1424,11 +1424,13 @@ export function transformWordOrderingGroupTake(questions) {
1424
1424
  const isExample = item.isExample === true;
1425
1425
  const fromItem = Array.isArray(item.correctAnswer?.answer) ? item.correctAnswer.answer : [];
1426
1426
  const fromParent = Array.isArray(parentCorrect[index]) ? parentCorrect[index] : [];
1427
+ const imageUrl = typeof item.content?.imageUrl === 'string' ? item.content.imageUrl.trim() : '';
1427
1428
  return {
1428
1429
  id: `${parent?.id || 'q'}-item-${index}`,
1429
1430
  questionNumber: item.questionNumber || index + 1,
1430
1431
  words: Array.isArray(item.content?.words) ? item.content.words : [],
1431
1432
  answer: fromItem.length ? fromItem : fromParent,
1433
+ ...(imageUrl ? { imageUrl } : {}),
1432
1434
  isExample,
1433
1435
  points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
1434
1436
  };
@@ -4,6 +4,7 @@ export declare const WORD_ORDERING_ITEM_TYPE: "WORD_ORDERING";
4
4
  export interface WordOrderingGroupItemData {
5
5
  words: string[];
6
6
  answer: string[];
7
+ imageUrl?: string;
7
8
  isExample?: boolean;
8
9
  points: number;
9
10
  questionNumber: number;
@@ -20,9 +20,28 @@ function toItemAnswer(item) {
20
20
  caseSensitive: item.caseSensitive === true,
21
21
  };
22
22
  }
23
- export const transformFillInBlankGroup = (question) => {
23
+ /**
24
+ * wrapOnSave nests creator fields under `answer`. getFormData() (Quiz Playground
25
+ * Update save) returns the same fields at the root. Prefer nested items, then root.
26
+ */
27
+ function resolveFillInBlankGroupSource(question) {
24
28
  const answerData = question.answer;
25
- const items = Array.isArray(answerData?.items) ? answerData.items : [];
29
+ const rootData = question;
30
+ const answerItems = Array.isArray(answerData?.items) ? answerData.items : null;
31
+ const rootItems = Array.isArray(rootData.items) ? rootData.items : null;
32
+ if (answerItems && answerItems.length > 0) {
33
+ return { items: answerItems, source: answerData ?? {} };
34
+ }
35
+ if (rootItems && rootItems.length > 0) {
36
+ return { items: rootItems, source: rootData };
37
+ }
38
+ return {
39
+ items: answerItems ?? rootItems ?? [],
40
+ source: answerData ?? {},
41
+ };
42
+ }
43
+ export const transformFillInBlankGroup = (question) => {
44
+ const { items, source: answerData } = resolveFillInBlankGroupSource(question);
26
45
  if (items.length === 0) {
27
46
  return { apiContent: { items: [] }, apiCorrectAnswer: { answer: [] } };
28
47
  }
@@ -21,5 +21,6 @@ export function WordOrderingGroupClient({ questionData, isReviewMode = false, us
21
21
  answer: item.answer,
22
22
  isExample: item.isExample,
23
23
  points: item.points,
24
+ imageUrl: item.imageUrl,
24
25
  }, isReviewMode: isReviewMode || item.isExample, userAnswer: item.isExample ? item.answer : userAnswers[itemIndex], onSubmit: (ordered) => handleItemSubmit(itemIndex, ordered) })] }, `${item.questionNumber}-${itemIndex}`))), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
25
26
  }
@@ -18,18 +18,26 @@ function parsePointsValue(raw, allowZero = false) {
18
18
  const min = allowZero ? 0 : 1;
19
19
  return Math.max(min, parsed);
20
20
  }
21
- import { Eye, Pencil, Plus, Star, Trash2 } from 'lucide-react';
21
+ import { Eye, ImageIcon, Pencil, Plus, Star, Trash2 } from 'lucide-react';
22
+ import { FileUpload } from '../../../../components/ui/file-upload';
22
23
  import { WordOrderingGroupClient } from './WordOrderingGroupClient';
23
24
  import { WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/word-ordering-group.type';
24
25
  import { createEmptyWordOrderingGroupItem, mapQuestionToWordOrderingGroupData, } from './map-word-ordering-group-data';
25
- function parseTokens(value) {
26
- return value
27
- .split(/[,\n]/)
28
- .map((token) => token.trim())
29
- .filter(Boolean);
26
+ function withMinTokens(tokens, min = 2) {
27
+ if (tokens.length >= min) {
28
+ return tokens;
29
+ }
30
+ return [...tokens, ...Array.from({ length: min - tokens.length }, () => '')];
30
31
  }
31
- function joinTokens(tokens) {
32
- return tokens.join(', ');
32
+ function setTokenAt(tokens, index, value) {
33
+ const next = tokens.length > index
34
+ ? [...tokens]
35
+ : [...tokens, ...Array.from({ length: index - tokens.length + 1 }, () => '')];
36
+ next[index] = value;
37
+ return next;
38
+ }
39
+ function TokenBoxRow({ tokens, onChange, onAdd, onRemove, }) {
40
+ return (_jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-2", children: [tokens.map((token, index) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-gray-100 text-xs font-semibold text-gray-600", children: index + 1 }), _jsx(Input, { className: "h-9 w-28", value: token, onChange: (event) => onChange(index, event.target.value), placeholder: `Token ${index + 1}` }), tokens.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "h-8 w-8 text-red-500 hover:bg-red-50 hover:text-red-700", onClick: () => onRemove(index), children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, index))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: onAdd, children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), "Th\u00EAm"] })] }));
33
41
  }
34
42
  function sumPoints(items) {
35
43
  return items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0);
@@ -100,7 +108,15 @@ export function WordOrderingGroupCreator({ initialData, onChange, externalErrors
100
108
  };
101
109
  const currentPayload = buildPayload();
102
110
  const displayErrors = externalErrors && externalErrors.length > 0 ? externalErrors : errors;
103
- return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-medium text-gray-900", children: "T\u1EA1o c\u00E2u h\u1ECFi S\u1EAFp x\u1EBFp t\u1EEB (nh\u00F3m)" }), _jsx("p", { className: "text-sm text-gray-500", children: "M\u1ED7i d\u00F2ng m\u1ED9t c\u00E2u x\u00E1o tr\u1ED9n. Token c\u00E1ch nhau b\u1EB1ng d\u1EA5u ph\u1EA9y." })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsClientMode((prev) => !prev), className: "gap-2", children: [isClientMode ? _jsx(Pencil, { className: "h-4 w-4" }) : _jsx(Eye, { className: "h-4 w-4" }), isClientMode ? 'Chế độ chỉnh sửa' : 'Xem trước học sinh'] })] }), displayErrors.length > 0 && (_jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-5", children: displayErrors.map((err, i) => _jsx("li", { children: err }, i)) }) })), isClientMode ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "Xem tr\u01B0\u1EDBc hi\u1EC3n th\u1ECB h\u1ECDc sinh" }) }), _jsx(CardContent, { children: _jsx(WordOrderingGroupClient, { questionData: currentPayload }) })] })) : (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "H\u01B0\u1EDBng d\u1EABn" }) }), _jsx(CardContent, { children: _jsx(Input, { value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION }) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base font-semibold", children: ["C\u00E2u h\u1ECFi (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setItems((prev) => [...prev, createEmptyWordOrderingGroupItem(prev.length + 1)]), children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), "Th\u00EAm c\u00E2u"] })] }), _jsx(CardContent, { className: "space-y-4", children: items.map((item, index) => (_jsxs("div", { className: "space-y-3 rounded-lg border border-gray-200 p-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-sm font-medium", children: ["C\u00E2u ", item.questionNumber || index + 1] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { className: "flex items-center gap-1 text-xs", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "Example"] }), !item.isExample && (_jsx("div", { className: "w-24", children: _jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
111
+ return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-medium text-gray-900", children: "T\u1EA1o c\u00E2u h\u1ECFi S\u1EAFp x\u1EBFp t\u1EEB (nh\u00F3m)" }), _jsx("p", { className: "text-sm text-gray-500", children: "M\u1ED7i d\u00F2ng m\u1ED9t c\u00E2u x\u00E1o tr\u1ED9n. M\u1ED7i token \u0111i\u1EC1n m\u1ED9t \u00F4." })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsClientMode((prev) => !prev), className: "gap-2", children: [isClientMode ? _jsx(Pencil, { className: "h-4 w-4" }) : _jsx(Eye, { className: "h-4 w-4" }), isClientMode ? 'Chế độ chỉnh sửa' : 'Xem trước học sinh'] })] }), displayErrors.length > 0 && (_jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-5", children: displayErrors.map((err, i) => _jsx("li", { children: err }, i)) }) })), isClientMode ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "Xem tr\u01B0\u1EDBc hi\u1EC3n th\u1ECB h\u1ECDc sinh" }) }), _jsx(CardContent, { children: _jsx(WordOrderingGroupClient, { questionData: currentPayload }) })] })) : (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "H\u01B0\u1EDBng d\u1EABn" }) }), _jsx(CardContent, { children: _jsx(Input, { value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION }) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base font-semibold", children: ["C\u00E2u h\u1ECFi (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setItems((prev) => [...prev, createEmptyWordOrderingGroupItem(prev.length + 1)]), children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), "Th\u00EAm c\u00E2u"] })] }), _jsx(CardContent, { className: "space-y-4", children: items.map((item, index) => (_jsxs("div", { className: "space-y-3 rounded-lg border border-gray-200 p-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-sm font-medium", children: ["C\u00E2u ", item.questionNumber || index + 1] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { className: "flex items-center gap-1 text-xs", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "Example"] }), !item.isExample && (_jsx("div", { className: "w-24", children: _jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
104
112
  points: parsePointsValue(event.target.value, true),
105
- }) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setItems((prev) => prev.filter((_, i) => i !== index).map((entry, i) => ({ ...entry, questionNumber: i + 1 }))), children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Token x\u00E1o tr\u1ED9n" }), _jsx(Input, { className: "mt-1", value: joinTokens(item.words), onChange: (e) => updateItem(index, { words: parseTokens(e.target.value) }), placeholder: "do, you, What, like, subjects, ?" })] }), _jsxs("div", { children: [_jsx(Label, { children: "Th\u1EE9 t\u1EF1 \u0111\u00FAng" }), _jsx(Input, { className: "mt-1", value: joinTokens(item.answer), onChange: (e) => updateItem(index, { answer: parseTokens(e.target.value) }), placeholder: "What, subjects, do, you, like, ?" })] })] }, index))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gi\u1EA3i th\u00EDch" }) }), _jsx(CardContent, { children: _jsx(Textarea, { value: explanation, onChange: (event) => setExplanation(event.target.value), rows: 3 }) })] })] }))] }));
113
+ }) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setItems((prev) => prev.filter((_, i) => i !== index).map((entry, i) => ({ ...entry, questionNumber: i + 1 }))), children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-amber-500" }), "H\u00ECnh \u1EA3nh (tu\u1EF3 ch\u1ECDn)"] }), _jsx(FileUpload, { id: `word-ordering-group-image-${index}`, label: "", accept: "image/*", value: item.imageUrl || '', onChange: (url) => updateItem(index, { imageUrl: url || undefined }), maxSize: 5, placeholder: "Upload h\u00ECnh \u1EA3nh ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" })] }), _jsxs("div", { children: [_jsx(Label, { children: "Token x\u00E1o tr\u1ED9n" }), _jsx(TokenBoxRow, { tokens: withMinTokens(item.words), onChange: (tokenIndex, value) => updateItem(index, {
114
+ words: setTokenAt(item.words, tokenIndex, value),
115
+ }), onAdd: () => updateItem(index, { words: [...item.words, ''] }), onRemove: (tokenIndex) => updateItem(index, {
116
+ words: item.words.filter((_, current) => current !== tokenIndex),
117
+ }) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Th\u1EE9 t\u1EF1 \u0111\u00FAng" }), _jsx(TokenBoxRow, { tokens: withMinTokens(item.answer), onChange: (tokenIndex, value) => updateItem(index, {
118
+ answer: setTokenAt(item.answer, tokenIndex, value),
119
+ }), onAdd: () => updateItem(index, { answer: [...item.answer, ''] }), onRemove: (tokenIndex) => updateItem(index, {
120
+ answer: item.answer.filter((_, current) => current !== tokenIndex),
121
+ }) })] })] }, index))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gi\u1EA3i th\u00EDch" }) }), _jsx(CardContent, { children: _jsx(Textarea, { value: explanation, onChange: (event) => setExplanation(event.target.value), rows: 3 }) })] })] }))] }));
106
122
  }
@@ -37,9 +37,11 @@ export function mapWordOrderingGroupItem(item, index, fallbackAnswer) {
37
37
  : asStringList(record.answer).length
38
38
  ? asStringList(record.answer)
39
39
  : asStringList(fallbackAnswer);
40
+ const imageUrl = asString(content.imageUrl).trim() || asString(record.imageUrl).trim();
40
41
  return {
41
42
  words,
42
43
  answer,
44
+ ...(imageUrl ? { imageUrl } : {}),
43
45
  isExample,
44
46
  points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
45
47
  questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
@@ -13,7 +13,10 @@ export const transformWordOrderingGroup = (question) => {
13
13
  const apiItems = items.map((item, index) => ({
14
14
  questionType: WORD_ORDERING_ITEM_TYPE,
15
15
  ...(item.isExample ? { isExample: true } : {}),
16
- content: { words: tokens(item.words) },
16
+ content: {
17
+ words: tokens(item.words),
18
+ ...(item.imageUrl?.trim() ? { imageUrl: item.imageUrl.trim() } : {}),
19
+ },
17
20
  correctAnswer: { answer: tokens(item.answer) },
18
21
  points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
19
22
  questionNumber: item.questionNumber || index + 1,
@@ -4,10 +4,10 @@ import { useState, useRef, useEffect } from 'react';
4
4
  import { Button } from '../../../../components/ui/button';
5
5
  import { Label } from '../../../../components/ui/label';
6
6
  import { Input } from '../../../../components/ui/input';
7
- import { Textarea } from '../../../../components/ui/textarea';
8
7
  import { PointsInput } from '../../../../components/ui/points-input';
9
8
  import { FileUpload } from '../../../../components/ui/file-upload';
10
9
  import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
10
+ import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
11
11
  import { Pencil, Eye, Plus, Trash2, Lightbulb, Mail, ImageIcon, X } from 'lucide-react';
12
12
  import { WriteAShortLetterClient } from './WriteAShortLetterClient';
13
13
  import { useReactHookForm } from '../../../../shared/lib/hooks/useReactHookForm';
@@ -316,10 +316,10 @@ function WriteAShortLetterCreatorContent({ initialData, onChange, externalErrors
316
316
  setMaxWords(numValue < 1 ? 1 : numValue);
317
317
  }
318
318
  form.clearErrors('maxWords');
319
- }, placeholder: "\u221E", className: `mt-1 ${errors.maxWords ? 'border-red-500 focus:ring-red-500' : ''}` }), errors.maxWords && (_jsx("p", { className: "mt-1 text-xs text-red-600", children: errors.maxWords }))] })] })] })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-medium", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Textarea, { id: "instruction", value: instruction, onChange: (e) => {
320
- setInstruction(e.target.value);
321
- form.clearErrors('instruction');
322
- }, placeholder: "V\u00ED d\u1EE5: You are having a birthday party. Write a short letter to your friend, Minh, to invite him. Tell him about the party.", className: `mt-1.5 resize-none ${errors.instruction ? 'border-red-500' : ''}`, rows: 3 }), errors.instruction && (_jsx("p", { className: "mt-1 text-sm text-red-600", children: errors.instruction }))] }), _jsxs("div", { className: "space-y-3 rounded-lg border border-blue-100 bg-blue-50/30 p-4", 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-full bg-blue-100", children: _jsx("span", { className: "text-xs font-semibold text-blue-600", children: "i" }) }), _jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "Th\u00F4ng tin c\u1EA7n bao g\u1ED3m" })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addInformation, className: "h-8 gap-1.5 border-blue-200 bg-white text-blue-600 hover:bg-blue-50 hover:text-blue-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm"] })] }), errors.informations && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-3", children: _jsx("p", { className: "text-sm text-red-600", children: errors.informations }) })), _jsx("div", { className: "space-y-2", children: informations.map((info, index) => (_jsx("div", { className: "space-y-1", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600", children: index + 1 }), _jsx(Input, { value: info, onChange: (e) => updateInformation(index, e.target.value), placeholder: `Thông tin ${index + 1}...`, className: "flex-1 border-gray-200" }), informations.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeInformation(index), className: "h-8 w-8 flex-shrink-0 p-0 text-gray-400 hover:bg-red-50 hover:text-red-500", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }) }, index))) }), _jsx("p", { className: "text-xs text-gray-500 italic", children: "\uD83D\uDCA1 Li\u1EC7t k\u00EA c\u00E1c th\u00F4ng tin h\u1ECDc sinh c\u1EA7n \u0111\u1EC1 c\u1EADp trong b\u00E0i vi\u1EBFt" })] }), _jsxs("div", { className: "space-y-3 rounded-lg border border-violet-100 bg-violet-50/30 p-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-full bg-violet-100", children: _jsx(ImageIcon, { className: "h-3.5 w-3.5 text-violet-600" }) }), _jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "\u1EA2nh h\u01B0\u1EDBng d\u1EABn" }), _jsx("span", { className: "text-xs text-gray-400" })] }), guideImages.length > 0 && (_jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4", children: guideImages.map((imageKey, index) => (_jsx(GuideImageItem, { imageKey: imageKey, index: index, localPreviewUrl: guideImagePreviewUrls[imageKey], onRemove: removeGuideImage }, `${imageKey}-${index}`))) })), _jsx(FileUpload, { label: "Th\u00EAm \u1EA3nh h\u01B0\u1EDBng d\u1EABn", accept: "image/*", value: "", onChange: handleGuideImageUpload, onPresignedUrlChange: handleGuideImagePresignedUrl, autoUpload: true, prefix: "write-short-letter/guide", placeholder: "Ch\u1ECDn ho\u1EB7c k\u00E9o th\u1EA3 \u1EA3nh v\u00E0o \u0111\u00E2y", icon: _jsx(ImageIcon, { className: "h-4 w-4" }) }), _jsx("p", { className: "text-xs text-gray-500 italic", children: "\uD83D\uDCF7 Upload nhi\u1EC1u \u1EA3nh \u0111\u1EC3 h\u01B0\u1EDBng d\u1EABn h\u1ECDc sinh vi\u1EBFt b\u00E0i (v\u00ED d\u1EE5: m\u1EABu th\u01B0, h\u00ECnh \u1EA3nh minh h\u1ECDa)" })] })] })] })] }));
319
+ }, placeholder: "\u221E", className: `mt-1 ${errors.maxWords ? 'border-red-500 focus:ring-red-500' : ''}` }), errors.maxWords && (_jsx("p", { className: "mt-1 text-xs text-red-600", children: errors.maxWords }))] })] })] })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-medium", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx("div", { className: `mt-1.5 ${errors.instruction ? 'rounded-md ring-1 ring-red-500' : ''}`, children: _jsx(RichTextEditor, { value: instruction, onChange: (html) => {
320
+ setInstruction(html);
321
+ form.clearErrors('instruction');
322
+ }, minHeightClassName: "min-h-[120px]" }) }), errors.instruction && (_jsx("p", { className: "mt-1 text-sm text-red-600", children: errors.instruction }))] }), _jsxs("div", { className: "space-y-3 rounded-lg border border-blue-100 bg-blue-50/30 p-4", 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-full bg-blue-100", children: _jsx("span", { className: "text-xs font-semibold text-blue-600", children: "i" }) }), _jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "Th\u00F4ng tin c\u1EA7n bao g\u1ED3m" })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addInformation, className: "h-8 gap-1.5 border-blue-200 bg-white text-blue-600 hover:bg-blue-50 hover:text-blue-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm"] })] }), errors.informations && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-3", children: _jsx("p", { className: "text-sm text-red-600", children: errors.informations }) })), _jsx("div", { className: "space-y-2", children: informations.map((info, index) => (_jsx("div", { className: "space-y-1", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600", children: index + 1 }), _jsx(Input, { value: info, onChange: (e) => updateInformation(index, e.target.value), placeholder: `Thông tin ${index + 1}...`, className: "flex-1 border-gray-200" }), informations.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeInformation(index), className: "h-8 w-8 flex-shrink-0 p-0 text-gray-400 hover:bg-red-50 hover:text-red-500", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }) }, index))) }), _jsx("p", { className: "text-xs text-gray-500 italic", children: "\uD83D\uDCA1 Li\u1EC7t k\u00EA c\u00E1c th\u00F4ng tin h\u1ECDc sinh c\u1EA7n \u0111\u1EC1 c\u1EADp trong b\u00E0i vi\u1EBFt" })] }), _jsxs("div", { className: "space-y-3 rounded-lg border border-violet-100 bg-violet-50/30 p-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-full bg-violet-100", children: _jsx(ImageIcon, { className: "h-3.5 w-3.5 text-violet-600" }) }), _jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "\u1EA2nh h\u01B0\u1EDBng d\u1EABn" }), _jsx("span", { className: "text-xs text-gray-400" })] }), guideImages.length > 0 && (_jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4", children: guideImages.map((imageKey, index) => (_jsx(GuideImageItem, { imageKey: imageKey, index: index, localPreviewUrl: guideImagePreviewUrls[imageKey], onRemove: removeGuideImage }, `${imageKey}-${index}`))) })), _jsx(FileUpload, { label: "Th\u00EAm \u1EA3nh h\u01B0\u1EDBng d\u1EABn", accept: "image/*", value: "", onChange: handleGuideImageUpload, onPresignedUrlChange: handleGuideImagePresignedUrl, autoUpload: true, prefix: "write-short-letter/guide", placeholder: "Ch\u1ECDn ho\u1EB7c k\u00E9o th\u1EA3 \u1EA3nh v\u00E0o \u0111\u00E2y", icon: _jsx(ImageIcon, { className: "h-4 w-4" }) }), _jsx("p", { className: "text-xs text-gray-500 italic", children: "\uD83D\uDCF7 Upload nhi\u1EC1u \u1EA3nh \u0111\u1EC3 h\u01B0\u1EDBng d\u1EABn h\u1ECDc sinh vi\u1EBFt b\u00E0i (v\u00ED d\u1EE5: m\u1EABu th\u01B0, h\u00ECnh \u1EA3nh minh h\u1ECDa)" })] })] })] })] }));
323
323
  }
324
324
  export function WriteAShortLetterCreator(props) {
325
325
  return _jsx(WriteAShortLetterCreatorContent, { ...props });
@@ -627,9 +627,11 @@ const reverseWordOrderingGroup = (apiQuestion) => {
627
627
  const words = asArray(itemContent.words).map((token) => asString(token)).filter(Boolean);
628
628
  const itemTokens = asArray(itemAnswer.answer).map((token) => asString(token)).filter(Boolean);
629
629
  const fallbackTokens = asArray(fallbackAnswers[index]).map((token) => asString(token)).filter(Boolean);
630
+ const imageUrl = asString(itemContent.imageUrl).trim();
630
631
  return {
631
632
  words,
632
633
  answer: itemTokens.length ? itemTokens : fallbackTokens,
634
+ ...(imageUrl ? { imageUrl } : {}),
633
635
  isExample,
634
636
  points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
635
637
  questionNumber: typeof item.questionNumber === 'number' ? item.questionNumber : index + 1,
@@ -4,6 +4,7 @@ export interface WordOrderingGroupItem {
4
4
  isExample?: boolean;
5
5
  content: {
6
6
  words: string[];
7
+ imageUrl?: string;
7
8
  };
8
9
  correctAnswer?: {
9
10
  answer: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyweb_dev/oe-exam-sdk",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Reusable OceanEdu question components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",