@tinyweb_dev/oe-exam-sdk 0.2.11 → 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;
@@ -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,7 +18,8 @@ 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';
@@ -109,7 +110,7 @@ export function WordOrderingGroupCreator({ initialData, onChange, externalErrors
109
110
  const displayErrors = externalErrors && externalErrors.length > 0 ? externalErrors : errors;
110
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, {
111
112
  points: parsePointsValue(event.target.value, true),
112
- }) }) })), 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(TokenBoxRow, { tokens: withMinTokens(item.words), onChange: (tokenIndex, value) => updateItem(index, {
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, {
113
114
  words: setTokenAt(item.words, tokenIndex, value),
114
115
  }), onAdd: () => updateItem(index, { words: [...item.words, ''] }), onRemove: (tokenIndex) => updateItem(index, {
115
116
  words: item.words.filter((_, current) => current !== tokenIndex),
@@ -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,
@@ -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.11",
3
+ "version": "0.2.12",
4
4
  "description": "Reusable OceanEdu question components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",