@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.
- package/dist/components/exams/take/ExamTakingPageContainer.js +18 -5
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.js +6 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupTextList.d.ts +9 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupTextList.js +55 -0
- package/dist/components/exams/take/components/speaking/renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/speaking/renderers/index.js +1 -0
- package/dist/components/exams/take/utils/cambridge-yle-inline-speaking.d.ts +1 -0
- package/dist/components/exams/take/utils/cambridge-yle-inline-speaking.js +5 -1
- package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +4 -0
- package/dist/components/questions/types/spontaneous-qa-group/flatten.js +19 -0
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +16 -3
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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,
|
package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js
CHANGED
|
@@ -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;
|