openclaw-liveavatar 1.0.0
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/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +42 -0
- package/.next/app-path-routes-manifest.json +6 -0
- package/.next/build-manifest.json +33 -0
- package/.next/cache/.previewinfo +1 -0
- package/.next/cache/.rscinfo +1 -0
- package/.next/cache/.tsbuildinfo +1 -0
- package/.next/cache/chrome-devtools-workspace-uuid +1 -0
- package/.next/cache/next-devtools-config.json +1 -0
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/3.pack +0 -0
- package/.next/cache/webpack/client-production/4.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/diagnostics/build-diagnostics.json +6 -0
- package/.next/diagnostics/framework.json +1 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +58 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +61 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.json +320 -0
- package/.next/routes-manifest.json +53 -0
- package/.next/server/app/_not-found/page.js +5 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +4 -0
- package/.next/server/app/_not-found.meta +8 -0
- package/.next/server/app/_not-found.rsc +15 -0
- package/.next/server/app/api/get-avatars/route.js +1 -0
- package/.next/server/app/api/get-avatars/route.js.nft.json +1 -0
- package/.next/server/app/api/get-avatars/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/start-session/route.js +1 -0
- package/.next/server/app/api/start-session/route.js.nft.json +1 -0
- package/.next/server/app/api/start-session/route_client-reference-manifest.js +1 -0
- package/.next/server/app/index.html +4 -0
- package/.next/server/app/index.meta +7 -0
- package/.next/server/app/index.rsc +16 -0
- package/.next/server/app/page.js +9 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +6 -0
- package/.next/server/chunks/361.js +9 -0
- package/.next/server/chunks/611.js +6 -0
- package/.next/server/chunks/873.js +22 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +4 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +19 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +6 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/chunks/144d3bae-37bcc55d23f188ee.js +1 -0
- package/.next/static/chunks/255-35bf8c00c5dde345.js +1 -0
- package/.next/static/chunks/336-a66237a0a1db954a.js +1 -0
- package/.next/static/chunks/4bd1b696-c023c6e3521b1417.js +1 -0
- package/.next/static/chunks/app/_not-found/page-dfc6e5d8e6c6203c.js +1 -0
- package/.next/static/chunks/app/api/get-avatars/route-8017e1cff542d5d0.js +1 -0
- package/.next/static/chunks/app/api/start-session/route-8017e1cff542d5d0.js +1 -0
- package/.next/static/chunks/app/layout-ff675313cc8f8fcf.js +1 -0
- package/.next/static/chunks/app/page-9e4b703722bef650.js +1 -0
- package/.next/static/chunks/framework-de98b93a850cfc71.js +1 -0
- package/.next/static/chunks/main-1a0dcce460eb61ce.js +1 -0
- package/.next/static/chunks/main-app-e7f1007edc7ad7e1.js +1 -0
- package/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
- package/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-4a462cecab786e93.js +1 -0
- package/.next/static/css/bfd73afa11897439.css +3 -0
- package/.next/static/v_GdCj8lVweDVhmIhhEcM/_buildManifest.js +1 -0
- package/.next/static/v_GdCj8lVweDVhmIhhEcM/_ssgManifest.js +1 -0
- package/.next/trace +2 -0
- package/.next/types/app/api/get-avatars/route.ts +347 -0
- package/.next/types/app/api/start-session/route.ts +347 -0
- package/.next/types/app/layout.ts +84 -0
- package/.next/types/app/page.ts +84 -0
- package/.next/types/cache-life.d.ts +141 -0
- package/.next/types/package.json +1 -0
- package/.next/types/routes.d.ts +74 -0
- package/.next/types/validator.ts +88 -0
- package/README.md +241 -0
- package/app/api/config.ts +18 -0
- package/app/api/get-avatars/route.ts +117 -0
- package/app/api/start-session/route.ts +95 -0
- package/app/globals.css +3 -0
- package/app/layout.tsx +37 -0
- package/app/page.tsx +9 -0
- package/bin/cli.js +100 -0
- package/package.json +66 -0
- package/src/components/LiveAvatarSession.tsx +825 -0
- package/src/components/OpenClawDemo.tsx +399 -0
- package/src/gateway/client.ts +522 -0
- package/src/gateway/types.ts +83 -0
- package/src/liveavatar/context.tsx +750 -0
- package/src/liveavatar/index.ts +6 -0
- package/src/liveavatar/types.ts +10 -0
- package/src/liveavatar/useAvatarActions.ts +41 -0
- package/src/liveavatar/useChatHistory.ts +7 -0
- package/src/liveavatar/useSession.ts +37 -0
- package/src/liveavatar/useTextChat.ts +32 -0
- package/src/liveavatar/useVoiceChat.ts +70 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import {
|
|
12
|
+
ConnectionQuality,
|
|
13
|
+
LiveAvatarSession,
|
|
14
|
+
SessionState,
|
|
15
|
+
SessionEvent,
|
|
16
|
+
VoiceChatEvent,
|
|
17
|
+
VoiceChatState,
|
|
18
|
+
AgentEventsEnum,
|
|
19
|
+
} from "@heygen/liveavatar-web-sdk";
|
|
20
|
+
import { LiveAvatarSessionMessage, MessageSender } from "./types";
|
|
21
|
+
import { LIVEAVATAR_API_URL } from "../../app/api/config";
|
|
22
|
+
import {
|
|
23
|
+
OpenClawGatewayClient,
|
|
24
|
+
getGatewayClient,
|
|
25
|
+
} from "../gateway/client";
|
|
26
|
+
import { GatewayConnectionState } from "../gateway/types";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Truncate text for TTS to avoid overwhelming the avatar with long responses.
|
|
30
|
+
* Keeps approximately 2-3 sentences (around 200 characters max).
|
|
31
|
+
*/
|
|
32
|
+
const truncateForTTS = (text: string, maxLength: number = 200): string => {
|
|
33
|
+
if (text.length <= maxLength) {
|
|
34
|
+
return text;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Try to cut at a sentence boundary
|
|
38
|
+
const truncated = text.substring(0, maxLength);
|
|
39
|
+
|
|
40
|
+
// Find the last sentence ending (., !, ?)
|
|
41
|
+
const lastSentenceEnd = Math.max(
|
|
42
|
+
truncated.lastIndexOf(". "),
|
|
43
|
+
truncated.lastIndexOf("! "),
|
|
44
|
+
truncated.lastIndexOf("? "),
|
|
45
|
+
truncated.lastIndexOf(".\n"),
|
|
46
|
+
truncated.lastIndexOf("!\n"),
|
|
47
|
+
truncated.lastIndexOf("?\n")
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (lastSentenceEnd > maxLength * 0.5) {
|
|
51
|
+
// Cut at sentence boundary if it's past halfway
|
|
52
|
+
return truncated.substring(0, lastSentenceEnd + 1).trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Otherwise cut at last space and add ellipsis
|
|
56
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
57
|
+
if (lastSpace > maxLength * 0.7) {
|
|
58
|
+
return truncated.substring(0, lastSpace).trim() + "...";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return truncated.trim() + "...";
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Placeholder phrases to speak when OpenClaw takes longer than 2 seconds to respond
|
|
66
|
+
*/
|
|
67
|
+
const PROCESSING_PHRASES = [
|
|
68
|
+
"Let me think about that...",
|
|
69
|
+
"One moment please...",
|
|
70
|
+
"Let me check on that...",
|
|
71
|
+
"Give me a second...",
|
|
72
|
+
"Looking into that...",
|
|
73
|
+
"Hmm, let me see...",
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const getRandomProcessingPhrase = (): string => {
|
|
77
|
+
return PROCESSING_PHRASES[Math.floor(Math.random() * PROCESSING_PHRASES.length)];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Intro phrases for when the avatar session starts
|
|
82
|
+
*/
|
|
83
|
+
const INTRO_PHRASES = [
|
|
84
|
+
"Hey there! I'm ready to help. What can I do for you today?",
|
|
85
|
+
"Hi! Good to see you. What would you like to work on?",
|
|
86
|
+
"Hello! I'm all set. What's on your mind?",
|
|
87
|
+
"Hey! Ready when you are. What can I help you with?",
|
|
88
|
+
"Hi there! Let's get started. What do you need?",
|
|
89
|
+
"Hello! I'm here to assist. What are you working on today?",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const DEMO_INTRO_PHRASES = [
|
|
93
|
+
"Hi there! Welcome to the OpenClaw demo. Feel free to ask me anything, or say 'help' to learn more!",
|
|
94
|
+
"Hello! This is the OpenClaw LiveAvatar demo. Try asking me what I can do!",
|
|
95
|
+
"Hey! Welcome to the demo. I'm here to show you how this works. What would you like to know?",
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const getRandomIntroPhrase = (isDemoMode: boolean): string => {
|
|
99
|
+
const phrases = isDemoMode ? DEMO_INTRO_PHRASES : INTRO_PHRASES;
|
|
100
|
+
return phrases[Math.floor(Math.random() * phrases.length)];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type LiveAvatarContextProps = {
|
|
104
|
+
sessionRef: React.RefObject<LiveAvatarSession>;
|
|
105
|
+
|
|
106
|
+
isMuted: boolean;
|
|
107
|
+
voiceChatState: VoiceChatState;
|
|
108
|
+
|
|
109
|
+
sessionState: SessionState;
|
|
110
|
+
isStreamReady: boolean;
|
|
111
|
+
connectionQuality: ConnectionQuality;
|
|
112
|
+
|
|
113
|
+
isUserTalking: boolean;
|
|
114
|
+
isAvatarTalking: boolean;
|
|
115
|
+
|
|
116
|
+
messages: LiveAvatarSessionMessage[];
|
|
117
|
+
addMessage: (message: LiveAvatarSessionMessage) => void;
|
|
118
|
+
addTypedMessage: (text: string) => void;
|
|
119
|
+
|
|
120
|
+
// OpenClaw Gateway state
|
|
121
|
+
gatewayState: GatewayConnectionState;
|
|
122
|
+
isProcessingAgent: boolean;
|
|
123
|
+
isDemoMode: boolean;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const LiveAvatarContext = createContext<LiveAvatarContextProps>({
|
|
127
|
+
sessionRef: {
|
|
128
|
+
current: null,
|
|
129
|
+
} as unknown as React.RefObject<LiveAvatarSession>,
|
|
130
|
+
connectionQuality: ConnectionQuality.UNKNOWN,
|
|
131
|
+
isMuted: true,
|
|
132
|
+
voiceChatState: VoiceChatState.INACTIVE,
|
|
133
|
+
sessionState: SessionState.DISCONNECTED,
|
|
134
|
+
isStreamReady: false,
|
|
135
|
+
isUserTalking: false,
|
|
136
|
+
isAvatarTalking: false,
|
|
137
|
+
messages: [],
|
|
138
|
+
addMessage: () => {},
|
|
139
|
+
addTypedMessage: () => {},
|
|
140
|
+
gatewayState: "disconnected",
|
|
141
|
+
isProcessingAgent: false,
|
|
142
|
+
isDemoMode: true,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
type LiveAvatarContextProviderProps = {
|
|
146
|
+
children: React.ReactNode;
|
|
147
|
+
sessionAccessToken: string;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const useSessionState = (sessionRef: React.RefObject<LiveAvatarSession>) => {
|
|
151
|
+
const [sessionState, setSessionState] = useState<SessionState>(
|
|
152
|
+
sessionRef.current?.state || SessionState.INACTIVE
|
|
153
|
+
);
|
|
154
|
+
const [connectionQuality, setConnectionQuality] = useState<ConnectionQuality>(
|
|
155
|
+
sessionRef.current?.connectionQuality || ConnectionQuality.UNKNOWN
|
|
156
|
+
);
|
|
157
|
+
const [isStreamReady, setIsStreamReady] = useState<boolean>(false);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (sessionRef.current) {
|
|
161
|
+
sessionRef.current.on(SessionEvent.SESSION_STATE_CHANGED, (state) => {
|
|
162
|
+
setSessionState(state);
|
|
163
|
+
if (state === SessionState.DISCONNECTED) {
|
|
164
|
+
sessionRef.current.removeAllListeners();
|
|
165
|
+
sessionRef.current.voiceChat.removeAllListeners();
|
|
166
|
+
setIsStreamReady(false);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
sessionRef.current.on(SessionEvent.SESSION_STREAM_READY, () => {
|
|
170
|
+
setIsStreamReady(true);
|
|
171
|
+
});
|
|
172
|
+
sessionRef.current.on(
|
|
173
|
+
SessionEvent.SESSION_CONNECTION_QUALITY_CHANGED,
|
|
174
|
+
setConnectionQuality
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}, [sessionRef]);
|
|
178
|
+
|
|
179
|
+
return { sessionState, isStreamReady, connectionQuality };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const useVoiceChatState = (sessionRef: React.RefObject<LiveAvatarSession>) => {
|
|
183
|
+
const [isMuted, setIsMuted] = useState(true);
|
|
184
|
+
const [voiceChatState, setVoiceChatState] = useState<VoiceChatState>(
|
|
185
|
+
sessionRef.current?.voiceChat.state || VoiceChatState.INACTIVE
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (sessionRef.current) {
|
|
190
|
+
sessionRef.current.voiceChat.on(VoiceChatEvent.MUTED, () => {
|
|
191
|
+
setIsMuted(true);
|
|
192
|
+
});
|
|
193
|
+
sessionRef.current.voiceChat.on(VoiceChatEvent.UNMUTED, () => {
|
|
194
|
+
setIsMuted(false);
|
|
195
|
+
});
|
|
196
|
+
sessionRef.current.voiceChat.on(
|
|
197
|
+
VoiceChatEvent.STATE_CHANGED,
|
|
198
|
+
setVoiceChatState
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}, [sessionRef]);
|
|
202
|
+
|
|
203
|
+
return { isMuted, voiceChatState };
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const useTalkingState = (sessionRef: React.RefObject<LiveAvatarSession>) => {
|
|
207
|
+
const [isUserTalking, setIsUserTalking] = useState(false);
|
|
208
|
+
const [isAvatarTalking, setIsAvatarTalking] = useState(false);
|
|
209
|
+
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (sessionRef.current) {
|
|
212
|
+
sessionRef.current.on(AgentEventsEnum.USER_SPEAK_STARTED, () => {
|
|
213
|
+
setIsUserTalking(true);
|
|
214
|
+
});
|
|
215
|
+
sessionRef.current.on(AgentEventsEnum.USER_SPEAK_ENDED, () => {
|
|
216
|
+
setIsUserTalking(false);
|
|
217
|
+
});
|
|
218
|
+
sessionRef.current.on(AgentEventsEnum.AVATAR_SPEAK_STARTED, () => {
|
|
219
|
+
setIsAvatarTalking(true);
|
|
220
|
+
});
|
|
221
|
+
sessionRef.current.on(AgentEventsEnum.AVATAR_SPEAK_ENDED, () => {
|
|
222
|
+
setIsAvatarTalking(false);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}, [sessionRef]);
|
|
226
|
+
|
|
227
|
+
return { isUserTalking, isAvatarTalking };
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Demo mode FAQ responses - comprehensive guide to the OpenClaw LiveAvatar integration
|
|
232
|
+
*/
|
|
233
|
+
const getDemoResponse = (text: string): string => {
|
|
234
|
+
const lowerText = text.toLowerCase().trim();
|
|
235
|
+
|
|
236
|
+
// Greetings
|
|
237
|
+
if (lowerText.match(/^(hi|hello|hey|greetings|good morning|good afternoon|good evening)\.?$/)) {
|
|
238
|
+
return "Hello! Welcome to the OpenClaw LiveAvatar integration demo. I'm here to show you how this works. Try asking me 'what is this?' or 'how does it work?' to learn more!";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// What is this / Introduction
|
|
242
|
+
if (lowerText.includes("what is this") || lowerText.includes("what are you") || lowerText.includes("who are you")) {
|
|
243
|
+
return "I'm a LiveAvatar - a real-time AI video avatar that serves as your voice and video interface to OpenClaw agents. Think of me as a friendly face for your AI assistant. When connected to OpenClaw, I'll speak your agent's responses and listen to your voice commands!";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// How does it work
|
|
247
|
+
if (lowerText.includes("how does it work") || lowerText.includes("how do you work") || lowerText.includes("explain")) {
|
|
248
|
+
return "Here's how it works: You speak to me or type a message. Your input goes to your OpenClaw agent, which processes it and generates a response. Then I speak that response back to you with natural lip-sync and expressions. It's like having a video call with your AI agent!";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// What is OpenClaw
|
|
252
|
+
if (lowerText.includes("openclaw") && (lowerText.includes("what") || lowerText.includes("tell me about"))) {
|
|
253
|
+
return "OpenClaw is an AI agent platform that lets you build and deploy intelligent assistants. These agents can handle tasks, answer questions, and integrate with your tools. This LiveAvatar integration adds a human-like video interface to make interactions more engaging and natural.";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// What is LiveAvatar
|
|
257
|
+
if (lowerText.includes("liveavatar") && (lowerText.includes("what") || lowerText.includes("tell me about"))) {
|
|
258
|
+
return "LiveAvatar is powered by HeyGen's streaming avatar technology. It creates real-time, photorealistic AI avatars that can speak any text with natural expressions and lip-sync. Combined with OpenClaw, it transforms text-based AI interactions into face-to-face conversations.";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Help / Commands
|
|
262
|
+
if (lowerText === "help" || lowerText.includes("what can i ask") || lowerText.includes("what can you do") || lowerText.includes("commands")) {
|
|
263
|
+
return "In demo mode, you can ask me about: 'What is this?', 'How does it work?', 'What is OpenClaw?', 'What is LiveAvatar?', 'How do I connect?', 'Features', 'Requirements', 'Pricing', or 'Get started'. Once connected to OpenClaw, I'll respond with your actual agent's intelligence!";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// How to connect / Setup
|
|
267
|
+
if (lowerText.includes("connect") || lowerText.includes("setup") || lowerText.includes("get started") || lowerText.includes("install")) {
|
|
268
|
+
return "To connect to your OpenClaw agent: First, make sure OpenClaw is running on your computer with the Gateway enabled on port 18789. Then refresh this page - I'll automatically detect the connection and switch from demo mode to live mode. You'll see the status change from 'Demo Mode' to 'OpenClaw Connected'.";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Features
|
|
272
|
+
if (lowerText.includes("feature")) {
|
|
273
|
+
return "Key features include: Voice-to-voice conversations with your AI agent, real-time video avatar with natural expressions, text chat as an alternative to voice, multiple avatar options to choose from, and seamless integration with your OpenClaw workflows. It's like giving your AI a face!";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Requirements
|
|
277
|
+
if (lowerText.includes("requirement") || lowerText.includes("need") || lowerText.includes("prerequisite")) {
|
|
278
|
+
return "To use this integration you'll need: An OpenClaw account with an active agent, the OpenClaw Gateway running locally, a LiveAvatar API key from HeyGen, a modern browser with microphone access, and a stable internet connection for the video stream.";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Pricing / Cost
|
|
282
|
+
if (lowerText.includes("price") || lowerText.includes("cost") || lowerText.includes("free") || lowerText.includes("pricing")) {
|
|
283
|
+
return "LiveAvatar sessions consume HeyGen credits based on session duration. OpenClaw has its own pricing for agent usage. Check openclaw.ai and heygen.com for current pricing. This demo mode is free to try and shows you exactly how the integration works!";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Demo mode explanation
|
|
287
|
+
if (lowerText.includes("demo mode") || lowerText.includes("demo")) {
|
|
288
|
+
return "You're currently in demo mode because no OpenClaw Gateway connection was detected. In this mode, I respond with pre-set information about the integration. Once you connect to OpenClaw, I'll relay your messages to your actual AI agent and speak its responses!";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Voice / Microphone
|
|
292
|
+
if (lowerText.includes("voice") || lowerText.includes("microphone") || lowerText.includes("speak") || lowerText.includes("talk")) {
|
|
293
|
+
return "You can talk to me using your microphone! Click the green microphone button to unmute, then just speak naturally. I'll transcribe what you say, process it, and respond verbally. You can also type in the chat box if you prefer text input.";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Avatar / Change avatar
|
|
297
|
+
if (lowerText.includes("avatar") || lowerText.includes("change") || lowerText.includes("appearance")) {
|
|
298
|
+
return "You can change my appearance by clicking the person icon in the bottom right of the video. This opens the avatar selector where you can choose from different available avatars. Each avatar has its own look and voice!";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Goodbye
|
|
302
|
+
if (lowerText.includes("bye") || lowerText.includes("goodbye") || lowerText.includes("see you") || lowerText.includes("thanks")) {
|
|
303
|
+
return "Thank you for trying the OpenClaw LiveAvatar demo! When you're ready to use it with your actual OpenClaw agent, just start the Gateway and refresh this page. Have a great day!";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Default response for unrecognized input
|
|
307
|
+
return `I heard: "${text}". I'm currently in demo mode, showing you how this integration works. Try asking me about 'what is this?', 'how does it work?', or type 'help' for more options. Once connected to OpenClaw, your agent will provide intelligent responses to any question!`;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Hook to bridge LiveAvatar transcriptions to OpenClaw Gateway (or demo mode)
|
|
312
|
+
* When user speaks, send to OpenClaw agent and make avatar speak the response
|
|
313
|
+
*/
|
|
314
|
+
const useOpenClawBridge = (
|
|
315
|
+
sessionRef: React.RefObject<LiveAvatarSession>,
|
|
316
|
+
addMessage: (message: LiveAvatarSessionMessage) => void,
|
|
317
|
+
recentTypedMessages: React.RefObject<Set<string>>,
|
|
318
|
+
recentMessagesRef: React.RefObject<Set<string>>,
|
|
319
|
+
isAvatarTalkingRef: React.RefObject<boolean>
|
|
320
|
+
) => {
|
|
321
|
+
const [gatewayState, setGatewayState] = useState<GatewayConnectionState>("disconnected");
|
|
322
|
+
const [isProcessingAgent, setIsProcessingAgent] = useState(false);
|
|
323
|
+
const [isDemoMode, setIsDemoMode] = useState(true); // Start in demo mode, switch if Gateway connects
|
|
324
|
+
const gatewayRef = useRef<OpenClawGatewayClient | null>(null);
|
|
325
|
+
|
|
326
|
+
// Try to connect to OpenClaw Gateway on mount
|
|
327
|
+
// If connection fails, stay in demo mode
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
const gateway = getGatewayClient();
|
|
330
|
+
gatewayRef.current = gateway;
|
|
331
|
+
|
|
332
|
+
gateway.onConnectionState((state) => {
|
|
333
|
+
setGatewayState(state);
|
|
334
|
+
// If we successfully connect, disable demo mode
|
|
335
|
+
if (state === "connected") {
|
|
336
|
+
console.log("[OpenClaw] Gateway connected - switching to live mode");
|
|
337
|
+
setIsDemoMode(false);
|
|
338
|
+
}
|
|
339
|
+
// If we disconnect/error after being connected, fall back to demo mode
|
|
340
|
+
if (state === "disconnected" || state === "error") {
|
|
341
|
+
console.log("[OpenClaw] Gateway disconnected - falling back to demo mode");
|
|
342
|
+
setIsDemoMode(true);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Try to connect to gateway
|
|
347
|
+
gateway.connect().catch((err) => {
|
|
348
|
+
console.log("[OpenClaw] Gateway not available, staying in demo mode:", err.message);
|
|
349
|
+
setIsDemoMode(true);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return () => {
|
|
353
|
+
gateway.disconnect();
|
|
354
|
+
};
|
|
355
|
+
}, []);
|
|
356
|
+
|
|
357
|
+
// Listen to user transcriptions and respond
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
const session = sessionRef.current;
|
|
360
|
+
if (!session) return;
|
|
361
|
+
|
|
362
|
+
// Handler for user transcriptions
|
|
363
|
+
const handleUserTranscription = async (data: {
|
|
364
|
+
text?: string;
|
|
365
|
+
transcript?: string;
|
|
366
|
+
}) => {
|
|
367
|
+
const text = data.text || data.transcript || "";
|
|
368
|
+
if (!text.trim()) return;
|
|
369
|
+
|
|
370
|
+
// Skip if avatar is currently speaking (avoid echo/feedback loop)
|
|
371
|
+
if (isAvatarTalkingRef.current) {
|
|
372
|
+
console.log("[Voice] Ignoring transcription while avatar is speaking:", text.substring(0, 30));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Skip if this message was recently typed (to avoid duplicates)
|
|
377
|
+
if (recentTypedMessages.current?.has(text.trim())) {
|
|
378
|
+
recentTypedMessages.current.delete(text.trim());
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Skip if we've already seen this exact message recently (dedupe)
|
|
383
|
+
const messageKey = `user:${text.trim()}`;
|
|
384
|
+
if (recentMessagesRef.current?.has(messageKey)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
recentMessagesRef.current?.add(messageKey);
|
|
388
|
+
setTimeout(() => recentMessagesRef.current?.delete(messageKey), 3000);
|
|
389
|
+
|
|
390
|
+
// Add user message to chat
|
|
391
|
+
addMessage({
|
|
392
|
+
sender: MessageSender.USER,
|
|
393
|
+
message: text,
|
|
394
|
+
timestamp: Date.now(),
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
setIsProcessingAgent(true);
|
|
399
|
+
|
|
400
|
+
let responseText: string;
|
|
401
|
+
let placeholderSpoken = false;
|
|
402
|
+
|
|
403
|
+
if (isDemoMode) {
|
|
404
|
+
// Demo mode: use comprehensive FAQ responses
|
|
405
|
+
console.log("[Demo] Processing voice message locally:", text);
|
|
406
|
+
responseText = getDemoResponse(text);
|
|
407
|
+
} else {
|
|
408
|
+
// Production mode: send to OpenClaw Gateway
|
|
409
|
+
const gateway = gatewayRef.current;
|
|
410
|
+
if (!gateway || gateway.state !== "connected") {
|
|
411
|
+
console.warn("[OpenClaw] Gateway not connected, cannot send message");
|
|
412
|
+
responseText = "I'm not connected to the agent. Please check the Gateway connection.";
|
|
413
|
+
} else {
|
|
414
|
+
console.log("[OpenClaw] Sending to agent:", text);
|
|
415
|
+
|
|
416
|
+
// Set up placeholder timeout - speak a filler if response takes > 2s
|
|
417
|
+
let placeholderTimeout: NodeJS.Timeout | null = null;
|
|
418
|
+
if (session && session.state === SessionState.CONNECTED) {
|
|
419
|
+
placeholderTimeout = setTimeout(async () => {
|
|
420
|
+
const placeholder = getRandomProcessingPhrase();
|
|
421
|
+
console.log("[Avatar] Speaking placeholder (slow response):", placeholder);
|
|
422
|
+
placeholderSpoken = true;
|
|
423
|
+
try {
|
|
424
|
+
await session.repeat(placeholder);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
console.error("[Avatar] Failed to speak placeholder:", err);
|
|
427
|
+
}
|
|
428
|
+
}, 2000);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const response = await gateway.sendToAgent(text);
|
|
432
|
+
|
|
433
|
+
// Clear placeholder timeout if response came back in time
|
|
434
|
+
if (placeholderTimeout) {
|
|
435
|
+
clearTimeout(placeholderTimeout);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
console.log("[OpenClaw] Agent response:", response);
|
|
439
|
+
|
|
440
|
+
if (response.status === "completed" && response.text) {
|
|
441
|
+
responseText = response.text;
|
|
442
|
+
} else {
|
|
443
|
+
responseText = "Sorry, I didn't get a response from the agent.";
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Parse response to extract TTS summary and full message
|
|
449
|
+
const gateway = gatewayRef.current;
|
|
450
|
+
let ttsText = responseText;
|
|
451
|
+
let displayText = responseText;
|
|
452
|
+
|
|
453
|
+
if (!isDemoMode && gateway) {
|
|
454
|
+
const parsed = gateway.parseResponse(responseText);
|
|
455
|
+
ttsText = parsed.tts;
|
|
456
|
+
displayText = parsed.full;
|
|
457
|
+
} else {
|
|
458
|
+
// Demo mode: just truncate for TTS
|
|
459
|
+
ttsText = truncateForTTS(responseText);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Add full response to chat (without the TTS block)
|
|
463
|
+
addMessage({
|
|
464
|
+
sender: MessageSender.AVATAR,
|
|
465
|
+
message: displayText,
|
|
466
|
+
timestamp: Date.now(),
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Make avatar speak - only the TTS summary
|
|
470
|
+
// If placeholder was spoken, wait a moment before speaking the actual response
|
|
471
|
+
if (session && session.state === SessionState.CONNECTED) {
|
|
472
|
+
try {
|
|
473
|
+
if (placeholderSpoken) {
|
|
474
|
+
// Small delay to let placeholder finish
|
|
475
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
476
|
+
}
|
|
477
|
+
console.log("[Avatar] Speaking TTS summary:", ttsText);
|
|
478
|
+
await session.repeat(ttsText);
|
|
479
|
+
} catch (speakErr) {
|
|
480
|
+
console.error("[Avatar] Failed to make avatar speak:", speakErr);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} catch (err) {
|
|
484
|
+
console.error("[Chat] Failed to process voice message:", err);
|
|
485
|
+
addMessage({
|
|
486
|
+
sender: MessageSender.AVATAR,
|
|
487
|
+
message: "Sorry, I couldn't process that. Please try again.",
|
|
488
|
+
timestamp: Date.now(),
|
|
489
|
+
});
|
|
490
|
+
} finally {
|
|
491
|
+
setIsProcessingAgent(false);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// Register listener for user transcription
|
|
496
|
+
session.on(AgentEventsEnum.USER_TRANSCRIPTION, handleUserTranscription);
|
|
497
|
+
|
|
498
|
+
return () => {
|
|
499
|
+
session.off(AgentEventsEnum.USER_TRANSCRIPTION, handleUserTranscription);
|
|
500
|
+
};
|
|
501
|
+
}, [sessionRef, addMessage, recentTypedMessages, recentMessagesRef, isDemoMode]);
|
|
502
|
+
|
|
503
|
+
return { gatewayState, isProcessingAgent, isDemoMode };
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
export const LiveAvatarContextProvider = ({
|
|
507
|
+
children,
|
|
508
|
+
sessionAccessToken,
|
|
509
|
+
}: LiveAvatarContextProviderProps) => {
|
|
510
|
+
// Voice chat config - start unmuted so user can speak immediately
|
|
511
|
+
const config = {
|
|
512
|
+
voiceChat: {
|
|
513
|
+
defaultMuted: false,
|
|
514
|
+
},
|
|
515
|
+
apiUrl: LIVEAVATAR_API_URL,
|
|
516
|
+
};
|
|
517
|
+
const sessionRef = useRef<LiveAvatarSession>(
|
|
518
|
+
new LiveAvatarSession(sessionAccessToken, config)
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
const [messages, setMessages] = useState<LiveAvatarSessionMessage[]>([]);
|
|
522
|
+
|
|
523
|
+
// Track recently typed messages to avoid duplicates from transcription events
|
|
524
|
+
const recentTypedMessagesRef = useRef<Set<string>>(new Set());
|
|
525
|
+
// Track all recent messages to dedupe events that fire multiple times
|
|
526
|
+
const recentMessagesRef = useRef<Set<string>>(new Set());
|
|
527
|
+
|
|
528
|
+
const addMessage = useCallback((message: LiveAvatarSessionMessage) => {
|
|
529
|
+
setMessages((prev) => [...prev, message]);
|
|
530
|
+
}, []);
|
|
531
|
+
|
|
532
|
+
const { sessionState, isStreamReady, connectionQuality } =
|
|
533
|
+
useSessionState(sessionRef);
|
|
534
|
+
|
|
535
|
+
const { isMuted, voiceChatState } = useVoiceChatState(sessionRef);
|
|
536
|
+
const { isUserTalking, isAvatarTalking } = useTalkingState(sessionRef);
|
|
537
|
+
|
|
538
|
+
// Ref to track avatar talking state for use in async handlers (avoids stale closure)
|
|
539
|
+
const isAvatarTalkingRef = useRef(false);
|
|
540
|
+
useEffect(() => {
|
|
541
|
+
isAvatarTalkingRef.current = isAvatarTalking;
|
|
542
|
+
}, [isAvatarTalking]);
|
|
543
|
+
|
|
544
|
+
// Bridge to OpenClaw Gateway - this determines demo mode
|
|
545
|
+
const { gatewayState, isProcessingAgent: isProcessingVoiceAgent, isDemoMode } = useOpenClawBridge(
|
|
546
|
+
sessionRef,
|
|
547
|
+
addMessage,
|
|
548
|
+
recentTypedMessagesRef,
|
|
549
|
+
recentMessagesRef,
|
|
550
|
+
isAvatarTalkingRef
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
// State for tracking if we're processing a typed message
|
|
554
|
+
const [isProcessingTypedMessage, setIsProcessingTypedMessage] = useState(false);
|
|
555
|
+
const gatewayClientRef = useRef<OpenClawGatewayClient | null>(null);
|
|
556
|
+
const hasPlayedIntroRef = useRef(false);
|
|
557
|
+
|
|
558
|
+
// Store gateway client reference
|
|
559
|
+
useEffect(() => {
|
|
560
|
+
gatewayClientRef.current = getGatewayClient();
|
|
561
|
+
}, []);
|
|
562
|
+
|
|
563
|
+
// Play intro message when stream is ready
|
|
564
|
+
useEffect(() => {
|
|
565
|
+
console.log("[Intro] Effect triggered - isStreamReady:", isStreamReady, "hasPlayed:", hasPlayedIntroRef.current);
|
|
566
|
+
|
|
567
|
+
if (!isStreamReady) return;
|
|
568
|
+
if (hasPlayedIntroRef.current) return;
|
|
569
|
+
|
|
570
|
+
const session = sessionRef.current;
|
|
571
|
+
if (!session) {
|
|
572
|
+
console.log("[Intro] No session ref");
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
console.log("[Intro] Session state:", session.state);
|
|
577
|
+
|
|
578
|
+
// Mark as played immediately to prevent double-play
|
|
579
|
+
hasPlayedIntroRef.current = true;
|
|
580
|
+
|
|
581
|
+
const playIntro = async () => {
|
|
582
|
+
const introPhrase = getRandomIntroPhrase(isDemoMode);
|
|
583
|
+
|
|
584
|
+
addMessage({
|
|
585
|
+
sender: MessageSender.AVATAR,
|
|
586
|
+
message: introPhrase,
|
|
587
|
+
timestamp: Date.now(),
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
console.log("[Intro] Playing intro:", introPhrase);
|
|
592
|
+
await session.repeat(introPhrase);
|
|
593
|
+
console.log("[Intro] Intro played successfully");
|
|
594
|
+
} catch (err) {
|
|
595
|
+
console.error("[Intro] Failed to play intro:", err);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// Small delay to ensure avatar is fully ready to speak
|
|
600
|
+
setTimeout(playIntro, 1000);
|
|
601
|
+
}, [isStreamReady, isDemoMode, addMessage, sessionRef]);
|
|
602
|
+
|
|
603
|
+
// Add a typed message (from text input) - adds to messages and gets response
|
|
604
|
+
const addTypedMessage = useCallback(
|
|
605
|
+
async (text: string) => {
|
|
606
|
+
// Track this message so we can skip it if it appears in transcription
|
|
607
|
+
recentTypedMessagesRef.current.add(text);
|
|
608
|
+
// Clear from tracking after a short delay
|
|
609
|
+
setTimeout(() => {
|
|
610
|
+
recentTypedMessagesRef.current.delete(text);
|
|
611
|
+
}, 2000);
|
|
612
|
+
|
|
613
|
+
// Add to messages
|
|
614
|
+
addMessage({
|
|
615
|
+
sender: MessageSender.USER,
|
|
616
|
+
message: text,
|
|
617
|
+
timestamp: Date.now(),
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
setIsProcessingTypedMessage(true);
|
|
622
|
+
|
|
623
|
+
let responseText: string;
|
|
624
|
+
let placeholderSpoken = false;
|
|
625
|
+
const session = sessionRef.current;
|
|
626
|
+
|
|
627
|
+
if (isDemoMode) {
|
|
628
|
+
// Demo mode: use comprehensive FAQ responses
|
|
629
|
+
console.log("[Demo] Processing typed message locally:", text);
|
|
630
|
+
responseText = getDemoResponse(text);
|
|
631
|
+
} else {
|
|
632
|
+
// Production mode: send to OpenClaw Gateway
|
|
633
|
+
const gateway = gatewayClientRef.current;
|
|
634
|
+
if (!gateway || gateway.state !== "connected") {
|
|
635
|
+
console.warn("[OpenClaw] Gateway not connected, cannot send typed message");
|
|
636
|
+
responseText = "I'm not connected to the OpenClaw agent yet. Please make sure the Gateway is running.";
|
|
637
|
+
} else {
|
|
638
|
+
console.log("[OpenClaw] Sending typed message to agent:", text);
|
|
639
|
+
|
|
640
|
+
// Set up placeholder timeout - speak a filler if response takes > 2s
|
|
641
|
+
let placeholderTimeout: NodeJS.Timeout | null = null;
|
|
642
|
+
if (session && session.state === SessionState.CONNECTED) {
|
|
643
|
+
placeholderTimeout = setTimeout(async () => {
|
|
644
|
+
const placeholder = getRandomProcessingPhrase();
|
|
645
|
+
console.log("[Avatar] Speaking placeholder (slow response):", placeholder);
|
|
646
|
+
placeholderSpoken = true;
|
|
647
|
+
try {
|
|
648
|
+
await session.repeat(placeholder);
|
|
649
|
+
} catch (err) {
|
|
650
|
+
console.error("[Avatar] Failed to speak placeholder:", err);
|
|
651
|
+
}
|
|
652
|
+
}, 2000);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const response = await gateway.sendToAgent(text);
|
|
656
|
+
|
|
657
|
+
// Clear placeholder timeout if response came back in time
|
|
658
|
+
if (placeholderTimeout) {
|
|
659
|
+
clearTimeout(placeholderTimeout);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
console.log("[OpenClaw] Agent response:", response);
|
|
663
|
+
|
|
664
|
+
if (response.status === "completed" && response.text) {
|
|
665
|
+
responseText = response.text;
|
|
666
|
+
} else {
|
|
667
|
+
responseText = "Sorry, I didn't get a response from the agent.";
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Parse response to extract TTS summary and full message
|
|
673
|
+
const gateway = gatewayClientRef.current;
|
|
674
|
+
let ttsText = responseText;
|
|
675
|
+
let displayText = responseText;
|
|
676
|
+
|
|
677
|
+
if (!isDemoMode && gateway) {
|
|
678
|
+
const parsed = gateway.parseResponse(responseText);
|
|
679
|
+
ttsText = parsed.tts;
|
|
680
|
+
displayText = parsed.full;
|
|
681
|
+
} else {
|
|
682
|
+
// Demo mode: just truncate for TTS
|
|
683
|
+
ttsText = truncateForTTS(responseText);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Add full response to chat (without the TTS block)
|
|
687
|
+
addMessage({
|
|
688
|
+
sender: MessageSender.AVATAR,
|
|
689
|
+
message: displayText,
|
|
690
|
+
timestamp: Date.now(),
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Make avatar speak - only the TTS summary
|
|
694
|
+
// If placeholder was spoken, wait a moment before speaking the actual response
|
|
695
|
+
if (session && session.state === SessionState.CONNECTED) {
|
|
696
|
+
try {
|
|
697
|
+
if (placeholderSpoken) {
|
|
698
|
+
// Small delay to let placeholder finish
|
|
699
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
700
|
+
}
|
|
701
|
+
console.log("[Avatar] Speaking TTS summary:", ttsText);
|
|
702
|
+
await session.repeat(ttsText);
|
|
703
|
+
} catch (speakErr) {
|
|
704
|
+
console.error("[Avatar] Failed to make avatar speak:", speakErr);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
} catch (err) {
|
|
708
|
+
console.error("[Chat] Failed to process message:", err);
|
|
709
|
+
addMessage({
|
|
710
|
+
sender: MessageSender.AVATAR,
|
|
711
|
+
message: "Sorry, I couldn't process that. Please try again.",
|
|
712
|
+
timestamp: Date.now(),
|
|
713
|
+
});
|
|
714
|
+
} finally {
|
|
715
|
+
setIsProcessingTypedMessage(false);
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
[addMessage, sessionRef, isDemoMode]
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
// Combine processing states from voice and typed messages
|
|
722
|
+
const isProcessingAgent = isProcessingVoiceAgent || isProcessingTypedMessage;
|
|
723
|
+
|
|
724
|
+
return (
|
|
725
|
+
<LiveAvatarContext.Provider
|
|
726
|
+
value={{
|
|
727
|
+
sessionRef,
|
|
728
|
+
sessionState,
|
|
729
|
+
isStreamReady,
|
|
730
|
+
connectionQuality,
|
|
731
|
+
isMuted,
|
|
732
|
+
voiceChatState,
|
|
733
|
+
isUserTalking,
|
|
734
|
+
isAvatarTalking,
|
|
735
|
+
messages,
|
|
736
|
+
addMessage,
|
|
737
|
+
addTypedMessage,
|
|
738
|
+
gatewayState,
|
|
739
|
+
isProcessingAgent,
|
|
740
|
+
isDemoMode,
|
|
741
|
+
}}
|
|
742
|
+
>
|
|
743
|
+
{children}
|
|
744
|
+
</LiveAvatarContext.Provider>
|
|
745
|
+
);
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
export const useLiveAvatarContext = () => {
|
|
749
|
+
return useContext(LiveAvatarContext);
|
|
750
|
+
};
|