Skip to content

Commit 3643ee8

Browse files
tharindu20118claude
andcommitted
feat: voice calls via LiveKit (ChattyVoiceCallView)
Adds a ready-to-render call screen — token fetch from the same /api/widget/voice/token endpoint the web widget uses, LiveKit Room connect, mic enable, live transcript (RoomEvent.TranscriptionReceived), active-speaker-driven status (connecting/listening/agent-speaking), mute/hangup, and per-design theming via CHATTY_DESIGN_TOKENS — same states and structure as the web widget's voice-call-widget.tsx. Opt-in, not wired into ChattyChatView automatically: render it yourself from the existing onVoiceCallPress callback (see README). livekit-client/@livekit/react-native/@livekit/react-native-webrtc are optional peer dependencies — apps that never use voice don't pay for them. iOS calls setupIOSAudioManagement(true) on mount, needed for the mic/speaker routing a call requires (plain playback doesn't need it). Verified: tsc --noEmit clean and `npm run build` succeeds against the real installed livekit-client 2.22.1 / @livekit/react-native 2.12.0 type definitions, and the existing jest suite (33 tests) still passes. NOT verified: an actual call on a real device/simulator — written without any way to run React Native, LiveKit, or a device/simulator in this environment. Build and test a real call end-to-end (mic permission grant, actual audio, transcript rendering) before bumping the package version or publishing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 465a354 commit 3643ee8

11 files changed

Lines changed: 834 additions & 6 deletions

‎README.md‎

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ npm install @personaliai/react-native @react-native-async-storage/async-storage
3737
```
3838

3939
`react` and `react-native` are peer dependencies — install versions matching your app (`react`
40-
18+, `react-native` 0.72+).
40+
18+, `react-native` 0.72+). Voice calls (`ChattyVoiceCallView`) are the one feature that needs
41+
native linking — see [Voice-call button](#chattychatview) — everything else needs nothing beyond
42+
`@react-native-async-storage/async-storage`.
4143

4244
## Quick start
4345

@@ -210,9 +212,39 @@ yet, it's backend work in `chatty-backend`.
210212

211213
<br>
212214

213-
Only shown when the bot's dashboard has voice enabled, and only fires `onVoiceCallPress` — this
214-
SDK doesn't bundle a voice-call implementation (a separate LiveKit integration, out of scope
215-
here).
215+
Only shown when the bot's dashboard has voice enabled, and fires `onVoiceCallPress`. This SDK now
216+
ships a ready-to-render call screen, `ChattyVoiceCallView` — render it yourself from that callback
217+
(it's opt-in, not wired in automatically, so apps that never use voice don't pay for the
218+
dependency):
219+
220+
```tsx
221+
import { ChattyChatView, ChattyVoiceCallView, ChattyClient } from "@personaliai/react-native";
222+
import { registerGlobals } from "@livekit/react-native"; // once, at app startup (e.g. index.js)
223+
224+
registerGlobals();
225+
226+
function MyChat() {
227+
const [showCall, setShowCall] = useState(false);
228+
const client = useMemo(() => new ChattyClient({ botId: "YOUR_BOT_ID" }), []);
229+
230+
if (showCall) {
231+
return (
232+
<ChattyVoiceCallView
233+
client={client}
234+
sessionId={sessionId} // the same session id ChattyChatView/useChattyChat is using
235+
widgetStyle={theme?.widget_style}
236+
onClose={() => setShowCall(false)}
237+
/>
238+
);
239+
}
240+
return <ChattyChatView botId="YOUR_BOT_ID" onVoiceCallPress={() => setShowCall(true)} />;
241+
}
242+
```
243+
244+
Needs three additional peer dependencies only if you use this (`npm i @livekit/react-native
245+
livekit-client @livekit/react-native-webrtc` + the usual iOS `pod install` / Android autolinking) —
246+
apps that don't render `ChattyVoiceCallView` don't need them. See LiveKit's own React Native setup
247+
docs for platform permissions (`NSMicrophoneUsageDescription` on iOS, `RECORD_AUDIO` on Android).
216248

217249
</details>
218250

‎dist/ChattyVoiceCallView.d.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from "react";
2+
import { ChattyClient } from "./api";
3+
export interface ChattyVoiceCallViewProps {
4+
client: ChattyClient;
5+
sessionId: string;
6+
widgetStyle?: string | null;
7+
visitorTimezone?: string;
8+
onClose: () => void;
9+
}
10+
/**
11+
* Full-screen (or modal) voice-call UI — present this when the chat view's
12+
* phone button is tapped and `theme.voice_enabled` is true, e.g.:
13+
*
14+
* {showCall && (
15+
* <ChattyVoiceCallView client={client} sessionId={sessionId}
16+
* widgetStyle={theme?.widget_style} onClose={() => setShowCall(false)} />
17+
* )}
18+
*
19+
* Requires (app-level, once): `import { registerGlobals } from
20+
* "@livekit/react-native"; registerGlobals();` before any Room is created —
21+
* typically at the top of index.js. iOS also needs an AudioSession category
22+
* configured (see @livekit/react-native's AudioSession helper) for the mic
23+
* to route correctly during a call.
24+
*/
25+
export declare function ChattyVoiceCallView({ client, sessionId, widgetStyle, visitorTimezone, onClose }: ChattyVoiceCallViewProps): React.JSX.Element;

‎dist/ChattyVoiceCallView.js‎

Lines changed: 316 additions & 0 deletions
Large diffs are not rendered by default.

‎dist/api.d.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ export declare class ChattyClient {
7575
}): Promise<string>;
7676
/** Poll for new messages (e.g. from a human agent) since the given ISO timestamp. */
7777
poll(sessionId: string, after: string): Promise<ChattyPollResponse>;
78+
/**
79+
* Mints a LiveKit room token + dispatches the voice agent for a call, same
80+
* endpoint the web widget uses. Only meaningful when `getTheme().voice_enabled`
81+
* is true. This SDK doesn't bundle the LiveKit client itself — see
82+
* ChattyVoiceCallView, which needs `@livekit/react-native` + `livekit-client`
83+
* as peer dependencies only apps actually using voice calls need to install.
84+
*/
85+
getVoiceToken(sessionId: string, visitorTimezone?: string): Promise<ChattyVoiceToken>;
86+
}
87+
export interface ChattyVoiceToken {
88+
token: string;
89+
livekit_url: string;
90+
room_name: string;
91+
session_id: string;
7892
}
7993
export declare class ChattyRateLimitError extends Error {
8094
constructor();

‎dist/api.js‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,29 @@ export class ChattyClient {
158158
throw new Error(`poll failed: ${res.status}`);
159159
return res.json();
160160
}
161+
/**
162+
* Mints a LiveKit room token + dispatches the voice agent for a call, same
163+
* endpoint the web widget uses. Only meaningful when `getTheme().voice_enabled`
164+
* is true. This SDK doesn't bundle the LiveKit client itself — see
165+
* ChattyVoiceCallView, which needs `@livekit/react-native` + `livekit-client`
166+
* as peer dependencies only apps actually using voice calls need to install.
167+
*/
168+
async getVoiceToken(sessionId, visitorTimezone = "UTC") {
169+
const res = await fetch(buildUrl(this.baseUrl, "/api/widget/voice/token", {}), {
170+
method: "POST",
171+
headers: { "Content-Type": "application/json" },
172+
body: JSON.stringify({ bot_id: this.botId, session_id: sessionId, visitor_timezone: visitorTimezone }),
173+
});
174+
if (res.status === 429)
175+
throw new ChattyRateLimitError();
176+
if (res.status === 403)
177+
throw new ChattyDomainNotAllowedError();
178+
if (!res.ok) {
179+
const detail = await res.json().catch(() => ({}));
180+
throw new Error(detail.detail || `getVoiceToken failed: ${res.status}`);
181+
}
182+
return res.json();
183+
}
161184
}
162185
// The backend can't cryptographically verify a native app's identity the way
163186
// it verifies a browser's Referer for the web widget, so a bot with

‎dist/index.d.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
export { ChattyClient, DEFAULT_BASE_URL, ChattyRateLimitError, ChattyDomainNotAllowedError } from "./api";
2-
export type { ChattyTheme, ChattyChatResponse, ChattyMediaResponse, ChattyPollMessage, ChattyPollResponse, ChattyClientOptions, } from "./api";
2+
export type { ChattyTheme, ChattyChatResponse, ChattyMediaResponse, ChattyPollMessage, ChattyPollResponse, ChattyClientOptions, ChattyVoiceToken, } from "./api";
33
export { useChattyChat } from "./useChattyChat";
44
export type { ChattyMessage, ChattyRole, UseChattyChatOptions, UseChattyChatResult } from "./useChattyChat";
55
export { ChattyChatView } from "./ChattyChatView";
66
export type { ChattyChatViewProps } from "./ChattyChatView";
77
export { ChattyLauncher } from "./ChattyLauncher";
88
export type { ChattyLauncherProps } from "./ChattyLauncher";
9+
export { ChattyVoiceCallView } from "./ChattyVoiceCallView";
10+
export type { ChattyVoiceCallViewProps } from "./ChattyVoiceCallView";
911
export { getOrCreateSessionId, newSession } from "./session";
1012
export { CHATTY_DESIGN_TOKENS, chattyNormalizeWidgetStyle, chattyLogoBgColor, chattyLauncherRadii, chattyBubbleRadii } from "./designTokens";
1113
export type { ChattyDesignTokens } from "./designTokens";

‎dist/index.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,9 @@ export { ChattyClient, DEFAULT_BASE_URL, ChattyRateLimitError, ChattyDomainNotAl
22
export { useChattyChat } from "./useChattyChat";
33
export { ChattyChatView } from "./ChattyChatView";
44
export { ChattyLauncher } from "./ChattyLauncher";
5+
// Voice calls (LiveKit) — only importable once the app has installed the
6+
// optional peer dependencies (@livekit/react-native, livekit-client,
7+
// @livekit/react-native-webrtc); see ChattyVoiceCallView.tsx's own comment.
8+
export { ChattyVoiceCallView } from "./ChattyVoiceCallView";
59
export { getOrCreateSessionId, newSession } from "./session";
610
export { CHATTY_DESIGN_TOKENS, chattyNormalizeWidgetStyle, chattyLogoBgColor, chattyLauncherRadii, chattyBubbleRadii } from "./designTokens";

‎package.json‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,25 @@
3535
"peerDependencies": {
3636
"@react-native-async-storage/async-storage": ">=1.19",
3737
"react": ">=18",
38-
"react-native": ">=0.72"
38+
"react-native": ">=0.72",
39+
"livekit-client": ">=2.0.0",
40+
"@livekit/react-native": ">=2.0.0",
41+
"@livekit/react-native-webrtc": ">=125.0.0"
42+
},
43+
"peerDependenciesMeta": {
44+
"livekit-client": { "optional": true },
45+
"@livekit/react-native": { "optional": true },
46+
"@livekit/react-native-webrtc": { "optional": true }
3947
},
4048
"devDependencies": {
4149
"@react-native-async-storage/async-storage": "^1.24.0",
4250
"@types/jest": "^30.0.0",
4351
"@types/react": "^18.3.31",
4452
"@types/react-test-renderer": "^18.3.1",
4553
"jest": "^30.4.2",
54+
"livekit-client": "^2.22.1",
55+
"@livekit/react-native": "^2.12.0",
56+
"@livekit/react-native-webrtc": "^144.1.2",
4657
"react": "^18.2.0",
4758
"react-native": "^0.74.7",
4859
"react-test-renderer": "18.2.0",

0 commit comments

Comments
 (0)