@sage-rsc/talking-head-react 1.0.65 → 1.0.67
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 +2 -2
- package/dist/index.js +495 -478
- package/package.json +1 -1
- package/src/components/CurriculumLearning.jsx +35 -10
- package/src/components/TalkingHeadAvatar.jsx +58 -18
package/package.json
CHANGED
|
@@ -402,21 +402,38 @@ const CurriculumLearning = forwardRef(({
|
|
|
402
402
|
|
|
403
403
|
const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
|
|
404
404
|
|
|
405
|
-
//
|
|
405
|
+
// Check if this is the last question to adjust the introduction
|
|
406
|
+
const currentLesson = getCurrentLesson();
|
|
407
|
+
const totalQuestionsInLesson = currentLesson?.questions?.length || 0;
|
|
408
|
+
const isLastQuestion = stateRef.current.currentQuestionIndex >= totalQuestionsInLesson - 1;
|
|
409
|
+
|
|
410
|
+
// Speak the question text with proper introduction (no "next" if it's the last question)
|
|
406
411
|
if (nextQuestionObj.type === 'code_test') {
|
|
407
|
-
|
|
412
|
+
const intro = isLastQuestion
|
|
413
|
+
? `Great! Here's your final coding challenge: ${nextQuestionObj.question}`
|
|
414
|
+
: `Great! Now let's move on to your next coding challenge: ${nextQuestionObj.question}`;
|
|
415
|
+
avatarRef.current.speakText(intro, {
|
|
408
416
|
lipsyncLang: config.lipsyncLang
|
|
409
417
|
});
|
|
410
418
|
} else if (nextQuestionObj.type === 'multiple_choice') {
|
|
411
|
-
|
|
419
|
+
const intro = isLastQuestion
|
|
420
|
+
? `Alright! Here's your final question: ${nextQuestionObj.question}`
|
|
421
|
+
: `Alright! Here's your next question: ${nextQuestionObj.question}`;
|
|
422
|
+
avatarRef.current.speakText(intro, {
|
|
412
423
|
lipsyncLang: config.lipsyncLang
|
|
413
424
|
});
|
|
414
425
|
} else if (nextQuestionObj.type === 'true_false') {
|
|
415
|
-
|
|
426
|
+
const intro = isLastQuestion
|
|
427
|
+
? `Now let's try this final one: ${nextQuestionObj.question}`
|
|
428
|
+
: `Now let's try this one: ${nextQuestionObj.question}`;
|
|
429
|
+
avatarRef.current.speakText(intro, {
|
|
416
430
|
lipsyncLang: config.lipsyncLang
|
|
417
431
|
});
|
|
418
432
|
} else {
|
|
419
|
-
|
|
433
|
+
const intro = isLastQuestion
|
|
434
|
+
? `Here's your final question: ${nextQuestionObj.question}`
|
|
435
|
+
: `Here's the next question: ${nextQuestionObj.question}`;
|
|
436
|
+
avatarRef.current.speakText(intro, {
|
|
420
437
|
lipsyncLang: config.lipsyncLang
|
|
421
438
|
});
|
|
422
439
|
}
|
|
@@ -649,6 +666,12 @@ const CurriculumLearning = forwardRef(({
|
|
|
649
666
|
}
|
|
650
667
|
}
|
|
651
668
|
avatarRef.current.setBodyMovement("gesturing");
|
|
669
|
+
|
|
670
|
+
// Check if this is the last question
|
|
671
|
+
const currentLesson = getCurrentLesson();
|
|
672
|
+
const totalQuestionsInLesson = currentLesson?.questions?.length || 0;
|
|
673
|
+
const isLastQuestion = stateRef.current.currentQuestionIndex >= totalQuestionsInLesson - 1;
|
|
674
|
+
|
|
652
675
|
const successMessage = currentQuestion.type === "code_test"
|
|
653
676
|
? `Great job! Your code passed all the tests! ${currentQuestion.explanation || ''}`
|
|
654
677
|
: `Excellent! That's correct! ${currentQuestion.explanation || ''}`;
|
|
@@ -660,8 +683,6 @@ const CurriculumLearning = forwardRef(({
|
|
|
660
683
|
lipsyncLang: config.lipsyncLang,
|
|
661
684
|
onSpeechEnd: () => {
|
|
662
685
|
// Notify parent that feedback is complete - parent decides next action
|
|
663
|
-
const currentLesson = getCurrentLesson();
|
|
664
|
-
const totalQuestionsInLesson = currentLesson?.questions?.length || 0;
|
|
665
686
|
callbacksRef.current.onCustomAction({
|
|
666
687
|
type: 'answerFeedbackComplete',
|
|
667
688
|
moduleIndex: stateRef.current.currentModuleIndex,
|
|
@@ -684,9 +705,15 @@ const CurriculumLearning = forwardRef(({
|
|
|
684
705
|
}
|
|
685
706
|
}
|
|
686
707
|
avatarRef.current.setBodyMovement("gesturing");
|
|
708
|
+
|
|
709
|
+
// Check if this is the last question
|
|
710
|
+
const currentLesson = getCurrentLesson();
|
|
711
|
+
const totalQuestionsInLesson = currentLesson?.questions?.length || 0;
|
|
712
|
+
const isLastQuestion = stateRef.current.currentQuestionIndex >= totalQuestionsInLesson - 1;
|
|
713
|
+
|
|
687
714
|
const failureMessage = currentQuestion.type === "code_test"
|
|
688
715
|
? `Your code didn't pass all the tests. ${currentQuestion.explanation || 'Try again!'}`
|
|
689
|
-
: `Not quite right, but don't worry! ${currentQuestion.explanation || ''} Let's move on to the next question
|
|
716
|
+
: `Not quite right, but don't worry! ${currentQuestion.explanation || ''}${isLastQuestion ? '' : " Let's move on to the next question."}`;
|
|
690
717
|
|
|
691
718
|
const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
|
|
692
719
|
|
|
@@ -695,8 +722,6 @@ const CurriculumLearning = forwardRef(({
|
|
|
695
722
|
lipsyncLang: config.lipsyncLang,
|
|
696
723
|
onSpeechEnd: () => {
|
|
697
724
|
// Notify parent that feedback is complete - parent decides next action
|
|
698
|
-
const currentLesson = getCurrentLesson();
|
|
699
|
-
const totalQuestionsInLesson = currentLesson?.questions?.length || 0;
|
|
700
725
|
callbacksRef.current.onCustomAction({
|
|
701
726
|
type: 'answerFeedbackComplete',
|
|
702
727
|
moduleIndex: stateRef.current.currentModuleIndex,
|
|
@@ -54,6 +54,8 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
54
54
|
const speechEndIntervalRef = useRef(null); // Track onSpeechEnd polling interval
|
|
55
55
|
const isPausedRef = useRef(false); // Track pause state for interval checks
|
|
56
56
|
const speechProgressRef = useRef({ remainingText: null, originalText: null, options: null }); // Track speech progress for resume
|
|
57
|
+
const originalSentencesRef = useRef([]); // Track original text split into sentences
|
|
58
|
+
const processedSentenceCountRef = useRef(0); // Track how many sentences have been processed
|
|
57
59
|
const [isLoading, setIsLoading] = useState(true);
|
|
58
60
|
const [error, setError] = useState(null);
|
|
59
61
|
const [isReady, setIsReady] = useState(false);
|
|
@@ -295,6 +297,17 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
295
297
|
pausedSpeechRef.current = { text: textToSpeak, options };
|
|
296
298
|
// Reset progress tracking when starting new speech
|
|
297
299
|
speechProgressRef.current = { remainingText: null, originalText: null, options: null };
|
|
300
|
+
|
|
301
|
+
// Split text into sentences for tracking
|
|
302
|
+
// Use the same regex as the library: /[!\.\?\n\p{Extended_Pictographic}]/ug
|
|
303
|
+
const sentenceDividers = /[!\.\?\n\p{Extended_Pictographic}]/ug;
|
|
304
|
+
const sentences = textToSpeak
|
|
305
|
+
.split(sentenceDividers)
|
|
306
|
+
.map(s => s.trim())
|
|
307
|
+
.filter(s => s.length > 0);
|
|
308
|
+
originalSentencesRef.current = sentences;
|
|
309
|
+
processedSentenceCountRef.current = 0;
|
|
310
|
+
|
|
298
311
|
setIsPaused(false);
|
|
299
312
|
isPausedRef.current = false;
|
|
300
313
|
|
|
@@ -440,29 +453,56 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
440
453
|
speechEndIntervalRef.current = null;
|
|
441
454
|
}
|
|
442
455
|
|
|
443
|
-
//
|
|
456
|
+
// Calculate remaining text by reconstructing from original sentences
|
|
457
|
+
// We need to account for:
|
|
458
|
+
// 1. Currently playing sentence (in audioPlaylist) - gets cleared by pauseSpeaking()
|
|
459
|
+
// 2. Queued sentences (in speechQueue) - not yet sent to TTS
|
|
444
460
|
let remainingText = '';
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
.filter(text => text.length > 0)
|
|
458
|
-
.join(' ');
|
|
461
|
+
|
|
462
|
+
if (pausedSpeechRef.current && originalSentencesRef.current.length > 0) {
|
|
463
|
+
const totalSentences = originalSentencesRef.current.length;
|
|
464
|
+
|
|
465
|
+
// Count sentences currently in speechQueue (not yet sent to TTS)
|
|
466
|
+
const queuedSentenceCount = talkingHead.speechQueue
|
|
467
|
+
? talkingHead.speechQueue.filter(item => item && item.text && Array.isArray(item.text) && item.text.length > 0).length
|
|
468
|
+
: 0;
|
|
469
|
+
|
|
470
|
+
// Check if there's a sentence currently playing (in audioPlaylist)
|
|
471
|
+
// This will be cleared by pauseSpeaking(), so we need to account for it now
|
|
472
|
+
const hasCurrentlyPlaying = talkingHead.audioPlaylist && talkingHead.audioPlaylist.length > 0;
|
|
459
473
|
|
|
460
|
-
|
|
461
|
-
|
|
474
|
+
// Total sentences remaining = queued + currently playing (if any)
|
|
475
|
+
const remainingSentenceCount = queuedSentenceCount + (hasCurrentlyPlaying ? 1 : 0);
|
|
476
|
+
|
|
477
|
+
// Calculate which sentence index we're at
|
|
478
|
+
const currentSentenceIndex = totalSentences - remainingSentenceCount;
|
|
479
|
+
|
|
480
|
+
// If there are remaining sentences, reconstruct the text
|
|
481
|
+
if (remainingSentenceCount > 0 && currentSentenceIndex < totalSentences) {
|
|
482
|
+
const remainingSentences = originalSentencesRef.current.slice(currentSentenceIndex);
|
|
483
|
+
remainingText = remainingSentences.join('. ').trim();
|
|
484
|
+
|
|
485
|
+
// Fallback: if reconstruction didn't work, try extracting from queue directly
|
|
486
|
+
if (!remainingText && queuedSentenceCount > 0 && talkingHead.speechQueue) {
|
|
487
|
+
const remainingParts = talkingHead.speechQueue
|
|
488
|
+
.filter(item => item && item.text && Array.isArray(item.text) && item.text.length > 0)
|
|
489
|
+
.map(item => {
|
|
490
|
+
return item.text
|
|
491
|
+
.map(wordObj => wordObj.word || '')
|
|
492
|
+
.filter(word => word.length > 0)
|
|
493
|
+
.join(' ');
|
|
494
|
+
})
|
|
495
|
+
.filter(text => text.length > 0)
|
|
496
|
+
.join(' ');
|
|
497
|
+
|
|
498
|
+
if (remainingParts && remainingParts.trim()) {
|
|
499
|
+
remainingText = remainingParts.trim();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
462
502
|
}
|
|
463
503
|
}
|
|
464
504
|
|
|
465
|
-
// Always save progress for resume
|
|
505
|
+
// Always save progress for resume
|
|
466
506
|
if (pausedSpeechRef.current) {
|
|
467
507
|
speechProgressRef.current = {
|
|
468
508
|
remainingText: remainingText || null,
|