@tinyweb_dev/oe-exam-sdk 1.0.4 → 1.0.6
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/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
|
+
}
|