@elevenlabs/react-native 0.5.11 → 1.0.0-rc.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.
Files changed (62) hide show
  1. package/.turbo/turbo-build.log +2 -12
  2. package/.turbo/turbo-check-types.log +1 -1
  3. package/.turbo/turbo-generate-version.log +1 -1
  4. package/.turbo/turbo-lint$colon$es.log +2 -2
  5. package/.turbo/turbo-lint$colon$prettier.log +1 -1
  6. package/CHANGELOG.md +92 -0
  7. package/README.md +42 -324
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +7 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/index.react-native.d.ts +2 -0
  13. package/dist/index.react-native.d.ts.map +1 -0
  14. package/dist/index.react-native.js +35 -0
  15. package/dist/index.react-native.js.map +1 -0
  16. package/dist/version.d.ts +2 -1
  17. package/dist/version.d.ts.map +1 -0
  18. package/dist/version.js +3 -0
  19. package/dist/version.js.map +1 -0
  20. package/package.json +13 -26
  21. package/src/index.react-native.ts +51 -0
  22. package/src/index.ts +25 -16
  23. package/src/version.ts +1 -1
  24. package/tsconfig.build.json +19 -0
  25. package/tsconfig.build.tsbuildinfo +1 -0
  26. package/tsconfig.json +5 -17
  27. package/dist/ElevenLabsProvider.d.ts +0 -26
  28. package/dist/ElevenLabsProvider.test.d.ts +0 -1
  29. package/dist/components/LiveKitRoomWrapper.d.ts +0 -23
  30. package/dist/components/MessageHandler.d.ts +0 -14
  31. package/dist/hooks/useConversationCallbacks.d.ts +0 -5
  32. package/dist/hooks/useConversationSession.d.ts +0 -33
  33. package/dist/hooks/useLiveKitRoom.d.ts +0 -12
  34. package/dist/hooks/useMessageSending.d.ts +0 -7
  35. package/dist/lib.js +0 -2
  36. package/dist/lib.js.map +0 -1
  37. package/dist/lib.modern.js +0 -2
  38. package/dist/lib.modern.js.map +0 -1
  39. package/dist/lib.module.js +0 -2
  40. package/dist/lib.module.js.map +0 -1
  41. package/dist/lib.umd.js +0 -2
  42. package/dist/lib.umd.js.map +0 -1
  43. package/dist/types.d.ts +0 -110
  44. package/dist/utils/constants.d.ts +0 -1
  45. package/dist/utils/overrides.d.ts +0 -2
  46. package/dist/utils/text-only.d.ts +0 -2
  47. package/dist/utils/tokenUtils.d.ts +0 -2
  48. package/jest.config.cjs +0 -263
  49. package/jest.rn-mock.js +0 -15
  50. package/src/ElevenLabsProvider.test.tsx +0 -257
  51. package/src/ElevenLabsProvider.tsx +0 -349
  52. package/src/components/LiveKitRoomWrapper.tsx +0 -94
  53. package/src/components/MessageHandler.tsx +0 -237
  54. package/src/hooks/useConversationCallbacks.ts +0 -15
  55. package/src/hooks/useConversationSession.ts +0 -129
  56. package/src/hooks/useLiveKitRoom.ts +0 -100
  57. package/src/hooks/useMessageSending.ts +0 -33
  58. package/src/types.ts +0 -169
  59. package/src/utils/constants.ts +0 -1
  60. package/src/utils/overrides.ts +0 -48
  61. package/src/utils/text-only.ts +0 -22
  62. package/src/utils/tokenUtils.ts +0 -49
@@ -1,349 +0,0 @@
1
- import React from 'react';
2
- import { createContext, useContext, useState } from 'react';
3
- import { registerGlobals } from '@livekit/react-native';
4
- import type { LocalParticipant } from 'livekit-client';
5
- import type { Callbacks, ConversationConfig, ConversationStatus, ClientToolsConfig, AudioSessionConfig } from './types';
6
- import { constructOverrides } from './utils/overrides';
7
- import { DEFAULT_SERVER_URL } from './utils/constants';
8
- import { useConversationCallbacks } from './hooks/useConversationCallbacks';
9
- import { useConversationSession } from './hooks/useConversationSession';
10
- import { useLiveKitRoom } from './hooks/useLiveKitRoom';
11
- import { useMessageSending } from './hooks/useMessageSending';
12
- import { LiveKitRoomWrapper } from './components/LiveKitRoomWrapper';
13
-
14
- interface ConversationOptions extends Callbacks, Partial<ClientToolsConfig> {
15
- serverUrl?: string;
16
- tokenFetchUrl?: string;
17
- }
18
-
19
- export interface Conversation {
20
- startSession: (config: ConversationConfig) => Promise<void>;
21
- endSession: (reason?: "user" | "agent") => Promise<void>;
22
- status: ConversationStatus;
23
- isSpeaking: boolean;
24
- // TODO: Implement setVolume when LiveKit React Native supports it
25
- // setVolume: (volume: number) => void;
26
- canSendFeedback: boolean;
27
- getId: () => string;
28
- sendFeedback: (like: boolean) => void;
29
- sendContextualUpdate: (text: string) => void;
30
- sendUserMessage: (text: string) => void;
31
- sendUserActivity: () => void;
32
- setMicMuted: (muted: boolean) => void;
33
- }
34
-
35
- interface ElevenLabsContextType {
36
- conversation: Conversation;
37
- callbacksRef: { current: Callbacks };
38
- serverUrl: string;
39
- tokenFetchUrl?: string;
40
- clientTools: ClientToolsConfig['clientTools'];
41
- setCallbacks: (callbacks: Callbacks) => void;
42
- setServerUrl: (url: string) => void;
43
- setTokenFetchUrl: (url: string) => void;
44
- setClientTools: (tools: ClientToolsConfig['clientTools']) => void;
45
- }
46
-
47
- const ElevenLabsContext = createContext<ElevenLabsContextType | null>(null);
48
-
49
- export const useConversation = (options: ConversationOptions = {}): Conversation => {
50
- const context = useContext(ElevenLabsContext);
51
- if (!context) {
52
- throw new Error('useConversation must be used within ElevenLabsProvider');
53
- }
54
-
55
- const { serverUrl, tokenFetchUrl, clientTools, ...callbacks } = options;
56
-
57
- React.useEffect(() => {
58
- if (serverUrl) {
59
- context.setServerUrl(serverUrl);
60
- }
61
- }, [context, serverUrl]);
62
-
63
- React.useEffect(() => {
64
- if (tokenFetchUrl) {
65
- context.setTokenFetchUrl(tokenFetchUrl);
66
- }
67
- }, [context, tokenFetchUrl]);
68
-
69
- React.useEffect(() => {
70
- if (clientTools) {
71
- context.setClientTools(clientTools);
72
- }
73
- }, [context, clientTools]);
74
-
75
- // Update callbacks - since this only updates a ref, it's safe to call on every render
76
- // and doesn't cause re-renders of the provider or its consumers
77
- context.setCallbacks(callbacks);
78
-
79
- return context.conversation;
80
- };
81
-
82
- interface ElevenLabsProviderProps {
83
- children: React.ReactNode;
84
- audioSessionConfig?: AudioSessionConfig;
85
- }
86
-
87
- export const ElevenLabsProvider: React.FC<ElevenLabsProviderProps> = ({ children, audioSessionConfig }) => {
88
- // Initialize globals on mount
89
- registerGlobals();
90
-
91
- // State management
92
- const [token, setToken] = useState('');
93
- const [connect, setConnect] = useState(false);
94
- const [status, setStatus] = useState<ConversationStatus>('disconnected');
95
- const [serverUrl, setServerUrl] = useState(DEFAULT_SERVER_URL);
96
- const [tokenFetchUrl, setTokenFetchUrl] = useState<string | undefined>(undefined);
97
- const [conversationId, setConversationId] = useState<string>('');
98
- const [isSpeaking, setIsSpeaking] = useState(false);
99
- const [canSendFeedback, setCanSendFeedback] = useState(false);
100
-
101
- // Feedback state tracking
102
- const currentEventIdRef = React.useRef(1);
103
- const lastFeedbackEventIdRef = React.useRef(1);
104
-
105
- // Use ref for clientTools to avoid re-renders (like callbacks)
106
- const clientToolsRef = React.useRef<ClientToolsConfig['clientTools']>({});
107
-
108
- // Custom hooks
109
- const { callbacksRef, setCallbacks: setCallbacksBase } = useConversationCallbacks();
110
-
111
- // Enhanced setCallbacks that wraps onModeChange to update isSpeaking state
112
- const setCallbacks = React.useCallback((callbacks: Callbacks) => {
113
- const wrappedCallbacks = {
114
- ...callbacks,
115
- onModeChange: (event: { mode: 'speaking' | 'listening' }) => {
116
- setIsSpeaking(event.mode === 'speaking');
117
- callbacks.onModeChange?.(event);
118
- }
119
- };
120
- setCallbacksBase(wrappedCallbacks);
121
- }, [setCallbacksBase]);
122
-
123
- const {
124
- startSession,
125
- endSession,
126
- overrides,
127
- customLlmExtraBody,
128
- dynamicVariables,
129
- userId,
130
- textOnly,
131
- } = useConversationSession(callbacksRef, setStatus, setConnect, setToken, setConversationId, tokenFetchUrl);
132
-
133
- const {
134
- roomConnected,
135
- localParticipant,
136
- handleParticipantReady,
137
- handleConnected,
138
- handleDisconnected,
139
- handleError,
140
- } = useLiveKitRoom(callbacksRef, setStatus, conversationId, status, textOnly);
141
-
142
- // Enhanced connection handler to initialize feedback state
143
- const handleConnectedWithFeedback = React.useCallback(() => {
144
- // Reset feedback state when connecting
145
- currentEventIdRef.current = 1;
146
- lastFeedbackEventIdRef.current = 1;
147
- setCanSendFeedback(false);
148
- callbacksRef.current.onCanSendFeedbackChange?.({ canSendFeedback: false });
149
-
150
- handleConnected();
151
- }, [handleConnected, callbacksRef]);
152
-
153
- // Enhanced disconnection handler to reset feedback state
154
- const handleDisconnectedWithFeedback = React.useCallback(() => {
155
- setCanSendFeedback(false);
156
- setIsSpeaking(false);
157
- handleDisconnected();
158
- }, [handleDisconnected]);
159
-
160
- const { sendMessage } = useMessageSending(status, localParticipant, callbacksRef);
161
-
162
- const updateCanSendFeedback = React.useCallback(() => {
163
- const newCanSendFeedback = currentEventIdRef.current !== lastFeedbackEventIdRef.current;
164
-
165
- if (canSendFeedback !== newCanSendFeedback) {
166
- setCanSendFeedback(newCanSendFeedback);
167
- callbacksRef.current.onCanSendFeedbackChange?.({ canSendFeedback: newCanSendFeedback });
168
- }
169
- }, [canSendFeedback, callbacksRef]);
170
-
171
- const sendFeedback = React.useCallback((like: boolean) => {
172
- if (!canSendFeedback) {
173
- console.warn(
174
- lastFeedbackEventIdRef.current === 0
175
- ? "Cannot send feedback: the conversation has not started yet."
176
- : "Cannot send feedback: feedback has already been sent for the current response."
177
- );
178
- return;
179
- }
180
-
181
- const feedbackMessage = {
182
- type: "feedback",
183
- score: like ? "like" : "dislike",
184
- event_id: currentEventIdRef.current,
185
- };
186
-
187
- sendMessage(feedbackMessage);
188
- lastFeedbackEventIdRef.current = currentEventIdRef.current;
189
- updateCanSendFeedback();
190
- }, [canSendFeedback, sendMessage, updateCanSendFeedback]);
191
-
192
- // setVolume placeholder (to be implemented when LiveKit supports it)
193
- const setVolume = React.useCallback((volume: number) => {
194
- console.warn('setVolume is not yet implemented in React Native SDK');
195
- }, []);
196
-
197
- const getId = React.useCallback(() => conversationId, [conversationId]);
198
-
199
- const setMicMuted = React.useCallback((muted: boolean) => {
200
- if (localParticipant) {
201
- localParticipant.setMicrophoneEnabled(!muted);
202
- }
203
- }, [localParticipant]);
204
-
205
- // Update current event ID for feedback tracking
206
- const updateCurrentEventId = React.useCallback((eventId: number) => {
207
- currentEventIdRef.current = eventId;
208
- updateCanSendFeedback();
209
- }, [updateCanSendFeedback]);
210
-
211
- // Handle participant ready with overrides
212
- const handleParticipantReadyWithOverrides = React.useCallback((participant: LocalParticipant) => {
213
- handleParticipantReady(participant);
214
-
215
- const overridesEvent = constructOverrides({
216
- overrides,
217
- customLlmExtraBody,
218
- dynamicVariables,
219
- userId,
220
- });
221
-
222
- if (overridesEvent) {
223
- try {
224
- const encoder = new TextEncoder();
225
- const data = encoder.encode(JSON.stringify(overridesEvent));
226
- participant.publishData(data, { reliable: true });
227
- } catch (error) {
228
- console.error("Failed to send overrides:", error);
229
- callbacksRef.current.onError?.(error as string);
230
- }
231
- }
232
- }, [handleParticipantReady, overrides, customLlmExtraBody, dynamicVariables, userId, callbacksRef]);
233
-
234
- // Create setClientTools function that only updates ref
235
- const setClientTools = React.useCallback((tools: ClientToolsConfig['clientTools']) => {
236
- clientToolsRef.current = tools;
237
- }, []);
238
-
239
- // Memoize inline callback functions
240
- const sendContextualUpdate = React.useCallback((text: string) => {
241
- sendMessage({
242
- type: "contextual_update",
243
- text,
244
- });
245
- }, [sendMessage]);
246
-
247
- const sendUserMessage = React.useCallback((text: string) => {
248
- sendMessage({
249
- type: "user_message",
250
- text,
251
- });
252
- }, [sendMessage]);
253
-
254
- const sendUserActivity = React.useCallback(() => {
255
- sendMessage({
256
- type: "user_activity",
257
- });
258
- }, [sendMessage]);
259
-
260
- // Store all conversation values/functions in refs for stable access
261
- const conversationValuesRef = React.useRef({
262
- startSession,
263
- endSession,
264
- status,
265
- isSpeaking,
266
- canSendFeedback,
267
- getId,
268
- setMicMuted,
269
- sendFeedback,
270
- sendContextualUpdate,
271
- sendUserMessage,
272
- sendUserActivity,
273
- });
274
-
275
- // Update ref on every render
276
- conversationValuesRef.current = {
277
- startSession,
278
- endSession,
279
- status,
280
- isSpeaking,
281
- canSendFeedback,
282
- getId,
283
- setMicMuted,
284
- sendFeedback,
285
- sendContextualUpdate,
286
- sendUserMessage,
287
- sendUserActivity,
288
- };
289
-
290
- // Create a stable conversation object that never changes reference
291
- // This prevents infinite loops when conversation is used in useEffect dependencies
292
- const conversation = React.useMemo<Conversation>(() => ({
293
- get startSession() { return conversationValuesRef.current.startSession; },
294
- get endSession() { return conversationValuesRef.current.endSession; },
295
- get status() { return conversationValuesRef.current.status; },
296
- get isSpeaking() { return conversationValuesRef.current.isSpeaking; },
297
- get canSendFeedback() { return conversationValuesRef.current.canSendFeedback; },
298
- get getId() { return conversationValuesRef.current.getId; },
299
- get setMicMuted() { return conversationValuesRef.current.setMicMuted; },
300
- get sendFeedback() { return conversationValuesRef.current.sendFeedback; },
301
- get sendContextualUpdate() { return conversationValuesRef.current.sendContextualUpdate; },
302
- get sendUserMessage() { return conversationValuesRef.current.sendUserMessage; },
303
- get sendUserActivity() { return conversationValuesRef.current.sendUserActivity; },
304
- }), []); // Empty deps - object is created once and never recreated
305
-
306
- // Memoize the context value to prevent unnecessary re-renders of consumers
307
- const contextValue = React.useMemo<ElevenLabsContextType>(() => ({
308
- conversation,
309
- callbacksRef,
310
- serverUrl,
311
- tokenFetchUrl,
312
- clientTools: clientToolsRef.current,
313
- setCallbacks,
314
- setServerUrl,
315
- setTokenFetchUrl,
316
- setClientTools,
317
- }), [
318
- conversation,
319
- callbacksRef,
320
- serverUrl,
321
- tokenFetchUrl,
322
- setCallbacks,
323
- setClientTools,
324
- ]);
325
-
326
- return (
327
- <ElevenLabsContext.Provider value={contextValue}>
328
- <LiveKitRoomWrapper
329
- serverUrl={serverUrl}
330
- token={token}
331
- connect={connect}
332
- onConnected={handleConnectedWithFeedback}
333
- onDisconnected={handleDisconnectedWithFeedback}
334
- onError={handleError}
335
- roomConnected={roomConnected}
336
- callbacks={callbacksRef.current}
337
- onParticipantReady={handleParticipantReadyWithOverrides}
338
- sendMessage={sendMessage}
339
- clientTools={clientToolsRef.current}
340
- updateCurrentEventId={updateCurrentEventId}
341
- onEndSession={endSession}
342
- audioSessionConfig={audioSessionConfig}
343
- textOnly={textOnly}
344
- >
345
- {children}
346
- </LiveKitRoomWrapper>
347
- </ElevenLabsContext.Provider>
348
- );
349
- };
@@ -1,94 +0,0 @@
1
- // @ts-nocheck - pnpm hoisting causes duplicate React type definitions
2
- import React from 'react';
3
- import { LiveKitRoom } from '@livekit/react-native';
4
- import type { LocalParticipant } from 'livekit-client';
5
- import type { Callbacks, ClientToolsConfig, AudioSessionConfig } from '../types';
6
- import { MessageHandler } from './MessageHandler';
7
-
8
- interface LiveKitRoomWrapperProps {
9
- children: React.ReactNode;
10
- serverUrl: string;
11
- token: string;
12
- connect: boolean;
13
- onConnected: () => void;
14
- onDisconnected: () => void;
15
- onError: (error: Error) => void;
16
- roomConnected: boolean;
17
- callbacks: Callbacks;
18
- onParticipantReady: (participant: LocalParticipant) => void;
19
- sendMessage: (message: unknown) => void;
20
- clientTools: ClientToolsConfig['clientTools'];
21
- onEndSession: (reason?: "user" | "agent") => void;
22
- updateCurrentEventId?: (eventId: number) => void;
23
- audioSessionConfig?: AudioSessionConfig;
24
- textOnly?: boolean;
25
- }
26
-
27
- export const LiveKitRoomWrapper = ({
28
- children,
29
- serverUrl,
30
- token,
31
- connect,
32
- onConnected,
33
- onDisconnected,
34
- onError,
35
- roomConnected,
36
- callbacks,
37
- onParticipantReady,
38
- sendMessage,
39
- clientTools,
40
- updateCurrentEventId,
41
- onEndSession,
42
- audioSessionConfig,
43
- textOnly = false,
44
- }: LiveKitRoomWrapperProps) => {
45
- // Configure audio options based on audioSessionConfig
46
- const audioOptions = React.useMemo(() => {
47
- if (textOnly) {
48
- // For now, we have to enable an audio session (even for text only conversations) to be able to send messages.
49
- return true;
50
- }
51
-
52
- if (!audioSessionConfig?.allowMixingWithOthers) {
53
- return true;
54
- }
55
-
56
- // When mixing is enabled, configure audio to allow concurrent playback
57
- return {
58
- audio: {
59
- noiseSuppression: true,
60
- echoCancellation: true,
61
- },
62
- audioSessionConfiguration: {
63
- allowMixingWithOthers: true,
64
- },
65
- };
66
- }, [audioSessionConfig, textOnly]);
67
-
68
- return (
69
- <LiveKitRoom
70
- serverUrl={serverUrl}
71
- token={token}
72
- connect={connect}
73
- audio={audioOptions}
74
- video={false}
75
- options={{
76
- adaptiveStream: { pixelDensity: 'screen' },
77
- }}
78
- onConnected={onConnected}
79
- onDisconnected={onDisconnected}
80
- onError={onError}
81
- >
82
- <MessageHandler
83
- onReady={onParticipantReady}
84
- isConnected={roomConnected}
85
- callbacks={callbacks}
86
- sendMessage={sendMessage}
87
- clientTools={clientTools}
88
- updateCurrentEventId={updateCurrentEventId}
89
- onEndSession={onEndSession}
90
- />
91
- {children as any}
92
- </LiveKitRoom>
93
- );
94
- };
@@ -1,237 +0,0 @@
1
- import { useEffect } from "react";
2
- import { useLocalParticipant, useDataChannel, useRoomContext } from "@livekit/react-native";
3
- import { RoomEvent } from "livekit-client";
4
- import type { LocalParticipant, RemoteParticipant } from "livekit-client";
5
- import type {
6
- Callbacks,
7
- ClientToolsConfig,
8
- ClientToolCallEvent,
9
- ConversationEvent,
10
- AudioEventWithAlignment,
11
- } from "../types";
12
- import React from "react";
13
-
14
- interface MessageHandlerProps {
15
- onReady: (participant: LocalParticipant) => void;
16
- isConnected: boolean;
17
- callbacks: Callbacks;
18
- sendMessage: (message: unknown) => void;
19
- onEndSession: (reason: "user" | "agent") => void;
20
- clientTools?: ClientToolsConfig["clientTools"];
21
- updateCurrentEventId?: (eventId: number) => void;
22
- }
23
-
24
- export function isValidEvent(event: unknown): event is ConversationEvent {
25
- return typeof event === "object" && event !== null && "type" in event;
26
- }
27
-
28
- function extractMessageText(event: ConversationEvent): string | null {
29
- switch (event.type) {
30
- case "user_transcript":
31
- return event.user_transcription_event.user_transcript;
32
- case "agent_response":
33
- return event.agent_response_event.agent_response;
34
- default:
35
- return null;
36
- }
37
- }
38
-
39
- export const MessageHandler = ({
40
- onReady,
41
- isConnected,
42
- callbacks,
43
- sendMessage,
44
- clientTools = {},
45
- updateCurrentEventId,
46
- onEndSession,
47
- }: MessageHandlerProps) => {
48
- const { localParticipant } = useLocalParticipant();
49
- const room = useRoomContext();
50
-
51
- // Track agent response count for synthetic event IDs (WebRTC mode)
52
- const agentResponseCountRef = React.useRef(1);
53
-
54
- // Refs for callbacks to avoid unnecessary effect re-runs
55
- const onEndSessionRef = React.useRef(onEndSession);
56
- onEndSessionRef.current = onEndSession;
57
- const onReadyRef = React.useRef(onReady);
58
- onReadyRef.current = onReady;
59
- const callbacksRef = React.useRef(callbacks);
60
- callbacksRef.current = callbacks;
61
-
62
- // Detect agent disconnection
63
- useEffect(() => {
64
- const handleParticipantDisconnected = (participant: RemoteParticipant) => {
65
- if (participant.identity?.startsWith("agent")) {
66
- onEndSessionRef.current("agent");
67
- }
68
- };
69
-
70
- room.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected);
71
-
72
- return () => {
73
- room.off(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected);
74
- };
75
- }, [room]);
76
-
77
- // Reset agent response count when connection status changes
78
- useEffect(() => {
79
- if (!isConnected) {
80
- agentResponseCountRef.current = 1;
81
- }
82
- }, [isConnected]);
83
-
84
- useEffect(() => {
85
- if (isConnected && localParticipant) {
86
- onReadyRef.current(localParticipant);
87
- }
88
- }, [isConnected, localParticipant]);
89
-
90
- const handleClientToolCall = async (clientToolCall: ClientToolCallEvent) => {
91
- if (clientToolCall.client_tool_call.tool_name in clientTools) {
92
- try {
93
- const result =
94
- (await clientTools[clientToolCall.client_tool_call.tool_name](
95
- clientToolCall.client_tool_call.parameters
96
- )) ?? "Client tool execution successful."; // default client-tool call response
97
-
98
- // The API expects result to be a string, so we need to convert it if it's not already a string
99
- const formattedResult =
100
- typeof result === "object" ? JSON.stringify(result) : String(result);
101
-
102
- sendMessage({
103
- type: "client_tool_result",
104
- tool_call_id: clientToolCall.client_tool_call.tool_call_id,
105
- result: formattedResult,
106
- is_error: false,
107
- });
108
- } catch (e) {
109
- const errorMessage = `Client tool execution failed with following error: ${(e as Error)?.message}`;
110
- callbacksRef.current.onError?.(errorMessage, {
111
- clientToolName: clientToolCall.client_tool_call.tool_name,
112
- });
113
- sendMessage({
114
- type: "client_tool_result",
115
- tool_call_id: clientToolCall.client_tool_call.tool_call_id,
116
- result: `Client tool execution failed: ${(e as Error)?.message}`,
117
- is_error: true,
118
- });
119
- }
120
- } else {
121
- if (callbacksRef.current.onUnhandledClientToolCall) {
122
- callbacksRef.current.onUnhandledClientToolCall(clientToolCall.client_tool_call);
123
- return;
124
- }
125
-
126
- const errorMessage = `Client tool with name ${clientToolCall.client_tool_call.tool_name} is not defined on client`;
127
- callbacksRef.current.onError?.(errorMessage, {
128
- clientToolName: clientToolCall.client_tool_call.tool_name,
129
- });
130
- sendMessage({
131
- type: "client_tool_result",
132
- tool_call_id: clientToolCall.client_tool_call.tool_call_id,
133
- result: errorMessage,
134
- is_error: true,
135
- });
136
- }
137
- };
138
-
139
- const _ = useDataChannel(msg => {
140
- const decoder = new TextDecoder();
141
- const message = JSON.parse(decoder.decode(msg.payload));
142
-
143
- if (!isValidEvent(message)) {
144
- callbacksRef.current.onDebug?.({
145
- type: "invalid_event",
146
- message,
147
- });
148
- return;
149
- }
150
-
151
- const messageText = extractMessageText(message);
152
- if (messageText !== null) {
153
- callbacksRef.current.onMessage?.({
154
- message: messageText,
155
- source: message.type === "user_transcript" ? "user" : "ai",
156
- role: message.type === "user_transcript" ? "user" : "agent",
157
- });
158
- }
159
-
160
- if (msg.from?.isAgent) {
161
- callbacksRef.current.onModeChange?.({
162
- mode: msg.from?.isSpeaking ? "speaking" : "listening",
163
- });
164
-
165
- // Track agent responses for feedback (WebRTC mode needs synthetic event IDs)
166
- if (message.type === "agent_response" && updateCurrentEventId) {
167
- const eventId = agentResponseCountRef.current++;
168
- updateCurrentEventId(eventId);
169
- }
170
- }
171
-
172
- switch (message.type) {
173
- case "ping":
174
- sendMessage({
175
- type: "pong",
176
- event_id: message.ping_event.event_id,
177
- });
178
- break;
179
- case "client_tool_call":
180
- handleClientToolCall(message);
181
- break;
182
- case "audio": {
183
- const audioEvent = message.audio_event as AudioEventWithAlignment;
184
- if (audioEvent.audio_base_64) {
185
- callbacksRef.current.onAudio?.(audioEvent.audio_base_64);
186
- }
187
- if (audioEvent.alignment) {
188
- callbacksRef.current.onAudioAlignment?.(audioEvent.alignment);
189
- }
190
- break;
191
- }
192
- case "vad_score":
193
- callbacksRef.current.onVadScore?.({
194
- vadScore: message.vad_score_event.vad_score,
195
- });
196
- break;
197
- case "interruption":
198
- callbacksRef.current.onInterruption?.(message.interruption_event);
199
- break;
200
- case "mcp_tool_call":
201
- callbacksRef.current.onMCPToolCall?.(message.mcp_tool_call);
202
- break;
203
- case "mcp_connection_status":
204
- callbacksRef.current.onMCPConnectionStatus?.(message.mcp_connection_status);
205
- break;
206
- case "agent_tool_request":
207
- callbacksRef.current.onAgentToolRequest?.(message.agent_tool_request);
208
- break;
209
- case "agent_tool_response":
210
- callbacksRef.current.onAgentToolResponse?.(message.agent_tool_response);
211
-
212
- if (message.agent_tool_response.tool_name === "end_call") {
213
- // End the call
214
- onEndSessionRef.current("agent");
215
- }
216
- break;
217
- case "conversation_initiation_metadata":
218
- callbacksRef.current.onConversationMetadata?.(
219
- message.conversation_initiation_metadata_event
220
- );
221
- break;
222
- case "asr_initiation_metadata":
223
- callbacksRef.current.onAsrInitiationMetadata?.(
224
- message.asr_initiation_metadata_event
225
- );
226
- break;
227
- case "agent_chat_response_part":
228
- callbacksRef.current.onAgentChatResponsePart?.(message.text_response_part);
229
- break;
230
- default:
231
- callbacksRef.current.onDebug?.(message);
232
- break;
233
- }
234
- });
235
-
236
- return null;
237
- };
@@ -1,15 +0,0 @@
1
- import { useRef, useCallback } from "react";
2
- import type { Callbacks } from "../types";
3
-
4
- export const useConversationCallbacks = () => {
5
- const callbacksRef = useRef<Callbacks>({});
6
-
7
- const setCallbacks = useCallback((callbacks: Callbacks) => {
8
- callbacksRef.current = callbacks;
9
- }, []);
10
-
11
- return {
12
- callbacksRef,
13
- setCallbacks,
14
- };
15
- };