@tinyweb_dev/oe-exam-sdk 1.0.4 → 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyweb_dev/oe-exam-sdk",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Reusable OceanEdu question components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",