@sage-rsc/talking-head-react 1.0.29 → 1.0.31
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/{fbxAnimationLoader-Bsel7DtJ.js → fbxAnimationLoader-C0BDPJ-P.js} +1 -1
- package/dist/{fbxAnimationLoader-C_QSrujX.cjs → fbxAnimationLoader-C1sYMpqd.cjs} +1 -1
- package/dist/index-WXvj5jje.cjs +13 -0
- package/dist/{index-BvDk1TRw.js → index-iUfJQ70v.js} +1148 -1109
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/CurriculumLearning.jsx +158 -75
- package/src/lib/talkinghead.mjs +1 -1
- package/dist/index-C3A1_CcE.cjs +0 -13
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("./index-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("./index-WXvj5jje.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
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useRef, useEffect, forwardRef, useImperativeHandle, useCallback } from 'react';
|
|
1
|
+
import React, { useRef, useEffect, useLayoutEffect, forwardRef, useImperativeHandle, useCallback } from 'react';
|
|
2
2
|
import TalkingHeadAvatar from './TalkingHeadAvatar';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -55,45 +55,77 @@ const CurriculumLearning = forwardRef(({
|
|
|
55
55
|
const startTeachingRef = useRef(null);
|
|
56
56
|
const nextLessonRef = useRef(null);
|
|
57
57
|
const completeLessonRef = useRef(null);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
onCurriculumComplete,
|
|
66
|
-
onCustomAction
|
|
67
|
-
};
|
|
68
|
-
}, [onLessonStart, onLessonComplete, onQuestionAnswer, onCurriculumComplete, onCustomAction]);
|
|
69
|
-
|
|
70
|
-
const curriculum = curriculumData?.curriculum || {
|
|
58
|
+
const nextQuestionRef = useRef(null);
|
|
59
|
+
const completeCurriculumRef = useRef(null);
|
|
60
|
+
const startQuestionsRef = useRef(null);
|
|
61
|
+
const handleAnswerSelectRef = useRef(null);
|
|
62
|
+
|
|
63
|
+
// Store curriculum in ref to avoid dependency issues - initialize early
|
|
64
|
+
const curriculumRef = useRef(curriculumData?.curriculum || {
|
|
71
65
|
title: "Default Curriculum",
|
|
72
66
|
description: "No curriculum data provided",
|
|
73
67
|
language: "en",
|
|
74
68
|
modules: []
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Store avatar config in ref to avoid dependency issues - initialize early
|
|
72
|
+
const defaultAvatarConfigRef = useRef({
|
|
78
73
|
avatarUrl: avatarConfig.avatarUrl || "/avatars/brunette.glb",
|
|
79
74
|
avatarBody: avatarConfig.avatarBody || "F",
|
|
80
75
|
mood: avatarConfig.mood || "happy",
|
|
81
76
|
ttsLang: avatarConfig.ttsLang || "en",
|
|
82
|
-
ttsService: avatarConfig.ttsService || null,
|
|
83
|
-
ttsVoice: avatarConfig.ttsVoice || null,
|
|
77
|
+
ttsService: avatarConfig.ttsService || null,
|
|
78
|
+
ttsVoice: avatarConfig.ttsVoice || null,
|
|
84
79
|
ttsApiKey: avatarConfig.ttsApiKey || null,
|
|
85
80
|
bodyMovement: avatarConfig.bodyMovement || "gesturing",
|
|
86
81
|
movementIntensity: avatarConfig.movementIntensity || 0.7,
|
|
87
82
|
showFullAvatar: avatarConfig.showFullAvatar !== undefined ? avatarConfig.showFullAvatar : true,
|
|
88
83
|
animations: animations,
|
|
89
|
-
lipsyncLang: 'en'
|
|
90
|
-
};
|
|
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]);
|
|
91
122
|
|
|
92
123
|
// Helper to get current lesson/question
|
|
93
124
|
const getCurrentLesson = useCallback(() => {
|
|
125
|
+
const curriculum = curriculumRef.current || { modules: [] };
|
|
94
126
|
const module = curriculum.modules[stateRef.current.currentModuleIndex];
|
|
95
127
|
return module?.lessons[stateRef.current.currentLessonIndex];
|
|
96
|
-
}, [
|
|
128
|
+
}, []);
|
|
97
129
|
|
|
98
130
|
const getCurrentQuestion = useCallback(() => {
|
|
99
131
|
const lesson = getCurrentLesson();
|
|
@@ -158,23 +190,26 @@ const CurriculumLearning = forwardRef(({
|
|
|
158
190
|
});
|
|
159
191
|
|
|
160
192
|
if (avatarRef.current) {
|
|
161
|
-
|
|
193
|
+
avatarRef.current.setMood("happy");
|
|
162
194
|
if (animations.lessonComplete) {
|
|
163
195
|
try {
|
|
164
196
|
avatarRef.current.playAnimation(animations.lessonComplete, true);
|
|
165
197
|
} catch (error) {
|
|
166
|
-
|
|
198
|
+
avatarRef.current.playCelebration();
|
|
167
199
|
}
|
|
168
200
|
}
|
|
169
201
|
|
|
170
202
|
// Check if there's a next lesson available
|
|
203
|
+
const curriculum = curriculumRef.current || { modules: [] };
|
|
171
204
|
const currentModule = curriculum.modules[stateRef.current.currentModuleIndex];
|
|
172
205
|
const hasNextLesson = stateRef.current.currentLessonIndex < (currentModule?.lessons?.length || 0) - 1;
|
|
173
206
|
|
|
207
|
+
const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
|
|
208
|
+
|
|
174
209
|
if (hasNextLesson) {
|
|
175
210
|
// Wait for speech to finish, then automatically move to next lesson
|
|
176
211
|
avatarRef.current.speakText(feedbackMessage, {
|
|
177
|
-
lipsyncLang:
|
|
212
|
+
lipsyncLang: config.lipsyncLang,
|
|
178
213
|
onSpeechEnd: () => {
|
|
179
214
|
// Add a small delay after speech ends for natural flow
|
|
180
215
|
setTimeout(() => {
|
|
@@ -188,25 +223,25 @@ const CurriculumLearning = forwardRef(({
|
|
|
188
223
|
} else {
|
|
189
224
|
// This is the last lesson, complete curriculum instead
|
|
190
225
|
avatarRef.current.speakText(feedbackMessage, {
|
|
191
|
-
lipsyncLang:
|
|
226
|
+
lipsyncLang: config.lipsyncLang,
|
|
192
227
|
onSpeechEnd: () => {
|
|
193
228
|
// Add a small delay after speech ends for natural flow
|
|
194
229
|
setTimeout(() => {
|
|
195
|
-
|
|
230
|
+
if (completeCurriculumRef.current) {
|
|
231
|
+
completeCurriculumRef.current();
|
|
232
|
+
}
|
|
196
233
|
}, 1000);
|
|
197
234
|
}
|
|
198
235
|
});
|
|
199
236
|
}
|
|
200
237
|
}
|
|
201
|
-
}, [animations.lessonComplete
|
|
202
|
-
|
|
203
|
-
// Assign ref immediately after function is defined
|
|
204
|
-
completeLessonRef.current = completeLesson;
|
|
238
|
+
}, [animations.lessonComplete]);
|
|
205
239
|
|
|
206
240
|
// Complete entire curriculum
|
|
207
241
|
const completeCurriculum = useCallback(() => {
|
|
208
242
|
stateRef.current.curriculumCompleted = true;
|
|
209
243
|
|
|
244
|
+
const curriculum = curriculumRef.current || { modules: [] };
|
|
210
245
|
callbacksRef.current.onCurriculumComplete({
|
|
211
246
|
modules: curriculum.modules.length,
|
|
212
247
|
totalLessons: curriculum.modules.reduce((sum, mod) => sum + mod.lessons.length, 0)
|
|
@@ -221,9 +256,10 @@ const CurriculumLearning = forwardRef(({
|
|
|
221
256
|
avatarRef.current.playCelebration();
|
|
222
257
|
}
|
|
223
258
|
}
|
|
224
|
-
|
|
259
|
+
const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
|
|
260
|
+
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 });
|
|
225
261
|
}
|
|
226
|
-
}, [animations.curriculumComplete
|
|
262
|
+
}, [animations.curriculumComplete]);
|
|
227
263
|
|
|
228
264
|
// Start asking questions
|
|
229
265
|
const startQuestions = useCallback(() => {
|
|
@@ -258,20 +294,23 @@ const CurriculumLearning = forwardRef(({
|
|
|
258
294
|
}
|
|
259
295
|
}
|
|
260
296
|
|
|
297
|
+
const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
|
|
298
|
+
|
|
261
299
|
// Introduce the first question properly based on type
|
|
262
300
|
if (firstQuestion.type === 'code_test') {
|
|
263
|
-
avatarRef.current.speakText(`Let's test your coding skills! Here's your first challenge: ${firstQuestion.question}`, { lipsyncLang:
|
|
301
|
+
avatarRef.current.speakText(`Let's test your coding skills! Here's your first challenge: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
|
|
264
302
|
} else if (firstQuestion.type === 'multiple_choice') {
|
|
265
|
-
avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang:
|
|
303
|
+
avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
|
|
266
304
|
} else if (firstQuestion.type === 'true_false') {
|
|
267
|
-
avatarRef.current.speakText(`Let's start with some true or false questions. First question: ${firstQuestion.question}`, { lipsyncLang:
|
|
305
|
+
avatarRef.current.speakText(`Let's start with some true or false questions. First question: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
|
|
268
306
|
} else {
|
|
269
|
-
avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang:
|
|
307
|
+
avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
|
|
270
308
|
}
|
|
271
309
|
} else if (avatarRef.current) {
|
|
272
|
-
|
|
310
|
+
const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
|
|
311
|
+
avatarRef.current.speakText("Now let me ask you some questions to test your understanding.", { lipsyncLang: config.lipsyncLang });
|
|
273
312
|
}
|
|
274
|
-
}, [animations.questionStart, getCurrentLesson, getCurrentQuestion
|
|
313
|
+
}, [animations.questionStart, getCurrentLesson, getCurrentQuestion]);
|
|
275
314
|
|
|
276
315
|
// Move to next question
|
|
277
316
|
const nextQuestion = useCallback(() => {
|
|
@@ -301,27 +340,29 @@ const CurriculumLearning = forwardRef(({
|
|
|
301
340
|
if (animations.nextQuestion) {
|
|
302
341
|
try {
|
|
303
342
|
avatarRef.current.playAnimation(animations.nextQuestion, true);
|
|
304
|
-
|
|
343
|
+
} catch (error) {
|
|
305
344
|
console.warn('Failed to play nextQuestion animation:', error);
|
|
306
345
|
}
|
|
307
346
|
}
|
|
308
347
|
|
|
348
|
+
const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
|
|
349
|
+
|
|
309
350
|
// Speak the question text with proper introduction
|
|
310
351
|
if (nextQuestionObj.type === 'code_test') {
|
|
311
352
|
avatarRef.current.speakText(`Great! Now let's move on to your next coding challenge: ${nextQuestionObj.question}`, {
|
|
312
|
-
lipsyncLang:
|
|
353
|
+
lipsyncLang: config.lipsyncLang
|
|
313
354
|
});
|
|
314
355
|
} else if (nextQuestionObj.type === 'multiple_choice') {
|
|
315
356
|
avatarRef.current.speakText(`Alright! Here's your next question: ${nextQuestionObj.question}`, {
|
|
316
|
-
lipsyncLang:
|
|
357
|
+
lipsyncLang: config.lipsyncLang
|
|
317
358
|
});
|
|
318
359
|
} else if (nextQuestionObj.type === 'true_false') {
|
|
319
360
|
avatarRef.current.speakText(`Now let's try this one: ${nextQuestionObj.question}`, {
|
|
320
|
-
lipsyncLang:
|
|
361
|
+
lipsyncLang: config.lipsyncLang
|
|
321
362
|
});
|
|
322
363
|
} else {
|
|
323
364
|
avatarRef.current.speakText(`Here's the next question: ${nextQuestionObj.question}`, {
|
|
324
|
-
lipsyncLang:
|
|
365
|
+
lipsyncLang: config.lipsyncLang
|
|
325
366
|
});
|
|
326
367
|
}
|
|
327
368
|
}
|
|
@@ -331,10 +372,11 @@ const CurriculumLearning = forwardRef(({
|
|
|
331
372
|
completeLessonRef.current();
|
|
332
373
|
}
|
|
333
374
|
}
|
|
334
|
-
}, [animations.nextQuestion, getCurrentLesson, getCurrentQuestion
|
|
375
|
+
}, [animations.nextQuestion, getCurrentLesson, getCurrentQuestion]);
|
|
335
376
|
|
|
336
377
|
// Move to next lesson
|
|
337
378
|
const nextLesson = useCallback(() => {
|
|
379
|
+
const curriculum = curriculumRef.current || { modules: [] };
|
|
338
380
|
const currentModule = curriculum.modules[stateRef.current.currentModuleIndex];
|
|
339
381
|
if (stateRef.current.currentLessonIndex < (currentModule?.lessons?.length || 0) - 1) {
|
|
340
382
|
stateRef.current.currentLessonIndex += 1;
|
|
@@ -353,7 +395,7 @@ const CurriculumLearning = forwardRef(({
|
|
|
353
395
|
});
|
|
354
396
|
|
|
355
397
|
if (avatarRef.current) {
|
|
356
|
-
|
|
398
|
+
avatarRef.current.setMood("happy");
|
|
357
399
|
avatarRef.current.setBodyMovement("idle");
|
|
358
400
|
|
|
359
401
|
// Automatically start teaching the next lesson after a brief pause
|
|
@@ -364,12 +406,11 @@ const CurriculumLearning = forwardRef(({
|
|
|
364
406
|
}, 500);
|
|
365
407
|
}
|
|
366
408
|
} else {
|
|
367
|
-
|
|
409
|
+
if (completeCurriculumRef.current) {
|
|
410
|
+
completeCurriculumRef.current();
|
|
411
|
+
}
|
|
368
412
|
}
|
|
369
|
-
}, [
|
|
370
|
-
|
|
371
|
-
// Assign ref immediately after function is defined
|
|
372
|
-
nextLessonRef.current = nextLesson;
|
|
413
|
+
}, []);
|
|
373
414
|
|
|
374
415
|
// Start teaching the lesson
|
|
375
416
|
const startTeaching = useCallback(() => {
|
|
@@ -383,10 +424,10 @@ const CurriculumLearning = forwardRef(({
|
|
|
383
424
|
// Play animation if available (can be overridden via custom action)
|
|
384
425
|
let animationPlayed = false;
|
|
385
426
|
if (animations.teaching) {
|
|
386
|
-
|
|
427
|
+
try {
|
|
387
428
|
avatarRef.current.playAnimation(animations.teaching, true);
|
|
388
429
|
animationPlayed = true;
|
|
389
|
-
|
|
430
|
+
} catch (error) {
|
|
390
431
|
console.warn('Failed to play teaching animation:', error);
|
|
391
432
|
}
|
|
392
433
|
}
|
|
@@ -395,7 +436,8 @@ const CurriculumLearning = forwardRef(({
|
|
|
395
436
|
avatarRef.current.setBodyMovement("gesturing");
|
|
396
437
|
}
|
|
397
438
|
|
|
398
|
-
|
|
439
|
+
const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
|
|
440
|
+
avatarRef.current.speakText(currentLesson.avatar_script, { lipsyncLang: config.lipsyncLang });
|
|
399
441
|
|
|
400
442
|
callbacksRef.current.onLessonStart({
|
|
401
443
|
moduleIndex: stateRef.current.currentModuleIndex,
|
|
@@ -415,8 +457,11 @@ const CurriculumLearning = forwardRef(({
|
|
|
415
457
|
setTimeout(() => {
|
|
416
458
|
stateRef.current.isTeaching = false;
|
|
417
459
|
if (currentLesson.questions && currentLesson.questions.length > 0) {
|
|
418
|
-
|
|
419
|
-
|
|
460
|
+
// Use ref to avoid circular dependency
|
|
461
|
+
if (startQuestionsRef.current) {
|
|
462
|
+
startQuestionsRef.current();
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
420
465
|
// No questions, complete the lesson using ref to avoid circular dependency
|
|
421
466
|
if (completeLessonRef.current) {
|
|
422
467
|
completeLessonRef.current();
|
|
@@ -424,10 +469,7 @@ const CurriculumLearning = forwardRef(({
|
|
|
424
469
|
}
|
|
425
470
|
}, 8000);
|
|
426
471
|
}
|
|
427
|
-
}, [animations.teaching, getCurrentLesson
|
|
428
|
-
|
|
429
|
-
// Assign ref immediately after function is defined
|
|
430
|
-
startTeachingRef.current = startTeaching;
|
|
472
|
+
}, [animations.teaching, getCurrentLesson]);
|
|
431
473
|
|
|
432
474
|
// Handle answer selection
|
|
433
475
|
const handleAnswerSelect = useCallback((answer) => {
|
|
@@ -449,11 +491,11 @@ const CurriculumLearning = forwardRef(({
|
|
|
449
491
|
|
|
450
492
|
if (avatarRef.current) {
|
|
451
493
|
if (isCorrect) {
|
|
452
|
-
|
|
494
|
+
avatarRef.current.setMood("happy");
|
|
453
495
|
if (animations.correct) {
|
|
454
|
-
|
|
496
|
+
try {
|
|
455
497
|
avatarRef.current.playReaction("happy");
|
|
456
|
-
|
|
498
|
+
} catch (error) {
|
|
457
499
|
avatarRef.current.setBodyMovement("happy");
|
|
458
500
|
}
|
|
459
501
|
}
|
|
@@ -462,22 +504,26 @@ const CurriculumLearning = forwardRef(({
|
|
|
462
504
|
? `Great job! Your code passed all the tests! ${currentQuestion.explanation || ''}`
|
|
463
505
|
: `Excellent! That's correct! ${currentQuestion.explanation || ''}`;
|
|
464
506
|
|
|
507
|
+
const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
|
|
508
|
+
|
|
465
509
|
// Wait for speech to finish before moving to next question
|
|
466
510
|
avatarRef.current.speakText(successMessage, {
|
|
467
|
-
lipsyncLang:
|
|
511
|
+
lipsyncLang: config.lipsyncLang,
|
|
468
512
|
onSpeechEnd: () => {
|
|
469
513
|
// Add a small delay after speech ends for natural flow
|
|
470
514
|
setTimeout(() => {
|
|
471
|
-
|
|
515
|
+
if (nextQuestionRef.current) {
|
|
516
|
+
nextQuestionRef.current();
|
|
517
|
+
}
|
|
472
518
|
}, 500);
|
|
473
519
|
}
|
|
474
520
|
});
|
|
475
|
-
|
|
521
|
+
} else {
|
|
476
522
|
avatarRef.current.setMood("sad");
|
|
477
523
|
if (animations.incorrect) {
|
|
478
524
|
try {
|
|
479
525
|
avatarRef.current.playAnimation(animations.incorrect, true);
|
|
480
|
-
|
|
526
|
+
} catch (error) {
|
|
481
527
|
avatarRef.current.setBodyMovement("idle");
|
|
482
528
|
}
|
|
483
529
|
}
|
|
@@ -486,22 +532,28 @@ const CurriculumLearning = forwardRef(({
|
|
|
486
532
|
? `Your code didn't pass all the tests. ${currentQuestion.explanation || 'Try again!'}`
|
|
487
533
|
: `Not quite right, but don't worry! ${currentQuestion.explanation || ''} Let's move on to the next question.`;
|
|
488
534
|
|
|
535
|
+
const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
|
|
536
|
+
|
|
489
537
|
// Wait for speech to finish before moving to next question
|
|
490
538
|
avatarRef.current.speakText(failureMessage, {
|
|
491
|
-
lipsyncLang:
|
|
539
|
+
lipsyncLang: config.lipsyncLang,
|
|
492
540
|
onSpeechEnd: () => {
|
|
493
541
|
// Add a small delay after speech ends for natural flow
|
|
494
542
|
setTimeout(() => {
|
|
495
|
-
|
|
543
|
+
if (nextQuestionRef.current) {
|
|
544
|
+
nextQuestionRef.current();
|
|
545
|
+
}
|
|
496
546
|
}, 500);
|
|
497
547
|
}
|
|
498
548
|
});
|
|
499
549
|
}
|
|
500
550
|
} else {
|
|
501
551
|
// If avatar not ready, move to next question immediately
|
|
502
|
-
|
|
552
|
+
if (nextQuestionRef.current) {
|
|
553
|
+
nextQuestionRef.current();
|
|
554
|
+
}
|
|
503
555
|
}
|
|
504
|
-
}, [animations.correct, animations.incorrect, getCurrentQuestion, checkAnswer
|
|
556
|
+
}, [animations.correct, animations.incorrect, getCurrentQuestion, checkAnswer]);
|
|
505
557
|
|
|
506
558
|
// Handle code test result submission
|
|
507
559
|
const handleCodeTestResult = useCallback((testResult) => {
|
|
@@ -542,8 +594,10 @@ const CurriculumLearning = forwardRef(({
|
|
|
542
594
|
});
|
|
543
595
|
|
|
544
596
|
// Handle answer using the same logic as regular questions
|
|
545
|
-
|
|
546
|
-
|
|
597
|
+
if (handleAnswerSelectRef.current) {
|
|
598
|
+
handleAnswerSelectRef.current(codeTestAnswer);
|
|
599
|
+
}
|
|
600
|
+
}, [getCurrentQuestion, checkAnswer]);
|
|
547
601
|
|
|
548
602
|
// Reset curriculum
|
|
549
603
|
const resetCurriculum = useCallback(() => {
|
|
@@ -571,6 +625,18 @@ const CurriculumLearning = forwardRef(({
|
|
|
571
625
|
}
|
|
572
626
|
}, [autoStart, getCurrentLesson]);
|
|
573
627
|
|
|
628
|
+
// Set refs after all functions are defined - use useLayoutEffect for synchronous execution
|
|
629
|
+
// This ensures refs are set before React commits the render, avoiding initialization errors
|
|
630
|
+
useLayoutEffect(() => {
|
|
631
|
+
startTeachingRef.current = startTeaching;
|
|
632
|
+
nextLessonRef.current = nextLesson;
|
|
633
|
+
completeLessonRef.current = completeLesson;
|
|
634
|
+
nextQuestionRef.current = nextQuestion;
|
|
635
|
+
completeCurriculumRef.current = completeCurriculum;
|
|
636
|
+
startQuestionsRef.current = startQuestions;
|
|
637
|
+
handleAnswerSelectRef.current = handleAnswerSelect;
|
|
638
|
+
});
|
|
639
|
+
|
|
574
640
|
// Expose methods via ref (for external control)
|
|
575
641
|
useImperativeHandle(ref, () => ({
|
|
576
642
|
// Curriculum control methods
|
|
@@ -594,7 +660,8 @@ const CurriculumLearning = forwardRef(({
|
|
|
594
660
|
speakText: async (text, options = {}) => {
|
|
595
661
|
// Ensure audio context is resumed before speaking
|
|
596
662
|
await avatarRef.current?.resumeAudioContext?.();
|
|
597
|
-
|
|
663
|
+
const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
|
|
664
|
+
avatarRef.current?.speakText(text, { ...options, lipsyncLang: options.lipsyncLang || config.lipsyncLang });
|
|
598
665
|
},
|
|
599
666
|
resumeAudioContext: async () => {
|
|
600
667
|
// Try to resume through avatar ref first
|
|
@@ -646,6 +713,22 @@ const CurriculumLearning = forwardRef(({
|
|
|
646
713
|
isAvatarReady: () => avatarRef.current?.isReady || false
|
|
647
714
|
}), [startTeaching, startQuestions, handleAnswerSelect, handleCodeTestResult, nextQuestion, nextLesson, completeLesson, completeCurriculum, resetCurriculum, getCurrentQuestion, getCurrentLesson]);
|
|
648
715
|
|
|
716
|
+
// Get current config for render
|
|
717
|
+
const defaultAvatarConfig = defaultAvatarConfigRef.current || {
|
|
718
|
+
avatarUrl: "/avatars/brunette.glb",
|
|
719
|
+
avatarBody: "F",
|
|
720
|
+
mood: "happy",
|
|
721
|
+
ttsLang: "en",
|
|
722
|
+
ttsService: null,
|
|
723
|
+
ttsVoice: null,
|
|
724
|
+
ttsApiKey: null,
|
|
725
|
+
bodyMovement: "gesturing",
|
|
726
|
+
movementIntensity: 0.7,
|
|
727
|
+
showFullAvatar: true,
|
|
728
|
+
animations: animations,
|
|
729
|
+
lipsyncLang: 'en'
|
|
730
|
+
};
|
|
731
|
+
|
|
649
732
|
return (
|
|
650
733
|
<div style={{ width: '100%', height: '100%' }}>
|
|
651
734
|
<TalkingHeadAvatar
|
|
@@ -676,4 +759,4 @@ const CurriculumLearning = forwardRef(({
|
|
|
676
759
|
|
|
677
760
|
CurriculumLearning.displayName = 'CurriculumLearning';
|
|
678
761
|
|
|
679
|
-
export default CurriculumLearning;
|
|
762
|
+
export default CurriculumLearning;
|
package/src/lib/talkinghead.mjs
CHANGED
|
@@ -3248,7 +3248,7 @@ class TalkingHead {
|
|
|
3248
3248
|
const module = LIPSYNC_MODULES[langLower];
|
|
3249
3249
|
|
|
3250
3250
|
if (module && module[className]) {
|
|
3251
|
-
|
|
3251
|
+
this.lipsync[lang] = new module[className];
|
|
3252
3252
|
console.log(`Loaded lip-sync module for ${lang}`);
|
|
3253
3253
|
} else {
|
|
3254
3254
|
console.warn(`Lip-sync module for ${lang} not found. Available modules:`, Object.keys(LIPSYNC_MODULES));
|