@tinyweb_dev/oe-exam-sdk 1.0.3 → 1.0.5

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.
@@ -23,7 +23,8 @@ import { SummerSkySubmitModal } from '../../../components/themes/summer-sky/Summ
23
23
  import { SummerSkyExitModal } from '../../../components/themes/summer-sky/SummerSkyExitModal';
24
24
  import { EnglishCertificationExamLayout, EnglishCertificationQuestionRenderer, scrollToEnglishCertQuestion, } from '../../../components/themes/english-certification';
25
25
  import { groupQuestionsByPart, } from './utils/question-transformers';
26
- import { findSidebarQuestionForPart, getCambridgeYlePartQuestionCount, getPartInitialQuestionIndex, isCambridgeYleInlineSpeakingType, listSpontaneousQaGroupChildIds, } from './utils/cambridge-yle-inline-speaking';
26
+ import { findSidebarQuestionForPart, getCambridgeYlePartQuestionCount, getPartInitialQuestionIndex, isCambridgeYleInlineSpeakingType, isTextOnlySpontaneousQaGroupContent, listSpontaneousQaGroupChildIds, } from './utils/cambridge-yle-inline-speaking';
27
+ import { hasFilledSpontaneousQaGroupTextAnswer } from '../../questions/types/spontaneous-qa-group/flatten';
27
28
  import { unwrap, unwrapAttempt } from './exam-taking.utils';
28
29
  import { ExamTakingApiProvider } from './ExamTakingApiContext';
29
30
  function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (attemptId) => `/student/results/${attemptId}`, fallbackPath = '/student/contests/discover', proctoringEnabled = false, achieversExamTitle, onSubmitted, }) {
@@ -665,9 +666,14 @@ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (a
665
666
  if (part.questionType === 'SPONTANEOUS_QA_GROUP') {
666
667
  const parent = part.questions[0];
667
668
  if (parent && !parent.is_example) {
668
- listSpontaneousQaGroupChildIds(parent.id, parent.content).forEach((id) => {
669
- countableQuestionIds.add(id);
670
- });
669
+ if (isTextOnlySpontaneousQaGroupContent(parent.content)) {
670
+ countableQuestionIds.add(parent.id);
671
+ }
672
+ else {
673
+ listSpontaneousQaGroupChildIds(parent.id, parent.content).forEach((id) => {
674
+ countableQuestionIds.add(id);
675
+ });
676
+ }
671
677
  }
672
678
  return;
673
679
  }
@@ -681,6 +687,12 @@ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (a
681
687
  const answer = store.answers[key];
682
688
  if (answer === undefined || answer === '')
683
689
  return;
690
+ if (typeof answer === 'object' &&
691
+ answer !== null &&
692
+ 'items' in answer &&
693
+ !hasFilledSpontaneousQaGroupTextAnswer(answer)) {
694
+ return;
695
+ }
684
696
  // Direct question ID match
685
697
  if (countableQuestionIds.has(key)) {
686
698
  answeredQuestions.add(key);
@@ -713,7 +725,8 @@ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (a
713
725
  const parent = part.questions[0];
714
726
  const shouldFlattenGroup = examTheme === 'CAMBRIDGE_YLE' &&
715
727
  part.questionType === 'SPONTANEOUS_QA_GROUP' &&
716
- !!parent;
728
+ !!parent &&
729
+ !isTextOnlySpontaneousQaGroupContent(parent.content);
717
730
  const questionsToList = shouldFlattenGroup
718
731
  ? listSpontaneousQaGroupChildIds(parent.id, parent.content).map((childId, childIndex) => ({
719
732
  id: childId,
@@ -14,6 +14,8 @@ import { SpeakingInfoExchangeRenderer } from './SpeakingInfoExchangeRenderer';
14
14
  import { SpeakingReadDisplayedContentRenderer } from './SpeakingReadDisplayedContentRenderer';
15
15
  import { SpeakingSpontaneousQARenderer } from './SpeakingSpontaneousQARenderer';
16
16
  import { SpeakingSpontaneousQaGroupRenderer } from './SpeakingSpontaneousQaGroupRenderer';
17
+ import { SpeakingSpontaneousQaGroupTextList } from './SpeakingSpontaneousQaGroupTextList';
18
+ import { isTextOnlySpontaneousQaGroupContent } from '../../../../../questions/types/spontaneous-qa-group/flatten';
17
19
  import { SpeakingConversationRenderer } from './SpeakingConversationRenderer';
18
20
  import { SpeakingGNInterviewRenderer } from './SpeakingGNInterviewRenderer';
19
21
  import { SpeakingCueCardRenderer } from './SpeakingCueCardRenderer';
@@ -23,6 +25,10 @@ import { SpeakingCueCardRenderer } from './SpeakingCueCardRenderer';
23
25
  export function SpeakingQuestionRenderer({ part, answers, onAnswerChange, isReviewMode = false, onExit, onPartComplete, onPartBack, speakingTheme, initialQuestionIndex, skipTeacherVideo, onTeacherIntroPlayed, currentQuestionIndex, }) {
24
26
  const handleExit = onExit ?? (() => { if (typeof window !== 'undefined')
25
27
  window.history.back(); });
28
+ if (part.questionType === 'SPONTANEOUS_QA_GROUP' &&
29
+ isTextOnlySpontaneousQaGroupContent(part.questions[0]?.content)) {
30
+ return (_jsx(SpeakingSpontaneousQaGroupTextList, { part: part, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
31
+ }
26
32
  // Determine if teacher video should play:
27
33
  // Only play on first visit to this part (skipTeacherVideo=false).
28
34
  const shouldPlayVideo = !skipTeacherVideo && shouldWaitForTeacherVideo(part.questionType);
@@ -0,0 +1,9 @@
1
+ import type { QuestionPart } from '../../../utils/question-transformers';
2
+ interface SpeakingSpontaneousQaGroupTextListProps {
3
+ part: QuestionPart;
4
+ answers: Record<string, unknown>;
5
+ onAnswerChange: (questionId: string, value: unknown) => void;
6
+ isReviewMode?: boolean;
7
+ }
8
+ export declare function SpeakingSpontaneousQaGroupTextList({ part, answers, onAnswerChange, isReviewMode, }: SpeakingSpontaneousQaGroupTextListProps): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Lightbulb } from 'lucide-react';
4
+ import { Textarea } from '../../../../../../components/ui/textarea';
5
+ import { flattenSpontaneousQaGroupContent, getSpontaneousQaGroupInstructionFromContent, } from '../../../../../questions/types/spontaneous-qa-group/flatten';
6
+ function isRecord(value) {
7
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
8
+ }
9
+ function readItemAnswers(parentId, answers, childCount) {
10
+ const parent = answers[parentId];
11
+ const out = {};
12
+ if (isRecord(parent)) {
13
+ const nested = parent.items;
14
+ const source = isRecord(nested) ? nested : parent;
15
+ for (const [key, value] of Object.entries(source)) {
16
+ if (key === 'items')
17
+ continue;
18
+ if (typeof value === 'string')
19
+ out[key] = value;
20
+ }
21
+ }
22
+ for (let index = 0; index < childCount; index += 1) {
23
+ const key = String(index + 1);
24
+ if (out[key]?.trim())
25
+ continue;
26
+ const synthetic = answers[`${parentId}-item-${index}`];
27
+ if (typeof synthetic === 'string')
28
+ out[key] = synthetic;
29
+ }
30
+ return out;
31
+ }
32
+ export function SpeakingSpontaneousQaGroupTextList({ part, answers, onAnswerChange, isReviewMode = false, }) {
33
+ const parent = part.questions[0];
34
+ if (!parent)
35
+ return null;
36
+ const children = flattenSpontaneousQaGroupContent(parent.content);
37
+ const instruction = getSpontaneousQaGroupInstructionFromContent(parent.content, part.instructions ?? '');
38
+ const itemAnswers = readItemAnswers(parent.id, answers, children.length);
39
+ const handleItemChange = (key, next) => {
40
+ if (isReviewMode)
41
+ return;
42
+ onAnswerChange(parent.id, { items: { ...itemAnswers, [key]: next } });
43
+ };
44
+ if (children.length === 0) {
45
+ return _jsx("p", { className: "text-sm text-slate-500", children: "C\u00E2u h\u1ECFi n\u00E0y ch\u01B0a c\u00F3 c\u1EE5m \u0111\u1EC3 l\u00E0m." });
46
+ }
47
+ return (_jsxs("div", { className: "space-y-4", children: [instruction ? (_jsxs("div", { className: "flex items-start gap-2 rounded-2xl border-2 border-amber-200 bg-amber-50 px-3 py-2.5", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-900", children: instruction })] })) : null, _jsx("ol", { className: "space-y-3", children: children.map((child, index) => {
48
+ const key = String(index + 1);
49
+ const sectionTitle = child.partTitle || child.topic || '';
50
+ const prev = children[index - 1];
51
+ const prevSection = prev ? prev.partTitle || prev.topic || '' : '';
52
+ const showSection = Boolean(sectionTitle) && sectionTitle !== prevSection;
53
+ return (_jsxs("li", { className: "space-y-2", children: [showSection ? (_jsx("p", { className: "text-[11px] font-bold uppercase tracking-wide text-sky-700", children: sectionTitle })) : null, _jsxs("div", { className: "rounded-2xl border-2 border-slate-100 bg-white p-3", children: [_jsxs("p", { className: "text-sm font-extrabold text-[#2C38B6]", children: [_jsxs("span", { className: "mr-2 text-xs font-bold text-slate-400", children: [index + 1, "."] }), child.questionText || '—'] }), _jsx(Textarea, { className: "mt-2 min-h-[80px] rounded-xl border-slate-200", value: itemAnswers[key] ?? '', onChange: (event) => handleItemChange(key, event.target.value), placeholder: "Nh\u1EADp c\u00E2u tr\u1EA3 l\u1EDDi\u2026", readOnly: isReviewMode, disabled: isReviewMode, "aria-label": `Câu ${index + 1}` })] })] }, `${index}-${key}`));
54
+ }) })] }));
55
+ }
@@ -11,4 +11,5 @@ export { SpeakingInfoExchangeRenderer } from './SpeakingInfoExchangeRenderer';
11
11
  export { SpeakingReadDisplayedContentRenderer } from './SpeakingReadDisplayedContentRenderer';
12
12
  export { SpeakingSpontaneousQARenderer } from './SpeakingSpontaneousQARenderer';
13
13
  export { SpeakingSpontaneousQaGroupRenderer } from './SpeakingSpontaneousQaGroupRenderer';
14
+ export { SpeakingSpontaneousQaGroupTextList } from './SpeakingSpontaneousQaGroupTextList';
14
15
  export { SpeakingQuestionRenderer } from './SpeakingQuestionRenderer';
@@ -11,4 +11,5 @@ export { SpeakingInfoExchangeRenderer } from './SpeakingInfoExchangeRenderer';
11
11
  export { SpeakingReadDisplayedContentRenderer } from './SpeakingReadDisplayedContentRenderer';
12
12
  export { SpeakingSpontaneousQARenderer } from './SpeakingSpontaneousQARenderer';
13
13
  export { SpeakingSpontaneousQaGroupRenderer } from './SpeakingSpontaneousQaGroupRenderer';
14
+ export { SpeakingSpontaneousQaGroupTextList } from './SpeakingSpontaneousQaGroupTextList';
14
15
  export { SpeakingQuestionRenderer } from './SpeakingQuestionRenderer';
@@ -1,3 +1,4 @@
1
+ export { isTextOnlySpontaneousQaGroupContent } from '../../../questions/types/spontaneous-qa-group/flatten';
1
2
  export declare const CAMBRIDGE_YLE_INLINE_SPEAKING_TYPES: readonly ["SPEAKING_DESCRIBE_IMAGE", "SPONTANEOUS_QA", "SPONTANEOUS_QA_GROUP", "SPEAKING_CUE_CARD", "READ_DISPLAYED_CONTENT"];
2
3
  export type CambridgeYleInlineSpeakingType = (typeof CAMBRIDGE_YLE_INLINE_SPEAKING_TYPES)[number];
3
4
  export declare function isCambridgeYleInlineSpeakingType(questionType: string | undefined): questionType is CambridgeYleInlineSpeakingType;
@@ -1,4 +1,5 @@
1
- import { flattenSpontaneousQaGroupContent } from '../../../questions/types/spontaneous-qa-group/flatten';
1
+ import { flattenSpontaneousQaGroupContent, isTextOnlySpontaneousQaGroupContent, } from '../../../questions/types/spontaneous-qa-group/flatten';
2
+ export { isTextOnlySpontaneousQaGroupContent } from '../../../questions/types/spontaneous-qa-group/flatten';
2
3
  export const CAMBRIDGE_YLE_INLINE_SPEAKING_TYPES = [
3
4
  'SPEAKING_DESCRIBE_IMAGE',
4
5
  'SPONTANEOUS_QA',
@@ -22,6 +23,9 @@ export function listSpontaneousQaGroupChildIds(parentId, content) {
22
23
  }
23
24
  export function getCambridgeYlePartQuestionCount(part) {
24
25
  if (part.questionType === 'SPONTANEOUS_QA_GROUP') {
26
+ if (isTextOnlySpontaneousQaGroupContent(part.questions[0]?.content)) {
27
+ return Math.max(part.questions.length, 1);
28
+ }
25
29
  return getSpontaneousQaGroupChildCount(part.questions[0]?.content);
26
30
  }
27
31
  return part.questions.length;
@@ -12,3 +12,7 @@ export interface SpontaneousQaGroupFlatChild {
12
12
  points: number;
13
13
  }
14
14
  export declare function flattenSpontaneousQaGroupContent(content: unknown): SpontaneousQaGroupFlatChild[];
15
+ /** Speaking Bank / translate lists — no teacher video or one-at-a-time mic. */
16
+ export declare function isTextOnlySpontaneousQaGroupContent(content: unknown): boolean;
17
+ export declare function getSpontaneousQaGroupInstructionFromContent(content: unknown, fallback?: string): string;
18
+ export declare function hasFilledSpontaneousQaGroupTextAnswer(value: unknown): boolean;
@@ -67,3 +67,22 @@ export function flattenSpontaneousQaGroupContent(content) {
67
67
  });
68
68
  return flattened;
69
69
  }
70
+ /** Speaking Bank / translate lists — no teacher video or one-at-a-time mic. */
71
+ export function isTextOnlySpontaneousQaGroupContent(content) {
72
+ const children = flattenSpontaneousQaGroupContent(content);
73
+ return (children.length > 0 &&
74
+ children.every((child) => child.inputType !== 'AUDIO'));
75
+ }
76
+ export function getSpontaneousQaGroupInstructionFromContent(content, fallback = '') {
77
+ const root = asRecord(content);
78
+ const meta = asRecord(root.meta);
79
+ const instruction = asString(root.instruction) || asString(meta.instruction);
80
+ return instruction.trim() || fallback;
81
+ }
82
+ export function hasFilledSpontaneousQaGroupTextAnswer(value) {
83
+ if (!isRecord(value))
84
+ return false;
85
+ const nested = value.items;
86
+ const source = isRecord(nested) ? nested : value;
87
+ return Object.entries(source).some(([key, entry]) => key !== 'items' && typeof entry === 'string' && entry.trim().length > 0);
88
+ }
@@ -99,6 +99,19 @@ const ColorToolbarBtn = ({ editor }) => {
99
99
  : 'text-gray-600 hover:bg-gray-200 hover:text-gray-900'}`, children: [_jsx(Palette, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "h-3.5 w-3.5 rounded-full border border-gray-300 shadow-sm", style: { backgroundColor: currentColor || '#0f172a' } })] }), showMenu && (_jsxs("div", { className: "absolute left-0 top-full z-50 mt-1 flex flex-col gap-2 rounded-lg border border-gray-200 bg-white p-2.5 shadow-xl min-w-[170px]", children: [_jsx("div", { className: "text-[11px] font-medium text-gray-500 px-0.5", children: "M\u00E0u ch\u1EEF nhanh" }), _jsx("div", { className: "grid grid-cols-4 gap-1.5", children: COLOR_PRESETS.map((p) => (_jsx("button", { type: "button", title: p.label, onClick: () => handleSetColor(p.color), className: "h-6 w-6 rounded-full border border-gray-200 shadow-xs transition-transform hover:scale-110 focus:outline-none", style: { backgroundColor: p.color } }, p.color))) }), _jsxs("div", { className: "flex items-center justify-between gap-2 pt-1.5 border-t border-gray-100", children: [_jsxs("label", { className: "flex items-center gap-1.5 text-[11px] text-gray-600 cursor-pointer", children: [_jsx("span", { children: "T\u00F9y ch\u1ECDn:" }), _jsx("input", { type: "color", value: currentColor || '#000000', onChange: (e) => handleSetColor(e.target.value), className: "h-5 w-5 cursor-pointer rounded border-0 p-0" })] }), _jsx("button", { type: "button", onClick: () => handleSetColor(''), className: "text-[11px] font-medium text-red-500 hover:underline", children: "M\u1EB7c \u0111\u1ECBnh" })] })] }))] }));
100
100
  };
101
101
  const ToolbarSep = () => _jsx("div", { className: "mx-1 h-4 w-px bg-gray-200" });
102
+ const BLANK_STYLE_OPTIONS = [
103
+ { value: 'letter', label: 'Letter', hint: 'Ô 1 ký tự' },
104
+ { value: 'word', label: 'Word', hint: 'Từ ngắn' },
105
+ { value: 'phrase', label: 'Phrase', hint: 'Cụm / câu' },
106
+ ];
107
+ function BlankStyleSwitch({ value, onChange, }) {
108
+ return (_jsxs("div", { role: "radiogroup", "aria-label": "Ki\u1EC3u \u00F4 tr\u1ED1ng", className: "inline-flex flex-wrap items-center gap-1 rounded-md border border-gray-200 bg-white p-1", children: [BLANK_STYLE_OPTIONS.map((option) => {
109
+ const selected = value === option.value;
110
+ return (_jsxs("button", { type: "button", role: "radio", "aria-checked": selected, title: option.hint, onClick: () => onChange(option.value), className: `inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-sm font-medium transition-colors ${selected
111
+ ? 'border border-teal-300 bg-teal-50 text-teal-700'
112
+ : 'border border-transparent text-gray-400 hover:bg-gray-50 hover:text-gray-600'}`, children: [option.label, _jsx("span", { className: `text-[10px] font-normal ${selected ? 'text-teal-600' : 'text-gray-400'}`, children: option.hint })] }, option.value));
113
+ }), value === 'translate' && (_jsxs("span", { role: "radio", "aria-checked": true, title: "Ch\u1EA5m LLM \u2014 gi\u1EEF nguy\u00EAn \u0111\u1EBFn khi ch\u1ECDn style kh\u00E1c", className: "inline-flex items-center gap-1.5 rounded border border-teal-300 bg-teal-50 px-2.5 py-1 text-sm font-medium text-teal-700", children: ["Translate", _jsx("span", { className: "text-[10px] font-normal text-teal-600", children: "D\u1ECBch (LLM)" })] }))] }));
114
+ }
102
115
  const EditorToolbar = ({ editor }) => {
103
116
  if (!editor)
104
117
  return null;
@@ -126,7 +139,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
126
139
  // Page rubric (Pearson draft.instruction). Prefer instruction; legacy load may use explanation.
127
140
  const [instruction, setInstruction] = useState(initialData?.instruction || initialData?.explanation || '');
128
141
  const [explanation, setExplanation] = useState(initialData?.explanation || '');
129
- const [blankStyle, setBlankStyle] = useState(initialData?.blankStyle);
142
+ const [blankStyle, setBlankStyle] = useState(initialData?.blankStyle ?? 'word');
130
143
  // ── Multiple Images & Audios ────────────────────────────────────────────────
131
144
  const [imageUrls, setImageUrls] = useState(() => {
132
145
  if (Array.isArray(initialData?.imageUrls) && initialData.imageUrls.length > 0) {
@@ -250,7 +263,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
250
263
  : '');
251
264
  }
252
265
  setAnswers(normalizeWfsfAnswersMap(initialData?.answers || {}));
253
- setBlankStyle(initialData?.blankStyle);
266
+ setBlankStyle(initialData?.blankStyle ?? 'word');
254
267
  const nextMode = detectWfsfBankMode(initialData?.viewMode ?? viewModeProp, initialData?.wordBank);
255
268
  setBankMode(nextMode);
256
269
  if (nextMode === 'WITH_WORD_BANK') {
@@ -481,7 +494,7 @@ export function WordFillStructuredFormCreator({ initialData, onSave, onCancel, o
481
494
  return (_jsxs("div", { className: "space-y-4", children: [isGrouped && effectiveGroupType === 'BASIC' && (_jsx(BasicQuestionGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `word-fill-structured-${partId ?? 'group'}` })), isGrouped && effectiveGroupType === 'DOCUMENT' && (_jsx(DocumentGroupCard, { groupNumber: currentGroupNumber, title: groupTitle, subtitle: groupSubtitle, content: groupContent, imageUrl: groupImageUrl, onTitleChange: setGroupTitle, onSubtitleChange: setGroupSubtitle, onContentChange: setGroupContent, onImageChange: setGroupImageUrl, groupConfig: groupType ? groupConfig : undefined, isReadOnly: !isFirstInGroup, uploadIdPrefix: `word-fill-structured-${partId ?? 'document'}` })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-teal-500 via-cyan-500 to-emerald-500" }), _jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-100", children: _jsx(PenLine, { className: "h-4 w-4 text-teal-600" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "\u0110i\u1EC1n t\u1EEB v\u00E0o bi\u1EC3u m\u1EABu c\u00F3 c\u1EA5u tr\u00FAc" }), _jsxs("p", { className: "text-xs text-gray-500", children: ["Nh\u1EADp n\u1ED9i dung, \u0111\u1EB7t ch\u1ED7 tr\u1ED1ng b\u1EB1ng ", `{0}`, " ", `{1}`, "\u2026 r\u1ED3i \u0111i\u1EC1n \u0111\u00E1p \u00E1n b\u00EAn d\u01B0\u1EDBi"] })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsPreviewMode(true), disabled: expectedBlankKeys.length === 0 || expectedBlankKeys.some(k => !hasWfsfBlankAnswer(answers[k])), className: "gap-2 border-teal-200 text-teal-600 hover:bg-teal-50 hover:text-teal-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: "space-y-2", children: [_jsxs(Label, { htmlFor: "wfsf-instruction", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(Info, { className: "h-4 w-4 text-teal-500" }), "Instruction", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(h\u01B0\u1EDBng d\u1EABn b\u00E0i t\u1EADp)" })] }), _jsx(Textarea, { id: "wfsf-instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: "VD: Complete the words in the sentences. Use the words in the box.", rows: 2, className: "min-h-[64px] resize-y border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx(PointsInput, { value: points, onChange: (e) => setPoints(Number(e.target.value) || 1) }), _jsxs("div", { role: "button", tabIndex: 0, onClick: () => setCaseSensitive(!caseSensitive), onKeyDown: (e) => { if (e.key === ' ' || e.key === 'Enter')
482
495
  setCaseSensitive(!caseSensitive); }, style: { paddingBlock: 'calc(var(--spacing) * 1)' }, className: `inline-flex cursor-pointer items-center gap-2 rounded-md border px-3.5 text-sm font-medium transition-colors select-none ${caseSensitive
483
496
  ? 'border-teal-300 bg-teal-50 text-teal-700'
484
- : 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'}`, children: [_jsx(Switch, { checked: caseSensitive, onCheckedChange: setCaseSensitive, className: caseSensitive ? 'data-[state=checked]:bg-teal-500 pointer-events-none' : 'pointer-events-none', tabIndex: -1 }), "Ph\u00E2n bi\u1EC7t Hoa / th\u01B0\u1EDDng"] })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "wfsf-title", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-teal-500" }), "Ti\u00EAu \u0111\u1EC1", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(Input, { id: "wfsf-title", value: effectiveTitle, onChange: (e) => setEffectiveTitle(e.target.value), placeholder: "Nh\u1EADp ti\u00EAu \u0111\u1EC1 c\u00E2u h\u1ECFi...", className: "border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100" })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", 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-teal-500" }), "File \u00E2m thanh (Audio)", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setAudioUrls((prev) => [...prev, '']), className: "gap-1.5 border-teal-200 text-teal-600 hover:bg-teal-50 hover:text-teal-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm audio"] })] }), audioUrls.length === 0 ? (_jsx("div", { className: "flex items-center justify-between rounded-lg border border-dashed border-gray-200 bg-gray-50/50 p-3 text-xs text-gray-500", children: _jsx("span", { children: "Ch\u01B0a c\u00F3 file audio. Nh\u1EA5n \"Th\u00EAm audio\" \u0111\u1EC3 upload file nghe cho c\u00E2u h\u1ECFi." }) })) : (_jsx("div", { className: "space-y-2.5", children: audioUrls.map((audioUrl, index) => (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50/30 p-2.5", children: [_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-teal-100 text-xs font-semibold text-teal-700", children: index + 1 }), _jsx("div", { className: "flex-1", children: _jsx(FileUpload, { id: `wfsf-audio-${index}`, label: "", accept: "audio/*", value: audioUrl, onChange: (url) => {
497
+ : 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'}`, children: [_jsx(Switch, { checked: caseSensitive, onCheckedChange: setCaseSensitive, className: caseSensitive ? 'data-[state=checked]:bg-teal-500 pointer-events-none' : 'pointer-events-none', tabIndex: -1 }), "Ph\u00E2n bi\u1EC7t Hoa / th\u01B0\u1EDDng"] }), _jsx(BlankStyleSwitch, { value: blankStyle, onChange: setBlankStyle })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "wfsf-title", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(MessageSquare, { className: "h-4 w-4 text-teal-500" }), "Ti\u00EAu \u0111\u1EC1", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(Input, { id: "wfsf-title", value: effectiveTitle, onChange: (e) => setEffectiveTitle(e.target.value), placeholder: "Nh\u1EADp ti\u00EAu \u0111\u1EC1 c\u00E2u h\u1ECFi...", className: "border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-100" })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", 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-teal-500" }), "File \u00E2m thanh (Audio)", _jsx("span", { className: "text-sm font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setAudioUrls((prev) => [...prev, '']), className: "gap-1.5 border-teal-200 text-teal-600 hover:bg-teal-50 hover:text-teal-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm audio"] })] }), audioUrls.length === 0 ? (_jsx("div", { className: "flex items-center justify-between rounded-lg border border-dashed border-gray-200 bg-gray-50/50 p-3 text-xs text-gray-500", children: _jsx("span", { children: "Ch\u01B0a c\u00F3 file audio. Nh\u1EA5n \"Th\u00EAm audio\" \u0111\u1EC3 upload file nghe cho c\u00E2u h\u1ECFi." }) })) : (_jsx("div", { className: "space-y-2.5", children: audioUrls.map((audioUrl, index) => (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50/30 p-2.5", children: [_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-teal-100 text-xs font-semibold text-teal-700", children: index + 1 }), _jsx("div", { className: "flex-1", children: _jsx(FileUpload, { id: `wfsf-audio-${index}`, label: "", accept: "audio/*", value: audioUrl, onChange: (url) => {
485
498
  setAudioUrls((prev) => {
486
499
  const next = [...prev];
487
500
  next[index] = url;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyweb_dev/oe-exam-sdk",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Reusable OceanEdu question components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",