@sage-rsc/talking-head-react 1.0.30 → 1.0.32

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/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("./index-BFQNNYmP.cjs");exports.CurriculumLearning=i.CurriculumLearning;exports.TalkingHeadAvatar=i.TalkingHeadAvatar;exports.TalkingHeadComponent=i.TalkingHeadComponent;exports.animations=i.animations;exports.getActiveTTSConfig=i.getActiveTTSConfig;exports.getAnimation=i.getAnimation;exports.getVoiceOptions=i.getVoiceOptions;exports.hasAnimation=i.hasAnimation;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("./index-CdCA-KAp.cjs");exports.CurriculumLearning=i.CurriculumLearning;exports.TalkingHeadAvatar=i.TalkingHeadAvatar;exports.TalkingHeadComponent=i.TalkingHeadComponent;exports.animations=i.animations;exports.getActiveTTSConfig=i.getActiveTTSConfig;exports.getAnimation=i.getAnimation;exports.getVoiceOptions=i.getVoiceOptions;exports.hasAnimation=i.hasAnimation;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as n, T as s, a as t, d as e, b as o, g, c as m, h as r } from "./index-V_d1NQ-5.js";
1
+ import { C as n, T as s, a as t, d as e, b as o, g, c as m, h as r } from "./index-Dd7BPF7g.js";
2
2
  export {
3
3
  n as CurriculumLearning,
4
4
  s as TalkingHeadAvatar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-rsc/talking-head-react",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "A reusable React component for 3D talking avatars with lip-sync and text-to-speech",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -58,45 +58,74 @@ const CurriculumLearning = forwardRef(({
58
58
  const nextQuestionRef = useRef(null);
59
59
  const completeCurriculumRef = useRef(null);
60
60
  const startQuestionsRef = useRef(null);
61
-
62
- // Update callbacks ref when they change
63
- useEffect(() => {
64
- callbacksRef.current = {
65
- onLessonStart,
66
- onLessonComplete,
67
- onQuestionAnswer,
68
- onCurriculumComplete,
69
- onCustomAction
70
- };
71
- }, [onLessonStart, onLessonComplete, onQuestionAnswer, onCurriculumComplete, onCustomAction]);
72
-
73
- const curriculum = curriculumData?.curriculum || {
61
+ const handleAnswerSelectRef = useRef(null);
62
+
63
+ // Store curriculum in ref to avoid dependency issues - initialize early
64
+ const curriculumRef = useRef(curriculumData?.curriculum || {
74
65
  title: "Default Curriculum",
75
66
  description: "No curriculum data provided",
76
67
  language: "en",
77
68
  modules: []
78
- };
79
-
80
- const defaultAvatarConfig = {
69
+ });
70
+
71
+ // Store avatar config in ref to avoid dependency issues - initialize early
72
+ const defaultAvatarConfigRef = useRef({
81
73
  avatarUrl: avatarConfig.avatarUrl || "/avatars/brunette.glb",
82
74
  avatarBody: avatarConfig.avatarBody || "F",
83
75
  mood: avatarConfig.mood || "happy",
84
76
  ttsLang: avatarConfig.ttsLang || "en",
85
- ttsService: avatarConfig.ttsService || null, // Don't default to "edge" - let config decide
86
- ttsVoice: avatarConfig.ttsVoice || null, // Don't default - let config decide
77
+ ttsService: avatarConfig.ttsService || null,
78
+ ttsVoice: avatarConfig.ttsVoice || null,
87
79
  ttsApiKey: avatarConfig.ttsApiKey || null,
88
80
  bodyMovement: avatarConfig.bodyMovement || "gesturing",
89
81
  movementIntensity: avatarConfig.movementIntensity || 0.7,
90
82
  showFullAvatar: avatarConfig.showFullAvatar !== undefined ? avatarConfig.showFullAvatar : true,
91
83
  animations: animations,
92
- lipsyncLang: 'en' // Default lipsync language
93
- };
84
+ lipsyncLang: 'en'
85
+ });
86
+
87
+ // Update callbacks ref when they change
88
+ useEffect(() => {
89
+ callbacksRef.current = {
90
+ onLessonStart,
91
+ onLessonComplete,
92
+ onQuestionAnswer,
93
+ onCurriculumComplete,
94
+ onCustomAction
95
+ };
96
+ }, [onLessonStart, onLessonComplete, onQuestionAnswer, onCurriculumComplete, onCustomAction]);
97
+
98
+ // Update curriculum and config refs when they change
99
+ useEffect(() => {
100
+ curriculumRef.current = curriculumData?.curriculum || {
101
+ title: "Default Curriculum",
102
+ description: "No curriculum data provided",
103
+ language: "en",
104
+ modules: []
105
+ };
106
+
107
+ defaultAvatarConfigRef.current = {
108
+ avatarUrl: avatarConfig.avatarUrl || "/avatars/brunette.glb",
109
+ avatarBody: avatarConfig.avatarBody || "F",
110
+ mood: avatarConfig.mood || "happy",
111
+ ttsLang: avatarConfig.ttsLang || "en",
112
+ ttsService: avatarConfig.ttsService || null,
113
+ ttsVoice: avatarConfig.ttsVoice || null,
114
+ ttsApiKey: avatarConfig.ttsApiKey || null,
115
+ bodyMovement: avatarConfig.bodyMovement || "gesturing",
116
+ movementIntensity: avatarConfig.movementIntensity || 0.7,
117
+ showFullAvatar: avatarConfig.showFullAvatar !== undefined ? avatarConfig.showFullAvatar : true,
118
+ animations: animations,
119
+ lipsyncLang: 'en'
120
+ };
121
+ }, [curriculumData, avatarConfig, animations]);
94
122
 
95
123
  // Helper to get current lesson/question
96
124
  const getCurrentLesson = useCallback(() => {
125
+ const curriculum = curriculumRef.current || { modules: [] };
97
126
  const module = curriculum.modules[stateRef.current.currentModuleIndex];
98
127
  return module?.lessons[stateRef.current.currentLessonIndex];
99
- }, [curriculum]);
128
+ }, []);
100
129
 
101
130
  const getCurrentQuestion = useCallback(() => {
102
131
  const lesson = getCurrentLesson();
@@ -130,7 +159,7 @@ const CurriculumLearning = forwardRef(({
130
159
  let feedbackMessage = `Congratulations! You've completed this lesson`;
131
160
  if (stateRef.current.totalQuestions > 0) {
132
161
  feedbackMessage += ` with a score of ${stateRef.current.score} out of ${stateRef.current.totalQuestions} (${percentage}%). `;
133
- } else {
162
+ } else {
134
163
  feedbackMessage += `! `;
135
164
  }
136
165
 
@@ -161,26 +190,31 @@ const CurriculumLearning = forwardRef(({
161
190
  });
162
191
 
163
192
  if (avatarRef.current) {
164
- avatarRef.current.setMood("happy");
193
+ avatarRef.current.setMood("happy");
165
194
  if (animations.lessonComplete) {
166
195
  try {
167
196
  avatarRef.current.playAnimation(animations.lessonComplete, true);
168
- } catch (error) {
169
- avatarRef.current.playCelebration();
197
+ } catch (error) {
198
+ avatarRef.current.playCelebration();
170
199
  }
171
200
  }
172
201
 
173
- // Check if there's a next lesson available
202
+ // Check if there's a next lesson available (either in current module or next module)
203
+ const curriculum = curriculumRef.current || { modules: [] };
174
204
  const currentModule = curriculum.modules[stateRef.current.currentModuleIndex];
175
- const hasNextLesson = stateRef.current.currentLessonIndex < (currentModule?.lessons?.length || 0) - 1;
205
+ const hasNextLessonInModule = stateRef.current.currentLessonIndex < (currentModule?.lessons?.length || 0) - 1;
206
+ const hasNextModule = stateRef.current.currentModuleIndex < (curriculum.modules?.length || 0) - 1;
207
+ const hasNextLesson = hasNextLessonInModule || hasNextModule;
208
+
209
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
176
210
 
177
211
  if (hasNextLesson) {
178
212
  // Wait for speech to finish, then automatically move to next lesson
179
213
  avatarRef.current.speakText(feedbackMessage, {
180
- lipsyncLang: defaultAvatarConfig.lipsyncLang,
214
+ lipsyncLang: config.lipsyncLang,
181
215
  onSpeechEnd: () => {
182
216
  // Add a small delay after speech ends for natural flow
183
- setTimeout(() => {
217
+ setTimeout(() => {
184
218
  // Use ref to avoid circular dependency
185
219
  if (nextLessonRef.current) {
186
220
  nextLessonRef.current();
@@ -188,10 +222,10 @@ const CurriculumLearning = forwardRef(({
188
222
  }, 1000);
189
223
  }
190
224
  });
191
- } else {
192
- // This is the last lesson, complete curriculum instead
225
+ } else {
226
+ // This is the last lesson in the last module, complete curriculum instead
193
227
  avatarRef.current.speakText(feedbackMessage, {
194
- lipsyncLang: defaultAvatarConfig.lipsyncLang,
228
+ lipsyncLang: config.lipsyncLang,
195
229
  onSpeechEnd: () => {
196
230
  // Add a small delay after speech ends for natural flow
197
231
  setTimeout(() => {
@@ -203,18 +237,19 @@ const CurriculumLearning = forwardRef(({
203
237
  });
204
238
  }
205
239
  }
206
- }, [animations.lessonComplete, curriculum, defaultAvatarConfig]);
240
+ }, [animations.lessonComplete]);
207
241
 
208
242
  // Complete entire curriculum
209
243
  const completeCurriculum = useCallback(() => {
210
244
  stateRef.current.curriculumCompleted = true;
211
245
 
246
+ const curriculum = curriculumRef.current || { modules: [] };
212
247
  callbacksRef.current.onCurriculumComplete({
213
248
  modules: curriculum.modules.length,
214
249
  totalLessons: curriculum.modules.reduce((sum, mod) => sum + mod.lessons.length, 0)
215
250
  });
216
-
217
- if (avatarRef.current) {
251
+
252
+ if (avatarRef.current) {
218
253
  avatarRef.current.setMood("celebrating");
219
254
  if (animations.curriculumComplete) {
220
255
  try {
@@ -223,9 +258,10 @@ const CurriculumLearning = forwardRef(({
223
258
  avatarRef.current.playCelebration();
224
259
  }
225
260
  }
226
- avatarRef.current.speakText("Amazing! You've completed the entire curriculum! You're now ready to move on to more advanced topics. Well done!", { lipsyncLang: defaultAvatarConfig.lipsyncLang });
261
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
262
+ avatarRef.current.speakText("Amazing! You've completed the entire curriculum! You're now ready to move on to more advanced topics. Well done!", { lipsyncLang: config.lipsyncLang });
227
263
  }
228
- }, [animations.curriculumComplete, curriculum, defaultAvatarConfig]);
264
+ }, [animations.curriculumComplete]);
229
265
 
230
266
  // Start asking questions
231
267
  const startQuestions = useCallback(() => {
@@ -260,20 +296,23 @@ const CurriculumLearning = forwardRef(({
260
296
  }
261
297
  }
262
298
 
299
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
300
+
263
301
  // Introduce the first question properly based on type
264
302
  if (firstQuestion.type === 'code_test') {
265
- avatarRef.current.speakText(`Let's test your coding skills! Here's your first challenge: ${firstQuestion.question}`, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
303
+ avatarRef.current.speakText(`Let's test your coding skills! Here's your first challenge: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
266
304
  } else if (firstQuestion.type === 'multiple_choice') {
267
- avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
305
+ avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
268
306
  } else if (firstQuestion.type === 'true_false') {
269
- avatarRef.current.speakText(`Let's start with some true or false questions. First question: ${firstQuestion.question}`, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
270
- } else {
271
- avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
307
+ avatarRef.current.speakText(`Let's start with some true or false questions. First question: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
308
+ } else {
309
+ avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
272
310
  }
273
311
  } else if (avatarRef.current) {
274
- avatarRef.current.speakText("Now let me ask you some questions to test your understanding.", { lipsyncLang: defaultAvatarConfig.lipsyncLang });
312
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
313
+ avatarRef.current.speakText("Now let me ask you some questions to test your understanding.", { lipsyncLang: config.lipsyncLang });
275
314
  }
276
- }, [animations.questionStart, getCurrentLesson, getCurrentQuestion, defaultAvatarConfig]);
315
+ }, [animations.questionStart, getCurrentLesson, getCurrentQuestion]);
277
316
 
278
317
  // Move to next question
279
318
  const nextQuestion = useCallback(() => {
@@ -296,34 +335,36 @@ const CurriculumLearning = forwardRef(({
296
335
  }
297
336
 
298
337
  if (avatarRef.current && nextQuestionObj) {
299
- avatarRef.current.setMood("happy");
338
+ avatarRef.current.setMood("happy");
300
339
  avatarRef.current.setBodyMovement("idle");
301
340
 
302
341
  // Play custom animation if available
303
342
  if (animations.nextQuestion) {
304
- try {
343
+ try {
305
344
  avatarRef.current.playAnimation(animations.nextQuestion, true);
306
- } catch (error) {
345
+ } catch (error) {
307
346
  console.warn('Failed to play nextQuestion animation:', error);
308
347
  }
309
348
  }
310
349
 
350
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
351
+
311
352
  // Speak the question text with proper introduction
312
353
  if (nextQuestionObj.type === 'code_test') {
313
354
  avatarRef.current.speakText(`Great! Now let's move on to your next coding challenge: ${nextQuestionObj.question}`, {
314
- lipsyncLang: defaultAvatarConfig.lipsyncLang
355
+ lipsyncLang: config.lipsyncLang
315
356
  });
316
357
  } else if (nextQuestionObj.type === 'multiple_choice') {
317
358
  avatarRef.current.speakText(`Alright! Here's your next question: ${nextQuestionObj.question}`, {
318
- lipsyncLang: defaultAvatarConfig.lipsyncLang
359
+ lipsyncLang: config.lipsyncLang
319
360
  });
320
361
  } else if (nextQuestionObj.type === 'true_false') {
321
362
  avatarRef.current.speakText(`Now let's try this one: ${nextQuestionObj.question}`, {
322
- lipsyncLang: defaultAvatarConfig.lipsyncLang
363
+ lipsyncLang: config.lipsyncLang
323
364
  });
324
365
  } else {
325
366
  avatarRef.current.speakText(`Here's the next question: ${nextQuestionObj.question}`, {
326
- lipsyncLang: defaultAvatarConfig.lipsyncLang
367
+ lipsyncLang: config.lipsyncLang
327
368
  });
328
369
  }
329
370
  }
@@ -333,12 +374,18 @@ const CurriculumLearning = forwardRef(({
333
374
  completeLessonRef.current();
334
375
  }
335
376
  }
336
- }, [animations.nextQuestion, getCurrentLesson, getCurrentQuestion, defaultAvatarConfig]);
377
+ }, [animations.nextQuestion, getCurrentLesson, getCurrentQuestion]);
337
378
 
338
379
  // Move to next lesson
339
380
  const nextLesson = useCallback(() => {
381
+ const curriculum = curriculumRef.current || { modules: [] };
340
382
  const currentModule = curriculum.modules[stateRef.current.currentModuleIndex];
341
- if (stateRef.current.currentLessonIndex < (currentModule?.lessons?.length || 0) - 1) {
383
+
384
+ // Check if there's a next lesson in the current module
385
+ const hasNextLessonInModule = stateRef.current.currentLessonIndex < (currentModule?.lessons?.length || 0) - 1;
386
+
387
+ if (hasNextLessonInModule) {
388
+ // Move to next lesson in current module
342
389
  stateRef.current.currentLessonIndex += 1;
343
390
  stateRef.current.currentQuestionIndex = 0;
344
391
  stateRef.current.lessonCompleted = false;
@@ -366,11 +413,46 @@ const CurriculumLearning = forwardRef(({
366
413
  }, 500);
367
414
  }
368
415
  } else {
369
- if (completeCurriculumRef.current) {
370
- completeCurriculumRef.current();
416
+ // No more lessons in current module - check if there's a next module
417
+ const hasNextModule = stateRef.current.currentModuleIndex < (curriculum.modules?.length || 0) - 1;
418
+
419
+ if (hasNextModule) {
420
+ // Move to first lesson of next module
421
+ stateRef.current.currentModuleIndex += 1;
422
+ stateRef.current.currentLessonIndex = 0;
423
+ stateRef.current.currentQuestionIndex = 0;
424
+ stateRef.current.lessonCompleted = false;
425
+ stateRef.current.isQuestionMode = false;
426
+ stateRef.current.isTeaching = false;
427
+ stateRef.current.score = 0;
428
+ stateRef.current.totalQuestions = 0;
429
+
430
+ // Clear current question in UI
431
+ callbacksRef.current.onCustomAction({
432
+ type: 'lessonStart',
433
+ moduleIndex: stateRef.current.currentModuleIndex,
434
+ lessonIndex: stateRef.current.currentLessonIndex
435
+ });
436
+
437
+ if (avatarRef.current) {
438
+ avatarRef.current.setMood("happy");
439
+ avatarRef.current.setBodyMovement("idle");
440
+
441
+ // Automatically start teaching the next lesson after a brief pause
442
+ setTimeout(() => {
443
+ if (startTeachingRef.current) {
444
+ startTeachingRef.current();
445
+ }
446
+ }, 500);
447
+ }
448
+ } else {
449
+ // No more modules or lessons - complete curriculum
450
+ if (completeCurriculumRef.current) {
451
+ completeCurriculumRef.current();
452
+ }
371
453
  }
372
454
  }
373
- }, [curriculum]);
455
+ }, []);
374
456
 
375
457
  // Start teaching the lesson
376
458
  const startTeaching = useCallback(() => {
@@ -396,7 +478,7 @@ const CurriculumLearning = forwardRef(({
396
478
  avatarRef.current.setBodyMovement("gesturing");
397
479
  }
398
480
 
399
- avatarRef.current.speakText(currentLesson.avatar_script, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
481
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
400
482
 
401
483
  callbacksRef.current.onLessonStart({
402
484
  moduleIndex: stateRef.current.currentModuleIndex,
@@ -412,23 +494,29 @@ const CurriculumLearning = forwardRef(({
412
494
  lesson: currentLesson
413
495
  });
414
496
 
415
- // After speaking, move to question mode
416
- setTimeout(() => {
417
- stateRef.current.isTeaching = false;
418
- if (currentLesson.questions && currentLesson.questions.length > 0) {
419
- // Use ref to avoid circular dependency
420
- if (startQuestionsRef.current) {
421
- startQuestionsRef.current();
422
- }
497
+ // Wait for avatar to finish speaking before moving to questions
498
+ avatarRef.current.speakText(currentLesson.avatar_script, {
499
+ lipsyncLang: config.lipsyncLang,
500
+ onSpeechEnd: () => {
501
+ stateRef.current.isTeaching = false;
502
+ // Add a small delay after speech ends for natural flow
503
+ setTimeout(() => {
504
+ if (currentLesson.questions && currentLesson.questions.length > 0) {
505
+ // Use ref to avoid circular dependency
506
+ if (startQuestionsRef.current) {
507
+ startQuestionsRef.current();
508
+ }
423
509
  } else {
424
- // No questions, complete the lesson using ref to avoid circular dependency
425
- if (completeLessonRef.current) {
426
- completeLessonRef.current();
427
- }
510
+ // No questions, complete the lesson using ref to avoid circular dependency
511
+ if (completeLessonRef.current) {
512
+ completeLessonRef.current();
513
+ }
514
+ }
515
+ }, 500);
428
516
  }
429
- }, 8000);
517
+ });
430
518
  }
431
- }, [animations.teaching, getCurrentLesson, defaultAvatarConfig]);
519
+ }, [animations.teaching, getCurrentLesson]);
432
520
 
433
521
  // Handle answer selection
434
522
  const handleAnswerSelect = useCallback((answer) => {
@@ -450,9 +538,9 @@ const CurriculumLearning = forwardRef(({
450
538
 
451
539
  if (avatarRef.current) {
452
540
  if (isCorrect) {
453
- avatarRef.current.setMood("happy");
541
+ avatarRef.current.setMood("happy");
454
542
  if (animations.correct) {
455
- try {
543
+ try {
456
544
  avatarRef.current.playReaction("happy");
457
545
  } catch (error) {
458
546
  avatarRef.current.setBodyMovement("happy");
@@ -463,9 +551,11 @@ const CurriculumLearning = forwardRef(({
463
551
  ? `Great job! Your code passed all the tests! ${currentQuestion.explanation || ''}`
464
552
  : `Excellent! That's correct! ${currentQuestion.explanation || ''}`;
465
553
 
554
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
555
+
466
556
  // Wait for speech to finish before moving to next question
467
557
  avatarRef.current.speakText(successMessage, {
468
- lipsyncLang: defaultAvatarConfig.lipsyncLang,
558
+ lipsyncLang: config.lipsyncLang,
469
559
  onSpeechEnd: () => {
470
560
  // Add a small delay after speech ends for natural flow
471
561
  setTimeout(() => {
@@ -475,12 +565,12 @@ const CurriculumLearning = forwardRef(({
475
565
  }, 500);
476
566
  }
477
567
  });
478
- } else {
568
+ } else {
479
569
  avatarRef.current.setMood("sad");
480
570
  if (animations.incorrect) {
481
571
  try {
482
572
  avatarRef.current.playAnimation(animations.incorrect, true);
483
- } catch (error) {
573
+ } catch (error) {
484
574
  avatarRef.current.setBodyMovement("idle");
485
575
  }
486
576
  }
@@ -489,9 +579,11 @@ const CurriculumLearning = forwardRef(({
489
579
  ? `Your code didn't pass all the tests. ${currentQuestion.explanation || 'Try again!'}`
490
580
  : `Not quite right, but don't worry! ${currentQuestion.explanation || ''} Let's move on to the next question.`;
491
581
 
582
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
583
+
492
584
  // Wait for speech to finish before moving to next question
493
585
  avatarRef.current.speakText(failureMessage, {
494
- lipsyncLang: defaultAvatarConfig.lipsyncLang,
586
+ lipsyncLang: config.lipsyncLang,
495
587
  onSpeechEnd: () => {
496
588
  // Add a small delay after speech ends for natural flow
497
589
  setTimeout(() => {
@@ -508,7 +600,7 @@ const CurriculumLearning = forwardRef(({
508
600
  nextQuestionRef.current();
509
601
  }
510
602
  }
511
- }, [animations.correct, animations.incorrect, getCurrentQuestion, checkAnswer, defaultAvatarConfig]);
603
+ }, [animations.correct, animations.incorrect, getCurrentQuestion, checkAnswer]);
512
604
 
513
605
  // Handle code test result submission
514
606
  const handleCodeTestResult = useCallback((testResult) => {
@@ -552,7 +644,7 @@ const CurriculumLearning = forwardRef(({
552
644
  if (handleAnswerSelectRef.current) {
553
645
  handleAnswerSelectRef.current(codeTestAnswer);
554
646
  }
555
- }, [getCurrentQuestion, checkAnswer, defaultAvatarConfig]);
647
+ }, [getCurrentQuestion, checkAnswer]);
556
648
 
557
649
  // Reset curriculum
558
650
  const resetCurriculum = useCallback(() => {
@@ -615,7 +707,8 @@ const CurriculumLearning = forwardRef(({
615
707
  speakText: async (text, options = {}) => {
616
708
  // Ensure audio context is resumed before speaking
617
709
  await avatarRef.current?.resumeAudioContext?.();
618
- avatarRef.current?.speakText(text, { ...options, lipsyncLang: options.lipsyncLang || defaultAvatarConfig.lipsyncLang });
710
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
711
+ avatarRef.current?.speakText(text, { ...options, lipsyncLang: options.lipsyncLang || config.lipsyncLang });
619
712
  },
620
713
  resumeAudioContext: async () => {
621
714
  // Try to resume through avatar ref first
@@ -667,6 +760,22 @@ const CurriculumLearning = forwardRef(({
667
760
  isAvatarReady: () => avatarRef.current?.isReady || false
668
761
  }), [startTeaching, startQuestions, handleAnswerSelect, handleCodeTestResult, nextQuestion, nextLesson, completeLesson, completeCurriculum, resetCurriculum, getCurrentQuestion, getCurrentLesson]);
669
762
 
763
+ // Get current config for render
764
+ const defaultAvatarConfig = defaultAvatarConfigRef.current || {
765
+ avatarUrl: "/avatars/brunette.glb",
766
+ avatarBody: "F",
767
+ mood: "happy",
768
+ ttsLang: "en",
769
+ ttsService: null,
770
+ ttsVoice: null,
771
+ ttsApiKey: null,
772
+ bodyMovement: "gesturing",
773
+ movementIntensity: 0.7,
774
+ showFullAvatar: true,
775
+ animations: animations,
776
+ lipsyncLang: 'en'
777
+ };
778
+
670
779
  return (
671
780
  <div style={{ width: '100%', height: '100%' }}>
672
781
  <TalkingHeadAvatar
@@ -697,4 +806,4 @@ const CurriculumLearning = forwardRef(({
697
806
 
698
807
  CurriculumLearning.displayName = 'CurriculumLearning';
699
808
 
700
- export default CurriculumLearning;
809
+ export default CurriculumLearning;