@sage-rsc/talking-head-react 1.0.51 → 1.0.53

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-rsc/talking-head-react",
3
- "version": "1.0.51",
3
+ "version": "1.0.53",
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",
@@ -299,9 +299,11 @@ const CurriculumLearning = forwardRef(({
299
299
  });
300
300
  }
301
301
 
302
- // Ensure avatar is ready before speaking
303
- if (avatarRef.current && avatarRef.current.isReady && firstQuestion) {
304
- avatarRef.current.setMood("curious");
302
+ // Function to speak the first question
303
+ const speakFirstQuestion = () => {
304
+ if (!avatarRef.current || !firstQuestion) return;
305
+
306
+ avatarRef.current.setMood("happy");
305
307
 
306
308
  // Play custom animation if available
307
309
  if (animations.questionStart) {
@@ -324,16 +326,30 @@ const CurriculumLearning = forwardRef(({
324
326
  } else {
325
327
  avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
326
328
  }
329
+ };
330
+
331
+ // Try to speak immediately if avatar is ready
332
+ if (avatarRef.current && avatarRef.current.isReady && firstQuestion) {
333
+ speakFirstQuestion();
327
334
  } else if (avatarRef.current && avatarRef.current.isReady) {
335
+ // No question but avatar ready - just announce
328
336
  const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
329
337
  avatarRef.current.speakText("Now let me ask you some questions to test your understanding.", { lipsyncLang: config.lipsyncLang });
330
338
  } else {
331
- // Avatar not ready yet, retry after a short delay
332
- setTimeout(() => {
333
- if (startQuestionsRef.current) {
334
- startQuestionsRef.current();
339
+ // Avatar not ready yet - wait for it to be ready, then speak
340
+ const checkReady = setInterval(() => {
341
+ if (avatarRef.current && avatarRef.current.isReady) {
342
+ clearInterval(checkReady);
343
+ if (firstQuestion) {
344
+ speakFirstQuestion();
345
+ }
335
346
  }
336
347
  }, 100);
348
+
349
+ // Timeout after 5 seconds
350
+ setTimeout(() => {
351
+ clearInterval(checkReady);
352
+ }, 5000);
337
353
  }
338
354
  }, [animations.questionStart, getCurrentLesson, getCurrentQuestion]);
339
355
 
@@ -357,15 +373,18 @@ const CurriculumLearning = forwardRef(({
357
373
  });
358
374
  }
359
375
 
360
- if (avatarRef.current && nextQuestionObj) {
376
+ // Function to speak the next question
377
+ const speakNextQuestion = () => {
378
+ if (!avatarRef.current || !nextQuestionObj) return;
379
+
361
380
  avatarRef.current.setMood("happy");
362
381
  avatarRef.current.setBodyMovement("idle");
363
-
382
+
364
383
  // Play custom animation if available
365
384
  if (animations.nextQuestion) {
366
385
  try {
367
386
  avatarRef.current.playAnimation(animations.nextQuestion, true);
368
- } catch (error) {
387
+ } catch (error) {
369
388
  console.warn('Failed to play nextQuestion animation:', error);
370
389
  }
371
390
  }
@@ -385,11 +404,29 @@ const CurriculumLearning = forwardRef(({
385
404
  avatarRef.current.speakText(`Now let's try this one: ${nextQuestionObj.question}`, {
386
405
  lipsyncLang: config.lipsyncLang
387
406
  });
388
- } else {
407
+ } else {
389
408
  avatarRef.current.speakText(`Here's the next question: ${nextQuestionObj.question}`, {
390
409
  lipsyncLang: config.lipsyncLang
391
410
  });
392
411
  }
412
+ };
413
+
414
+ // Try to speak immediately if avatar is ready
415
+ if (avatarRef.current && avatarRef.current.isReady && nextQuestionObj) {
416
+ speakNextQuestion();
417
+ } else if (nextQuestionObj) {
418
+ // Avatar not ready yet - wait for it to be ready, then speak
419
+ const checkReady = setInterval(() => {
420
+ if (avatarRef.current && avatarRef.current.isReady) {
421
+ clearInterval(checkReady);
422
+ speakNextQuestion();
423
+ }
424
+ }, 100);
425
+
426
+ // Timeout after 5 seconds
427
+ setTimeout(() => {
428
+ clearInterval(checkReady);
429
+ }, 5000);
393
430
  }
394
431
  } else {
395
432
  // No more questions - notify parent that all questions are done
@@ -438,7 +475,7 @@ const CurriculumLearning = forwardRef(({
438
475
 
439
476
  if (avatarRef.current) {
440
477
  avatarRef.current.setMood("happy");
441
- avatarRef.current.setBodyMovement("idle");
478
+ avatarRef.current.setBodyMovement("idle");
442
479
  }
443
480
  } else {
444
481
  // No more lessons in current module - check if there's a next module
@@ -610,7 +647,7 @@ const CurriculumLearning = forwardRef(({
610
647
  if (animations.incorrect) {
611
648
  try {
612
649
  avatarRef.current.playAnimation(animations.incorrect, true);
613
- } catch (error) {
650
+ } catch (error) {
614
651
  avatarRef.current.setBodyMovement("idle");
615
652
  }
616
653
  }
@@ -695,6 +732,136 @@ const CurriculumLearning = forwardRef(({
695
732
  }
696
733
  }, [getCurrentQuestion, checkAnswer]);
697
734
 
735
+ // Move to previous question
736
+ const previousQuestion = useCallback(() => {
737
+ if (stateRef.current.currentQuestionIndex > 0) {
738
+ stateRef.current.currentQuestionIndex -= 1;
739
+
740
+ // Trigger custom action for UI update
741
+ const prevQuestionObj = getCurrentQuestion();
742
+ if (prevQuestionObj) {
743
+ callbacksRef.current.onCustomAction({
744
+ type: 'questionStart',
745
+ moduleIndex: stateRef.current.currentModuleIndex,
746
+ lessonIndex: stateRef.current.currentLessonIndex,
747
+ questionIndex: stateRef.current.currentQuestionIndex,
748
+ totalQuestions: stateRef.current.totalQuestions,
749
+ question: prevQuestionObj
750
+ });
751
+ }
752
+
753
+ // Function to speak the previous question
754
+ const speakPrevQuestion = () => {
755
+ if (!avatarRef.current || !prevQuestionObj) return;
756
+
757
+ avatarRef.current.setMood("happy");
758
+ avatarRef.current.setBodyMovement("idle");
759
+
760
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
761
+
762
+ // Speak the question text
763
+ if (prevQuestionObj.type === 'code_test') {
764
+ avatarRef.current.speakText(`Let's go back to this coding challenge: ${prevQuestionObj.question}`, {
765
+ lipsyncLang: config.lipsyncLang
766
+ });
767
+ } else {
768
+ avatarRef.current.speakText(`Going back to: ${prevQuestionObj.question}`, {
769
+ lipsyncLang: config.lipsyncLang
770
+ });
771
+ }
772
+ };
773
+
774
+ // Try to speak immediately if avatar is ready
775
+ if (avatarRef.current && avatarRef.current.isReady && prevQuestionObj) {
776
+ speakPrevQuestion();
777
+ } else if (prevQuestionObj) {
778
+ // Avatar not ready yet - wait for it to be ready, then speak
779
+ const checkReady = setInterval(() => {
780
+ if (avatarRef.current && avatarRef.current.isReady) {
781
+ clearInterval(checkReady);
782
+ speakPrevQuestion();
783
+ }
784
+ }, 100);
785
+
786
+ setTimeout(() => {
787
+ clearInterval(checkReady);
788
+ }, 5000);
789
+ }
790
+ }
791
+ }, [getCurrentQuestion]);
792
+
793
+ // Move to previous lesson
794
+ const previousLesson = useCallback(() => {
795
+ const curriculum = curriculumRef.current || { modules: [] };
796
+ const currentModule = curriculum.modules[stateRef.current.currentModuleIndex];
797
+
798
+ // Check if there's a previous lesson in the current module
799
+ const hasPrevLessonInModule = stateRef.current.currentLessonIndex > 0;
800
+
801
+ if (hasPrevLessonInModule) {
802
+ // Move to previous lesson in current module
803
+ stateRef.current.currentLessonIndex -= 1;
804
+ stateRef.current.currentQuestionIndex = 0;
805
+ stateRef.current.lessonCompleted = false;
806
+ stateRef.current.isQuestionMode = false;
807
+ stateRef.current.isTeaching = false;
808
+ stateRef.current.score = 0;
809
+ stateRef.current.totalQuestions = 0;
810
+
811
+ // Notify parent that lesson has changed
812
+ callbacksRef.current.onCustomAction({
813
+ type: 'lessonStart',
814
+ moduleIndex: stateRef.current.currentModuleIndex,
815
+ lessonIndex: stateRef.current.currentLessonIndex
816
+ });
817
+
818
+ callbacksRef.current.onLessonStart({
819
+ moduleIndex: stateRef.current.currentModuleIndex,
820
+ lessonIndex: stateRef.current.currentLessonIndex,
821
+ lesson: getCurrentLesson()
822
+ });
823
+
824
+ if (avatarRef.current) {
825
+ avatarRef.current.setMood("happy");
826
+ avatarRef.current.setBodyMovement("idle");
827
+ }
828
+ } else {
829
+ // No previous lesson in current module - check if there's a previous module
830
+ const hasPrevModule = stateRef.current.currentModuleIndex > 0;
831
+
832
+ if (hasPrevModule) {
833
+ // Move to last lesson of previous module
834
+ const prevModule = curriculum.modules[stateRef.current.currentModuleIndex - 1];
835
+ stateRef.current.currentModuleIndex -= 1;
836
+ stateRef.current.currentLessonIndex = (prevModule?.lessons?.length || 1) - 1;
837
+ stateRef.current.currentQuestionIndex = 0;
838
+ stateRef.current.lessonCompleted = false;
839
+ stateRef.current.isQuestionMode = false;
840
+ stateRef.current.isTeaching = false;
841
+ stateRef.current.score = 0;
842
+ stateRef.current.totalQuestions = 0;
843
+
844
+ // Notify parent that lesson has changed
845
+ callbacksRef.current.onCustomAction({
846
+ type: 'lessonStart',
847
+ moduleIndex: stateRef.current.currentModuleIndex,
848
+ lessonIndex: stateRef.current.currentLessonIndex
849
+ });
850
+
851
+ callbacksRef.current.onLessonStart({
852
+ moduleIndex: stateRef.current.currentModuleIndex,
853
+ lessonIndex: stateRef.current.currentLessonIndex,
854
+ lesson: getCurrentLesson()
855
+ });
856
+
857
+ if (avatarRef.current) {
858
+ avatarRef.current.setMood("happy");
859
+ avatarRef.current.setBodyMovement("idle");
860
+ }
861
+ }
862
+ }
863
+ }, [getCurrentLesson]);
864
+
698
865
  // Reset curriculum
699
866
  const resetCurriculum = useCallback(() => {
700
867
  stateRef.current.currentModuleIndex = 0;
@@ -744,7 +911,9 @@ const CurriculumLearning = forwardRef(({
744
911
  handleAnswerSelect,
745
912
  handleCodeTestResult,
746
913
  nextQuestion,
914
+ previousQuestion,
747
915
  nextLesson,
916
+ previousLesson,
748
917
  completeLesson,
749
918
  completeCurriculum,
750
919
  resetCurriculum,