@elevenlabs/react-native 0.5.12 → 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.
- package/.turbo/turbo-build.log +2 -12
- package/.turbo/turbo-check-types.log +1 -1
- package/.turbo/turbo-generate-version.log +1 -1
- package/.turbo/turbo-lint$colon$es.log +2 -2
- package/.turbo/turbo-lint$colon$prettier.log +1 -1
- package/CHANGELOG.md +88 -4
- package/README.md +42 -324
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/index.react-native.d.ts +2 -0
- package/dist/index.react-native.d.ts.map +1 -0
- package/dist/index.react-native.js +35 -0
- package/dist/index.react-native.js.map +1 -0
- package/dist/version.d.ts +2 -1
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +13 -26
- package/src/index.react-native.ts +51 -0
- package/src/index.ts +25 -16
- package/src/version.ts +1 -1
- package/tsconfig.build.json +19 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +5 -17
- package/dist/ElevenLabsProvider.d.ts +0 -30
- package/dist/ElevenLabsProvider.test.d.ts +0 -1
- package/dist/components/LiveKitRoomWrapper.d.ts +0 -23
- package/dist/components/MessageHandler.d.ts +0 -14
- package/dist/hooks/useConversationCallbacks.d.ts +0 -5
- package/dist/hooks/useConversationSession.d.ts +0 -33
- package/dist/hooks/useLiveKitRoom.d.ts +0 -12
- package/dist/hooks/useMessageSending.d.ts +0 -7
- package/dist/lib.js +0 -2
- package/dist/lib.js.map +0 -1
- package/dist/lib.modern.js +0 -2
- package/dist/lib.modern.js.map +0 -1
- package/dist/lib.module.js +0 -2
- package/dist/lib.module.js.map +0 -1
- package/dist/lib.umd.js +0 -2
- package/dist/lib.umd.js.map +0 -1
- package/dist/types.d.ts +0 -111
- package/dist/utils/constants.d.ts +0 -1
- package/dist/utils/overrides.d.ts +0 -2
- package/dist/utils/text-only.d.ts +0 -2
- package/dist/utils/tokenUtils.d.ts +0 -2
- package/jest.config.cjs +0 -263
- package/jest.rn-mock.js +0 -15
- package/src/ElevenLabsProvider.test.tsx +0 -257
- package/src/ElevenLabsProvider.tsx +0 -365
- package/src/components/LiveKitRoomWrapper.tsx +0 -94
- package/src/components/MessageHandler.tsx +0 -237
- package/src/hooks/useConversationCallbacks.ts +0 -15
- package/src/hooks/useConversationSession.ts +0 -130
- package/src/hooks/useLiveKitRoom.ts +0 -100
- package/src/hooks/useMessageSending.ts +0 -33
- package/src/types.ts +0 -170
- package/src/utils/constants.ts +0 -1
- package/src/utils/overrides.ts +0 -48
- package/src/utils/text-only.ts +0 -22
- package/src/utils/tokenUtils.ts +0 -52
|
@@ -1,365 +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
|
-
sendMultimodalMessage: (options: { text?: string; fileId?: string }) => void;
|
|
33
|
-
setMicMuted: (muted: boolean) => void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface ElevenLabsContextType {
|
|
37
|
-
conversation: Conversation;
|
|
38
|
-
callbacksRef: { current: Callbacks };
|
|
39
|
-
serverUrl: string;
|
|
40
|
-
tokenFetchUrl?: string;
|
|
41
|
-
clientTools: ClientToolsConfig['clientTools'];
|
|
42
|
-
setCallbacks: (callbacks: Callbacks) => void;
|
|
43
|
-
setServerUrl: (url: string) => void;
|
|
44
|
-
setTokenFetchUrl: (url: string) => void;
|
|
45
|
-
setClientTools: (tools: ClientToolsConfig['clientTools']) => void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const ElevenLabsContext = createContext<ElevenLabsContextType | null>(null);
|
|
49
|
-
|
|
50
|
-
export const useConversation = (options: ConversationOptions = {}): Conversation => {
|
|
51
|
-
const context = useContext(ElevenLabsContext);
|
|
52
|
-
if (!context) {
|
|
53
|
-
throw new Error('useConversation must be used within ElevenLabsProvider');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const { serverUrl, tokenFetchUrl, clientTools, ...callbacks } = options;
|
|
57
|
-
|
|
58
|
-
React.useEffect(() => {
|
|
59
|
-
if (serverUrl) {
|
|
60
|
-
context.setServerUrl(serverUrl);
|
|
61
|
-
}
|
|
62
|
-
}, [context, serverUrl]);
|
|
63
|
-
|
|
64
|
-
React.useEffect(() => {
|
|
65
|
-
if (tokenFetchUrl) {
|
|
66
|
-
context.setTokenFetchUrl(tokenFetchUrl);
|
|
67
|
-
}
|
|
68
|
-
}, [context, tokenFetchUrl]);
|
|
69
|
-
|
|
70
|
-
React.useEffect(() => {
|
|
71
|
-
if (clientTools) {
|
|
72
|
-
context.setClientTools(clientTools);
|
|
73
|
-
}
|
|
74
|
-
}, [context, clientTools]);
|
|
75
|
-
|
|
76
|
-
// Update callbacks - since this only updates a ref, it's safe to call on every render
|
|
77
|
-
// and doesn't cause re-renders of the provider or its consumers
|
|
78
|
-
context.setCallbacks(callbacks);
|
|
79
|
-
|
|
80
|
-
return context.conversation;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
interface ElevenLabsProviderProps {
|
|
84
|
-
children: React.ReactNode;
|
|
85
|
-
audioSessionConfig?: AudioSessionConfig;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export const ElevenLabsProvider: React.FC<ElevenLabsProviderProps> = ({ children, audioSessionConfig }) => {
|
|
89
|
-
// Initialize globals on mount
|
|
90
|
-
registerGlobals();
|
|
91
|
-
|
|
92
|
-
// State management
|
|
93
|
-
const [token, setToken] = useState('');
|
|
94
|
-
const [connect, setConnect] = useState(false);
|
|
95
|
-
const [status, setStatus] = useState<ConversationStatus>('disconnected');
|
|
96
|
-
const [serverUrl, setServerUrl] = useState(DEFAULT_SERVER_URL);
|
|
97
|
-
const [tokenFetchUrl, setTokenFetchUrl] = useState<string | undefined>(undefined);
|
|
98
|
-
const [conversationId, setConversationId] = useState<string>('');
|
|
99
|
-
const [isSpeaking, setIsSpeaking] = useState(false);
|
|
100
|
-
const [canSendFeedback, setCanSendFeedback] = useState(false);
|
|
101
|
-
|
|
102
|
-
// Feedback state tracking
|
|
103
|
-
const currentEventIdRef = React.useRef(1);
|
|
104
|
-
const lastFeedbackEventIdRef = React.useRef(1);
|
|
105
|
-
|
|
106
|
-
// Use ref for clientTools to avoid re-renders (like callbacks)
|
|
107
|
-
const clientToolsRef = React.useRef<ClientToolsConfig['clientTools']>({});
|
|
108
|
-
|
|
109
|
-
// Custom hooks
|
|
110
|
-
const { callbacksRef, setCallbacks: setCallbacksBase } = useConversationCallbacks();
|
|
111
|
-
|
|
112
|
-
// Enhanced setCallbacks that wraps onModeChange to update isSpeaking state
|
|
113
|
-
const setCallbacks = React.useCallback((callbacks: Callbacks) => {
|
|
114
|
-
const wrappedCallbacks = {
|
|
115
|
-
...callbacks,
|
|
116
|
-
onModeChange: (event: { mode: 'speaking' | 'listening' }) => {
|
|
117
|
-
setIsSpeaking(event.mode === 'speaking');
|
|
118
|
-
callbacks.onModeChange?.(event);
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
setCallbacksBase(wrappedCallbacks);
|
|
122
|
-
}, [setCallbacksBase]);
|
|
123
|
-
|
|
124
|
-
const {
|
|
125
|
-
startSession,
|
|
126
|
-
endSession,
|
|
127
|
-
overrides,
|
|
128
|
-
customLlmExtraBody,
|
|
129
|
-
dynamicVariables,
|
|
130
|
-
userId,
|
|
131
|
-
textOnly,
|
|
132
|
-
} = useConversationSession(callbacksRef, setStatus, setConnect, setToken, setConversationId, tokenFetchUrl);
|
|
133
|
-
|
|
134
|
-
const {
|
|
135
|
-
roomConnected,
|
|
136
|
-
localParticipant,
|
|
137
|
-
handleParticipantReady,
|
|
138
|
-
handleConnected,
|
|
139
|
-
handleDisconnected,
|
|
140
|
-
handleError,
|
|
141
|
-
} = useLiveKitRoom(callbacksRef, setStatus, conversationId, status, textOnly);
|
|
142
|
-
|
|
143
|
-
// Enhanced connection handler to initialize feedback state
|
|
144
|
-
const handleConnectedWithFeedback = React.useCallback(() => {
|
|
145
|
-
// Reset feedback state when connecting
|
|
146
|
-
currentEventIdRef.current = 1;
|
|
147
|
-
lastFeedbackEventIdRef.current = 1;
|
|
148
|
-
setCanSendFeedback(false);
|
|
149
|
-
callbacksRef.current.onCanSendFeedbackChange?.({ canSendFeedback: false });
|
|
150
|
-
|
|
151
|
-
handleConnected();
|
|
152
|
-
}, [handleConnected, callbacksRef]);
|
|
153
|
-
|
|
154
|
-
// Enhanced disconnection handler to reset feedback state
|
|
155
|
-
const handleDisconnectedWithFeedback = React.useCallback(() => {
|
|
156
|
-
setCanSendFeedback(false);
|
|
157
|
-
setIsSpeaking(false);
|
|
158
|
-
handleDisconnected();
|
|
159
|
-
}, [handleDisconnected]);
|
|
160
|
-
|
|
161
|
-
const { sendMessage } = useMessageSending(status, localParticipant, callbacksRef);
|
|
162
|
-
|
|
163
|
-
const updateCanSendFeedback = React.useCallback(() => {
|
|
164
|
-
const newCanSendFeedback = currentEventIdRef.current !== lastFeedbackEventIdRef.current;
|
|
165
|
-
|
|
166
|
-
if (canSendFeedback !== newCanSendFeedback) {
|
|
167
|
-
setCanSendFeedback(newCanSendFeedback);
|
|
168
|
-
callbacksRef.current.onCanSendFeedbackChange?.({ canSendFeedback: newCanSendFeedback });
|
|
169
|
-
}
|
|
170
|
-
}, [canSendFeedback, callbacksRef]);
|
|
171
|
-
|
|
172
|
-
const sendFeedback = React.useCallback((like: boolean) => {
|
|
173
|
-
if (!canSendFeedback) {
|
|
174
|
-
console.warn(
|
|
175
|
-
lastFeedbackEventIdRef.current === 0
|
|
176
|
-
? "Cannot send feedback: the conversation has not started yet."
|
|
177
|
-
: "Cannot send feedback: feedback has already been sent for the current response."
|
|
178
|
-
);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const feedbackMessage = {
|
|
183
|
-
type: "feedback",
|
|
184
|
-
score: like ? "like" : "dislike",
|
|
185
|
-
event_id: currentEventIdRef.current,
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
sendMessage(feedbackMessage);
|
|
189
|
-
lastFeedbackEventIdRef.current = currentEventIdRef.current;
|
|
190
|
-
updateCanSendFeedback();
|
|
191
|
-
}, [canSendFeedback, sendMessage, updateCanSendFeedback]);
|
|
192
|
-
|
|
193
|
-
// setVolume placeholder (to be implemented when LiveKit supports it)
|
|
194
|
-
const setVolume = React.useCallback((volume: number) => {
|
|
195
|
-
console.warn('setVolume is not yet implemented in React Native SDK');
|
|
196
|
-
}, []);
|
|
197
|
-
|
|
198
|
-
const getId = React.useCallback(() => conversationId, [conversationId]);
|
|
199
|
-
|
|
200
|
-
const setMicMuted = React.useCallback((muted: boolean) => {
|
|
201
|
-
if (localParticipant) {
|
|
202
|
-
localParticipant.setMicrophoneEnabled(!muted);
|
|
203
|
-
}
|
|
204
|
-
}, [localParticipant]);
|
|
205
|
-
|
|
206
|
-
// Update current event ID for feedback tracking
|
|
207
|
-
const updateCurrentEventId = React.useCallback((eventId: number) => {
|
|
208
|
-
currentEventIdRef.current = eventId;
|
|
209
|
-
updateCanSendFeedback();
|
|
210
|
-
}, [updateCanSendFeedback]);
|
|
211
|
-
|
|
212
|
-
// Handle participant ready with overrides
|
|
213
|
-
const handleParticipantReadyWithOverrides = React.useCallback((participant: LocalParticipant) => {
|
|
214
|
-
handleParticipantReady(participant);
|
|
215
|
-
|
|
216
|
-
const overridesEvent = constructOverrides({
|
|
217
|
-
overrides,
|
|
218
|
-
customLlmExtraBody,
|
|
219
|
-
dynamicVariables,
|
|
220
|
-
userId,
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
if (overridesEvent) {
|
|
224
|
-
try {
|
|
225
|
-
const encoder = new TextEncoder();
|
|
226
|
-
const data = encoder.encode(JSON.stringify(overridesEvent));
|
|
227
|
-
participant.publishData(data, { reliable: true });
|
|
228
|
-
} catch (error) {
|
|
229
|
-
console.error("Failed to send overrides:", error);
|
|
230
|
-
callbacksRef.current.onError?.(error as string);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}, [handleParticipantReady, overrides, customLlmExtraBody, dynamicVariables, userId, callbacksRef]);
|
|
234
|
-
|
|
235
|
-
// Create setClientTools function that only updates ref
|
|
236
|
-
const setClientTools = React.useCallback((tools: ClientToolsConfig['clientTools']) => {
|
|
237
|
-
clientToolsRef.current = tools;
|
|
238
|
-
}, []);
|
|
239
|
-
|
|
240
|
-
// Memoize inline callback functions
|
|
241
|
-
const sendContextualUpdate = React.useCallback((text: string) => {
|
|
242
|
-
sendMessage({
|
|
243
|
-
type: "contextual_update",
|
|
244
|
-
text,
|
|
245
|
-
});
|
|
246
|
-
}, [sendMessage]);
|
|
247
|
-
|
|
248
|
-
const sendUserMessage = React.useCallback((text: string) => {
|
|
249
|
-
sendMessage({
|
|
250
|
-
type: "user_message",
|
|
251
|
-
text,
|
|
252
|
-
});
|
|
253
|
-
}, [sendMessage]);
|
|
254
|
-
|
|
255
|
-
const sendUserActivity = React.useCallback(() => {
|
|
256
|
-
sendMessage({
|
|
257
|
-
type: "user_activity",
|
|
258
|
-
});
|
|
259
|
-
}, [sendMessage]);
|
|
260
|
-
|
|
261
|
-
const sendMultimodalMessage = React.useCallback((options: { text?: string; fileId?: string }) => {
|
|
262
|
-
sendMessage({
|
|
263
|
-
type: "multimodal_message",
|
|
264
|
-
text: options.text
|
|
265
|
-
? { type: "user_message" as const, text: options.text }
|
|
266
|
-
: undefined,
|
|
267
|
-
file: options.fileId
|
|
268
|
-
? { type: "file_input" as const, file_id: options.fileId }
|
|
269
|
-
: undefined,
|
|
270
|
-
});
|
|
271
|
-
}, [sendMessage]);
|
|
272
|
-
|
|
273
|
-
// Store all conversation values/functions in refs for stable access
|
|
274
|
-
const conversationValuesRef = React.useRef({
|
|
275
|
-
startSession,
|
|
276
|
-
endSession,
|
|
277
|
-
status,
|
|
278
|
-
isSpeaking,
|
|
279
|
-
canSendFeedback,
|
|
280
|
-
getId,
|
|
281
|
-
setMicMuted,
|
|
282
|
-
sendFeedback,
|
|
283
|
-
sendContextualUpdate,
|
|
284
|
-
sendUserMessage,
|
|
285
|
-
sendUserActivity,
|
|
286
|
-
sendMultimodalMessage,
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// Update ref on every render
|
|
290
|
-
conversationValuesRef.current = {
|
|
291
|
-
startSession,
|
|
292
|
-
endSession,
|
|
293
|
-
status,
|
|
294
|
-
isSpeaking,
|
|
295
|
-
canSendFeedback,
|
|
296
|
-
getId,
|
|
297
|
-
setMicMuted,
|
|
298
|
-
sendFeedback,
|
|
299
|
-
sendContextualUpdate,
|
|
300
|
-
sendUserMessage,
|
|
301
|
-
sendUserActivity,
|
|
302
|
-
sendMultimodalMessage,
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
// Create a stable conversation object that never changes reference
|
|
306
|
-
// This prevents infinite loops when conversation is used in useEffect dependencies
|
|
307
|
-
const conversation = React.useMemo<Conversation>(() => ({
|
|
308
|
-
get startSession() { return conversationValuesRef.current.startSession; },
|
|
309
|
-
get endSession() { return conversationValuesRef.current.endSession; },
|
|
310
|
-
get status() { return conversationValuesRef.current.status; },
|
|
311
|
-
get isSpeaking() { return conversationValuesRef.current.isSpeaking; },
|
|
312
|
-
get canSendFeedback() { return conversationValuesRef.current.canSendFeedback; },
|
|
313
|
-
get getId() { return conversationValuesRef.current.getId; },
|
|
314
|
-
get setMicMuted() { return conversationValuesRef.current.setMicMuted; },
|
|
315
|
-
get sendFeedback() { return conversationValuesRef.current.sendFeedback; },
|
|
316
|
-
get sendContextualUpdate() { return conversationValuesRef.current.sendContextualUpdate; },
|
|
317
|
-
get sendUserMessage() { return conversationValuesRef.current.sendUserMessage; },
|
|
318
|
-
get sendUserActivity() { return conversationValuesRef.current.sendUserActivity; },
|
|
319
|
-
get sendMultimodalMessage() { return conversationValuesRef.current.sendMultimodalMessage; },
|
|
320
|
-
}), []); // Empty deps - object is created once and never recreated
|
|
321
|
-
|
|
322
|
-
// Memoize the context value to prevent unnecessary re-renders of consumers
|
|
323
|
-
const contextValue = React.useMemo<ElevenLabsContextType>(() => ({
|
|
324
|
-
conversation,
|
|
325
|
-
callbacksRef,
|
|
326
|
-
serverUrl,
|
|
327
|
-
tokenFetchUrl,
|
|
328
|
-
clientTools: clientToolsRef.current,
|
|
329
|
-
setCallbacks,
|
|
330
|
-
setServerUrl,
|
|
331
|
-
setTokenFetchUrl,
|
|
332
|
-
setClientTools,
|
|
333
|
-
}), [
|
|
334
|
-
conversation,
|
|
335
|
-
callbacksRef,
|
|
336
|
-
serverUrl,
|
|
337
|
-
tokenFetchUrl,
|
|
338
|
-
setCallbacks,
|
|
339
|
-
setClientTools,
|
|
340
|
-
]);
|
|
341
|
-
|
|
342
|
-
return (
|
|
343
|
-
<ElevenLabsContext.Provider value={contextValue}>
|
|
344
|
-
<LiveKitRoomWrapper
|
|
345
|
-
serverUrl={serverUrl}
|
|
346
|
-
token={token}
|
|
347
|
-
connect={connect}
|
|
348
|
-
onConnected={handleConnectedWithFeedback}
|
|
349
|
-
onDisconnected={handleDisconnectedWithFeedback}
|
|
350
|
-
onError={handleError}
|
|
351
|
-
roomConnected={roomConnected}
|
|
352
|
-
callbacks={callbacksRef.current}
|
|
353
|
-
onParticipantReady={handleParticipantReadyWithOverrides}
|
|
354
|
-
sendMessage={sendMessage}
|
|
355
|
-
clientTools={clientToolsRef.current}
|
|
356
|
-
updateCurrentEventId={updateCurrentEventId}
|
|
357
|
-
onEndSession={endSession}
|
|
358
|
-
audioSessionConfig={audioSessionConfig}
|
|
359
|
-
textOnly={textOnly}
|
|
360
|
-
>
|
|
361
|
-
{children}
|
|
362
|
-
</LiveKitRoomWrapper>
|
|
363
|
-
</ElevenLabsContext.Provider>
|
|
364
|
-
);
|
|
365
|
-
};
|
|
@@ -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
|
-
};
|