@thunderphone/widget 0.3.3 → 0.4.1
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/README.md +38 -0
- package/dist/index.d.mts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +78 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +79 -3
- package/dist/index.mjs.map +1 -1
- package/dist/mount.global.js +82 -6
- package/dist/mount.global.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ function App() {
|
|
|
42
42
|
| `onDisconnect` | `() => void` | No | Called when the session ends |
|
|
43
43
|
| `onError` | `(error) => void` | No | Called on errors. Error has `error` (code) and `message` fields |
|
|
44
44
|
| `className` | `string` | No | Additional CSS class for the widget container |
|
|
45
|
+
| `ringtone` | `boolean \| string` | No | Play a ringtone while connecting. `true` for default, or a URL for custom audio. Disabled by default |
|
|
45
46
|
|
|
46
47
|
## Headless Hook
|
|
47
48
|
|
|
@@ -91,6 +92,7 @@ function CustomCallButton() {
|
|
|
91
92
|
| `onConnect` | `() => void` | No | Called when the voice session connects |
|
|
92
93
|
| `onDisconnect` | `() => void` | No | Called when the session ends |
|
|
93
94
|
| `onError` | `(error) => void` | No | Called on errors |
|
|
95
|
+
| `ringtone` | `boolean \| string` | No | Play a ringtone while connecting. `true` for default, or a URL for custom audio |
|
|
94
96
|
|
|
95
97
|
### Hook Return Value
|
|
96
98
|
|
|
@@ -105,6 +107,42 @@ function CustomCallButton() {
|
|
|
105
107
|
| `agentName` | `string \| undefined` | Name of the connected agent |
|
|
106
108
|
| `audio` | `ReactNode` | Invisible element — must be rendered in the tree |
|
|
107
109
|
|
|
110
|
+
## Ringtone
|
|
111
|
+
|
|
112
|
+
Play a ringing sound while the widget connects, to simulate a phone call:
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
<ThunderPhoneWidget
|
|
116
|
+
apiKey="pk_live_your_publishable_key"
|
|
117
|
+
agentId={123}
|
|
118
|
+
ringtone={true}
|
|
119
|
+
/>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use a custom audio file by passing a URL:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<ThunderPhoneWidget
|
|
126
|
+
apiKey="pk_live_your_publishable_key"
|
|
127
|
+
agentId={123}
|
|
128
|
+
ringtone="https://example.com/my-ringtone.mp3"
|
|
129
|
+
/>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The ringtone loops during the `connecting` state and fades out when the agent connects. It is opt-in and disabled by default.
|
|
133
|
+
|
|
134
|
+
The headless hook accepts the same option:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
const phone = useThunderPhone({ apiKey, agentId, ringtone: true })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
And the script-tag mount API:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
ThunderPhone.mount({ element: '#thunderphone', apiKey: '...', agentId: 123, ringtone: true })
|
|
144
|
+
```
|
|
145
|
+
|
|
108
146
|
## Styling
|
|
109
147
|
|
|
110
148
|
The widget uses plain CSS with `tp-` prefixed classes, so it won't conflict with your styles. You can override any of these classes:
|
package/dist/index.d.mts
CHANGED
|
@@ -14,9 +14,16 @@ interface ThunderPhoneWidgetProps {
|
|
|
14
14
|
onDisconnect?: () => void;
|
|
15
15
|
onError?: (error: WidgetError) => void;
|
|
16
16
|
className?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Play a ringtone while connecting. Opt-in — disabled by default.
|
|
19
|
+
* - `true` plays the default ringtone from the ThunderPhone CDN.
|
|
20
|
+
* - A string URL plays a custom audio file.
|
|
21
|
+
* - `false` or omitted disables the ringtone.
|
|
22
|
+
*/
|
|
23
|
+
ringtone?: boolean | string;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
declare function ThunderPhoneWidget({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, className, }: ThunderPhoneWidgetProps): react_jsx_runtime.JSX.Element;
|
|
26
|
+
declare function ThunderPhoneWidget({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, className, ringtone, }: ThunderPhoneWidgetProps): react_jsx_runtime.JSX.Element;
|
|
20
27
|
|
|
21
28
|
interface UseThunderPhoneOptions {
|
|
22
29
|
apiKey: string;
|
|
@@ -28,6 +35,13 @@ interface UseThunderPhoneOptions {
|
|
|
28
35
|
error: string;
|
|
29
36
|
message: string;
|
|
30
37
|
}) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Play a ringtone while connecting. Opt-in — disabled by default.
|
|
40
|
+
* - `true` plays the default ringtone from the ThunderPhone CDN.
|
|
41
|
+
* - A string URL plays a custom audio file.
|
|
42
|
+
* - `false` or omitted disables the ringtone.
|
|
43
|
+
*/
|
|
44
|
+
ringtone?: boolean | string;
|
|
31
45
|
}
|
|
32
46
|
interface UseThunderPhoneReturn {
|
|
33
47
|
state: WidgetState;
|
package/dist/index.d.ts
CHANGED
|
@@ -14,9 +14,16 @@ interface ThunderPhoneWidgetProps {
|
|
|
14
14
|
onDisconnect?: () => void;
|
|
15
15
|
onError?: (error: WidgetError) => void;
|
|
16
16
|
className?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Play a ringtone while connecting. Opt-in — disabled by default.
|
|
19
|
+
* - `true` plays the default ringtone from the ThunderPhone CDN.
|
|
20
|
+
* - A string URL plays a custom audio file.
|
|
21
|
+
* - `false` or omitted disables the ringtone.
|
|
22
|
+
*/
|
|
23
|
+
ringtone?: boolean | string;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
declare function ThunderPhoneWidget({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, className, }: ThunderPhoneWidgetProps): react_jsx_runtime.JSX.Element;
|
|
26
|
+
declare function ThunderPhoneWidget({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, className, ringtone, }: ThunderPhoneWidgetProps): react_jsx_runtime.JSX.Element;
|
|
20
27
|
|
|
21
28
|
interface UseThunderPhoneOptions {
|
|
22
29
|
apiKey: string;
|
|
@@ -28,6 +35,13 @@ interface UseThunderPhoneOptions {
|
|
|
28
35
|
error: string;
|
|
29
36
|
message: string;
|
|
30
37
|
}) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Play a ringtone while connecting. Opt-in — disabled by default.
|
|
40
|
+
* - `true` plays the default ringtone from the ThunderPhone CDN.
|
|
41
|
+
* - A string URL plays a custom audio file.
|
|
42
|
+
* - `false` or omitted disables the ringtone.
|
|
43
|
+
*/
|
|
44
|
+
ringtone?: boolean | string;
|
|
31
45
|
}
|
|
32
46
|
interface UseThunderPhoneReturn {
|
|
33
47
|
state: WidgetState;
|
package/dist/index.js
CHANGED
|
@@ -181,11 +181,68 @@ async function createWidgetSession(apiKey, agentId, apiBase) {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
// src/useThunderPhone.ts
|
|
184
|
+
var DEFAULT_RINGTONE_URL = "https://storage.googleapis.com/thunderphone-widget-cdn/widget/assets/ringtone-default.mp3";
|
|
185
|
+
function resolveRingtoneUrl(ringtone) {
|
|
186
|
+
if (ringtone === true || ringtone === "default") return DEFAULT_RINGTONE_URL;
|
|
187
|
+
if (typeof ringtone === "string" && ringtone.length > 0) return ringtone;
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
function fadeOutAndStop(audio) {
|
|
191
|
+
if (audio.paused) return;
|
|
192
|
+
const fadeInterval = setInterval(() => {
|
|
193
|
+
const next = audio.volume - 0.1;
|
|
194
|
+
if (next <= 0) {
|
|
195
|
+
clearInterval(fadeInterval);
|
|
196
|
+
audio.pause();
|
|
197
|
+
audio.currentTime = 0;
|
|
198
|
+
audio.volume = 1;
|
|
199
|
+
} else {
|
|
200
|
+
audio.volume = next;
|
|
201
|
+
}
|
|
202
|
+
}, 20);
|
|
203
|
+
return fadeInterval;
|
|
204
|
+
}
|
|
184
205
|
function useThunderPhone(opts) {
|
|
185
206
|
const [state, setState] = (0, import_react3.useState)("idle");
|
|
186
207
|
const [session, setSession] = (0, import_react3.useState)(null);
|
|
187
208
|
const [muted, setMuted] = (0, import_react3.useState)(false);
|
|
188
209
|
const [error, setError] = (0, import_react3.useState)();
|
|
210
|
+
const ringtoneUrl = resolveRingtoneUrl(opts.ringtone);
|
|
211
|
+
const ringtoneRef = (0, import_react3.useRef)(null);
|
|
212
|
+
const fadeRef = (0, import_react3.useRef)(void 0);
|
|
213
|
+
(0, import_react3.useEffect)(() => {
|
|
214
|
+
if (!ringtoneUrl) {
|
|
215
|
+
ringtoneRef.current = null;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const audio2 = new Audio();
|
|
219
|
+
audio2.crossOrigin = "anonymous";
|
|
220
|
+
audio2.loop = true;
|
|
221
|
+
audio2.preload = "auto";
|
|
222
|
+
audio2.volume = 1;
|
|
223
|
+
audio2.src = ringtoneUrl;
|
|
224
|
+
ringtoneRef.current = audio2;
|
|
225
|
+
return () => {
|
|
226
|
+
audio2.pause();
|
|
227
|
+
audio2.src = "";
|
|
228
|
+
ringtoneRef.current = null;
|
|
229
|
+
};
|
|
230
|
+
}, [ringtoneUrl]);
|
|
231
|
+
(0, import_react3.useEffect)(() => {
|
|
232
|
+
if (state !== "connecting") {
|
|
233
|
+
const audio2 = ringtoneRef.current;
|
|
234
|
+
if (audio2 && !audio2.paused) {
|
|
235
|
+
if (fadeRef.current) clearInterval(fadeRef.current);
|
|
236
|
+
fadeRef.current = fadeOutAndStop(audio2);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return () => {
|
|
240
|
+
if (fadeRef.current) {
|
|
241
|
+
clearInterval(fadeRef.current);
|
|
242
|
+
fadeRef.current = void 0;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}, [state]);
|
|
189
246
|
const handleDisconnect = (0, import_react3.useCallback)(() => {
|
|
190
247
|
setState("disconnected");
|
|
191
248
|
setSession(null);
|
|
@@ -201,6 +258,24 @@ function useThunderPhone(opts) {
|
|
|
201
258
|
if (state === "connecting" || state === "connected") return;
|
|
202
259
|
setState("connecting");
|
|
203
260
|
setError(void 0);
|
|
261
|
+
const ringtoneAudio = ringtoneRef.current;
|
|
262
|
+
if (ringtoneAudio) {
|
|
263
|
+
if (fadeRef.current) {
|
|
264
|
+
clearInterval(fadeRef.current);
|
|
265
|
+
fadeRef.current = void 0;
|
|
266
|
+
}
|
|
267
|
+
ringtoneAudio.currentTime = 0;
|
|
268
|
+
ringtoneAudio.volume = 1;
|
|
269
|
+
ringtoneAudio.play().catch(() => {
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
navigator.mediaDevices.getUserMedia({ audio: true }).then(
|
|
273
|
+
(stream) => {
|
|
274
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
275
|
+
},
|
|
276
|
+
() => {
|
|
277
|
+
}
|
|
278
|
+
);
|
|
204
279
|
try {
|
|
205
280
|
const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase);
|
|
206
281
|
setSession(sess);
|
|
@@ -254,9 +329,10 @@ function ThunderPhoneWidget({
|
|
|
254
329
|
onConnect,
|
|
255
330
|
onDisconnect,
|
|
256
331
|
onError,
|
|
257
|
-
className
|
|
332
|
+
className,
|
|
333
|
+
ringtone
|
|
258
334
|
}) {
|
|
259
|
-
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError });
|
|
335
|
+
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, ringtone });
|
|
260
336
|
const handleClick = () => {
|
|
261
337
|
if (phone.state === "connected") {
|
|
262
338
|
phone.disconnect();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/WidgetButton.tsx","../src/WidgetStatus.tsx","../src/useThunderPhone.ts","../src/AudioHandler.tsx","../src/api.ts","../src/ThunderPhoneWidget.tsx"],"sourcesContent":["import './style.css'\n\nexport { ThunderPhoneWidget } from './ThunderPhoneWidget'\nexport { useThunderPhone } from './useThunderPhone'\nexport type { UseThunderPhoneOptions, UseThunderPhoneReturn } from './useThunderPhone'\nexport type { ThunderPhoneWidgetProps, WidgetError, WidgetState } from './types'\n","import type { WidgetState } from './types'\n\ninterface WidgetButtonProps {\n state: WidgetState\n muted: boolean\n onClick: () => void\n onMuteToggle: () => void\n}\n\nexport function WidgetButton({ state, muted, onClick, onMuteToggle }: WidgetButtonProps) {\n if (state === 'connected') {\n return (\n <div className=\"tp-button-group\">\n <button className=\"tp-button tp-button--mute\" onClick={onMuteToggle} type=\"button\">\n {muted ? (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\" />\n <path d=\"M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6\" />\n <path d=\"M17 16.95A7 7 0 0 1 5 12v-2m14 0v2c0 .76-.13 1.49-.36 2.18\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n <button className=\"tp-button tp-button--end\" onClick={onClick} type=\"button\">\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" />\n </svg>\n </button>\n </div>\n )\n }\n\n return (\n <button\n className={`tp-button tp-button--start ${state === 'connecting' ? 'tp-button--loading' : ''}`}\n onClick={onClick}\n disabled={state === 'connecting'}\n type=\"button\"\n >\n {state === 'connecting' ? (\n <svg className=\"tp-icon tp-spin\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n )\n}\n","import { useEffect, useState } from 'react'\nimport type { WidgetState } from './types'\n\ninterface WidgetStatusProps {\n state: WidgetState\n agentName: string | null\n errorMessage?: string\n}\n\nexport function WidgetStatus({ state, agentName, errorMessage }: WidgetStatusProps) {\n const [elapsed, setElapsed] = useState(0)\n\n useEffect(() => {\n if (state !== 'connected') {\n setElapsed(0)\n return\n }\n const interval = setInterval(() => setElapsed(s => s + 1), 1000)\n return () => clearInterval(interval)\n }, [state])\n\n const formatTime = (seconds: number) => {\n const m = Math.floor(seconds / 60)\n const s = seconds % 60\n return `${m}:${s.toString().padStart(2, '0')}`\n }\n\n const statusText: Record<WidgetState, string> = {\n idle: 'Ready',\n connecting: 'Connecting...',\n connected: formatTime(elapsed),\n disconnected: 'Disconnected',\n error: 'Unable to connect',\n }\n\n return (\n <div className=\"tp-status\">\n {agentName && <div className=\"tp-status__name\">{agentName}</div>}\n <div className={`tp-status__text tp-status--${state}`}>\n {state === 'connected' && <span className=\"tp-status__dot\" />}\n {errorMessage && state === 'error' ? errorMessage : statusText[state]}\n </div>\n </div>\n )\n}\n","import { useCallback, useState, type ReactNode, createElement } from 'react'\nimport { LiveKitRoom } from '@livekit/components-react'\nimport { AudioHandler } from './AudioHandler'\nimport { createWidgetSession, WidgetAPIError } from './api'\nimport type { WidgetState, WidgetSessionResponse } from './types'\n\nexport interface UseThunderPhoneOptions {\n apiKey: string\n agentId: number\n apiBase?: string\n onConnect?: () => void\n onDisconnect?: () => void\n onError?: (error: { error: string; message: string }) => void\n}\n\nexport interface UseThunderPhoneReturn {\n state: WidgetState\n connect: () => void\n disconnect: () => void\n toggleMute: () => void\n isMuted: boolean\n error: string | undefined\n agentName: string | undefined\n /** Render this somewhere in your tree — it's invisible but handles audio. */\n audio: ReactNode\n}\n\nexport function useThunderPhone(opts: UseThunderPhoneOptions): UseThunderPhoneReturn {\n const [state, setState] = useState<WidgetState>('idle')\n const [session, setSession] = useState<WidgetSessionResponse | null>(null)\n const [muted, setMuted] = useState(false)\n const [error, setError] = useState<string | undefined>()\n\n const handleDisconnect = useCallback(() => {\n setState('disconnected')\n setSession(null)\n setMuted(false)\n opts.onDisconnect?.()\n setTimeout(() => setState('idle'), 1500)\n }, [opts.onDisconnect])\n\n const handleAgentConnected = useCallback(() => {\n setState('connected')\n opts.onConnect?.()\n }, [opts.onConnect])\n\n const connect = useCallback(async () => {\n if (state === 'connecting' || state === 'connected') return\n setState('connecting')\n setError(undefined)\n try {\n const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase)\n setSession(sess)\n } catch (err) {\n setState('error')\n if (err instanceof WidgetAPIError) {\n setError(err.message)\n opts.onError?.({ error: err.code, message: err.message })\n } else {\n setError('Unable to connect.')\n opts.onError?.({ error: 'unknown', message: 'Unable to connect.' })\n }\n }\n }, [opts.apiKey, opts.agentId, opts.apiBase, state, opts.onError])\n\n const disconnect = useCallback(() => {\n handleDisconnect()\n }, [handleDisconnect])\n\n const toggleMute = useCallback(() => setMuted(m => !m), [])\n\n const audio: ReactNode = session\n ? createElement(\n LiveKitRoom,\n {\n token: session.token,\n serverUrl: session.server_url,\n audio: !muted,\n video: false,\n connect: true,\n },\n createElement(AudioHandler, {\n onAgentConnected: handleAgentConnected,\n onDisconnected: handleDisconnect,\n }),\n )\n : null\n\n return {\n state,\n connect,\n disconnect,\n toggleMute,\n isMuted: muted,\n error,\n agentName: session?.agent_name,\n audio,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport { useRoomContext } from '@livekit/components-react'\nimport { RoomEvent, Track, type RemoteTrackPublication, type RemoteParticipant } from 'livekit-client'\n\ninterface AudioHandlerProps {\n onAgentConnected: () => void\n onDisconnected: () => void\n}\n\nexport function AudioHandler({ onAgentConnected, onDisconnected }: AudioHandlerProps) {\n const room = useRoomContext()\n const audioRef = useRef<HTMLAudioElement>(null)\n\n useEffect(() => {\n if (room.remoteParticipants.size > 0) {\n onAgentConnected()\n }\n\n const handleParticipantConnected = () => onAgentConnected()\n const handleDisconnect = () => onDisconnected()\n\n // The bot publishes multiple audio tracks (tts, bg noise, hold music).\n // Add each track to a single MediaStream so the browser mixes them.\n const attachTrack = (\n track: { kind: Track.Kind; mediaStreamTrack: MediaStreamTrack },\n _pub: RemoteTrackPublication,\n _participant: RemoteParticipant,\n ) => {\n if (track.kind === Track.Kind.Audio && audioRef.current) {\n const existing = audioRef.current.srcObject as MediaStream | null\n if (existing) {\n existing.addTrack(track.mediaStreamTrack)\n } else {\n audioRef.current.srcObject = new MediaStream([track.mediaStreamTrack])\n audioRef.current.play().catch(() => {})\n }\n }\n }\n\n room.on(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.on(RoomEvent.Disconnected, handleDisconnect)\n room.on(RoomEvent.TrackSubscribed, attachTrack)\n\n const initialTracks: MediaStreamTrack[] = []\n for (const participant of room.remoteParticipants.values()) {\n for (const pub of participant.trackPublications.values()) {\n if (pub.track && pub.isSubscribed && pub.track.kind === Track.Kind.Audio) {\n initialTracks.push(pub.track.mediaStreamTrack)\n }\n }\n }\n if (initialTracks.length > 0 && audioRef.current) {\n audioRef.current.srcObject = new MediaStream(initialTracks)\n audioRef.current.play().catch(() => {})\n }\n\n return () => {\n room.off(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.off(RoomEvent.Disconnected, handleDisconnect)\n room.off(RoomEvent.TrackSubscribed, attachTrack)\n }\n }, [room, onAgentConnected, onDisconnected])\n\n return <audio ref={audioRef} autoPlay />\n}\n","import type { WidgetSessionResponse, WidgetError } from './types'\n\nconst DEFAULT_API_BASE = 'https://api.thunderphone.com/v1'\n\nexport class WidgetAPIError extends Error {\n constructor(public code: string, message: string) {\n super(message)\n this.name = 'WidgetAPIError'\n }\n}\n\nexport async function createWidgetSession(\n apiKey: string,\n agentId: number,\n apiBase?: string,\n): Promise<WidgetSessionResponse> {\n const base = apiBase || DEFAULT_API_BASE\n const response = await fetch(`${base}/widget/session`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': apiKey,\n },\n body: JSON.stringify({ agent_id: agentId }),\n })\n\n if (!response.ok) {\n const data: WidgetError = await response.json().catch(() => ({\n error: 'unknown',\n message: 'Unable to connect.',\n }))\n throw new WidgetAPIError(data.error, data.message)\n }\n\n return response.json()\n}\n","import { WidgetButton } from './WidgetButton'\nimport { WidgetStatus } from './WidgetStatus'\nimport { useThunderPhone } from './useThunderPhone'\nimport type { ThunderPhoneWidgetProps } from './types'\n\nexport function ThunderPhoneWidget({\n apiKey,\n agentId,\n apiBase,\n onConnect,\n onDisconnect,\n onError,\n className,\n}: ThunderPhoneWidgetProps) {\n const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError })\n\n const handleClick = () => {\n if (phone.state === 'connected') {\n phone.disconnect()\n } else if (phone.state === 'idle' || phone.state === 'error' || phone.state === 'disconnected') {\n phone.connect()\n }\n }\n\n return (\n <div className={`tp-widget ${className || ''}`}>\n <WidgetStatus\n state={phone.state}\n agentName={phone.agentName || null}\n errorMessage={phone.error}\n />\n <WidgetButton\n state={phone.state}\n muted={phone.isMuted}\n onClick={handleClick}\n onMuteToggle={phone.toggleMute}\n />\n {phone.audio}\n </div>\n )\n}\n"],"mappings":"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","names":["import_jsx_runtime","import_react","import_components_react","import_react","import_jsx_runtime","import_jsx_runtime"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/WidgetButton.tsx","../src/WidgetStatus.tsx","../src/useThunderPhone.ts","../src/AudioHandler.tsx","../src/api.ts","../src/ThunderPhoneWidget.tsx"],"sourcesContent":["import './style.css'\n\nexport { ThunderPhoneWidget } from './ThunderPhoneWidget'\nexport { useThunderPhone } from './useThunderPhone'\nexport type { UseThunderPhoneOptions, UseThunderPhoneReturn } from './useThunderPhone'\nexport type { ThunderPhoneWidgetProps, WidgetError, WidgetState } from './types'\n","import type { WidgetState } from './types'\n\ninterface WidgetButtonProps {\n state: WidgetState\n muted: boolean\n onClick: () => void\n onMuteToggle: () => void\n}\n\nexport function WidgetButton({ state, muted, onClick, onMuteToggle }: WidgetButtonProps) {\n if (state === 'connected') {\n return (\n <div className=\"tp-button-group\">\n <button className=\"tp-button tp-button--mute\" onClick={onMuteToggle} type=\"button\">\n {muted ? (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\" />\n <path d=\"M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6\" />\n <path d=\"M17 16.95A7 7 0 0 1 5 12v-2m14 0v2c0 .76-.13 1.49-.36 2.18\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n <button className=\"tp-button tp-button--end\" onClick={onClick} type=\"button\">\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" />\n </svg>\n </button>\n </div>\n )\n }\n\n return (\n <button\n className={`tp-button tp-button--start ${state === 'connecting' ? 'tp-button--loading' : ''}`}\n onClick={onClick}\n disabled={state === 'connecting'}\n type=\"button\"\n >\n {state === 'connecting' ? (\n <svg className=\"tp-icon tp-spin\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n )\n}\n","import { useEffect, useState } from 'react'\nimport type { WidgetState } from './types'\n\ninterface WidgetStatusProps {\n state: WidgetState\n agentName: string | null\n errorMessage?: string\n}\n\nexport function WidgetStatus({ state, agentName, errorMessage }: WidgetStatusProps) {\n const [elapsed, setElapsed] = useState(0)\n\n useEffect(() => {\n if (state !== 'connected') {\n setElapsed(0)\n return\n }\n const interval = setInterval(() => setElapsed(s => s + 1), 1000)\n return () => clearInterval(interval)\n }, [state])\n\n const formatTime = (seconds: number) => {\n const m = Math.floor(seconds / 60)\n const s = seconds % 60\n return `${m}:${s.toString().padStart(2, '0')}`\n }\n\n const statusText: Record<WidgetState, string> = {\n idle: 'Ready',\n connecting: 'Connecting...',\n connected: formatTime(elapsed),\n disconnected: 'Disconnected',\n error: 'Unable to connect',\n }\n\n return (\n <div className=\"tp-status\">\n {agentName && <div className=\"tp-status__name\">{agentName}</div>}\n <div className={`tp-status__text tp-status--${state}`}>\n {state === 'connected' && <span className=\"tp-status__dot\" />}\n {errorMessage && state === 'error' ? errorMessage : statusText[state]}\n </div>\n </div>\n )\n}\n","import { useCallback, useEffect, useRef, useState, type ReactNode, createElement } from 'react'\nimport { LiveKitRoom } from '@livekit/components-react'\nimport { AudioHandler } from './AudioHandler'\nimport { createWidgetSession, WidgetAPIError } from './api'\nimport type { WidgetState, WidgetSessionResponse } from './types'\n\nconst DEFAULT_RINGTONE_URL = 'https://storage.googleapis.com/thunderphone-widget-cdn/widget/assets/ringtone-default.mp3'\n\nexport interface UseThunderPhoneOptions {\n apiKey: string\n agentId: number\n apiBase?: string\n onConnect?: () => void\n onDisconnect?: () => void\n onError?: (error: { error: string; message: string }) => void\n /**\n * Play a ringtone while connecting. Opt-in — disabled by default.\n * - `true` plays the default ringtone from the ThunderPhone CDN.\n * - A string URL plays a custom audio file.\n * - `false` or omitted disables the ringtone.\n */\n ringtone?: boolean | string\n}\n\nexport interface UseThunderPhoneReturn {\n state: WidgetState\n connect: () => void\n disconnect: () => void\n toggleMute: () => void\n isMuted: boolean\n error: string | undefined\n agentName: string | undefined\n /** Render this somewhere in your tree — it's invisible but handles audio. */\n audio: ReactNode\n}\n\n/** Resolve the ringtone option to a URL or null. */\nfunction resolveRingtoneUrl(ringtone: boolean | string | undefined): string | null {\n if (ringtone === true || ringtone === 'default') return DEFAULT_RINGTONE_URL\n if (typeof ringtone === 'string' && ringtone.length > 0) return ringtone\n return null\n}\n\n/** Fade out an audio element over ~200ms, then pause and reset it. */\nfunction fadeOutAndStop(audio: HTMLAudioElement) {\n if (audio.paused) return\n const fadeInterval = setInterval(() => {\n const next = audio.volume - 0.1\n if (next <= 0) {\n clearInterval(fadeInterval)\n audio.pause()\n audio.currentTime = 0\n audio.volume = 1.0\n } else {\n audio.volume = next\n }\n }, 20) // 10 steps × 20ms = 200ms fade\n // Return the interval ID so callers can cancel if needed.\n return fadeInterval\n}\n\nexport function useThunderPhone(opts: UseThunderPhoneOptions): UseThunderPhoneReturn {\n const [state, setState] = useState<WidgetState>('idle')\n const [session, setSession] = useState<WidgetSessionResponse | null>(null)\n const [muted, setMuted] = useState(false)\n const [error, setError] = useState<string | undefined>()\n\n // --- Ringtone management ---\n const ringtoneUrl = resolveRingtoneUrl(opts.ringtone)\n const ringtoneRef = useRef<HTMLAudioElement | null>(null)\n const fadeRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined)\n\n // Preload the ringtone audio element once when configured.\n useEffect(() => {\n if (!ringtoneUrl) {\n ringtoneRef.current = null\n return\n }\n const audio = new Audio()\n audio.crossOrigin = 'anonymous'\n audio.loop = true\n audio.preload = 'auto'\n audio.volume = 1.0\n audio.src = ringtoneUrl\n ringtoneRef.current = audio\n return () => {\n audio.pause()\n audio.src = ''\n ringtoneRef.current = null\n }\n }, [ringtoneUrl])\n\n // Stop ringtone when leaving the 'connecting' state.\n // Starting the ringtone is done synchronously inside connect() so it\n // executes within the user-gesture call stack (required by browsers).\n useEffect(() => {\n if (state !== 'connecting') {\n const audio = ringtoneRef.current\n if (audio && !audio.paused) {\n // Cancel any previous fade that's still running.\n if (fadeRef.current) clearInterval(fadeRef.current)\n fadeRef.current = fadeOutAndStop(audio)\n }\n }\n\n return () => {\n if (fadeRef.current) {\n clearInterval(fadeRef.current)\n fadeRef.current = undefined\n }\n }\n }, [state])\n\n const handleDisconnect = useCallback(() => {\n setState('disconnected')\n setSession(null)\n setMuted(false)\n opts.onDisconnect?.()\n setTimeout(() => setState('idle'), 1500)\n }, [opts.onDisconnect])\n\n const handleAgentConnected = useCallback(() => {\n setState('connected')\n opts.onConnect?.()\n }, [opts.onConnect])\n\n const connect = useCallback(async () => {\n if (state === 'connecting' || state === 'connected') return\n setState('connecting')\n setError(undefined)\n\n // Start ringtone immediately — this MUST happen synchronously within\n // the user-gesture (click) call stack or the browser will block it.\n const ringtoneAudio = ringtoneRef.current\n if (ringtoneAudio) {\n // Cancel any lingering fade from a previous attempt.\n if (fadeRef.current) {\n clearInterval(fadeRef.current)\n fadeRef.current = undefined\n }\n ringtoneAudio.currentTime = 0\n ringtoneAudio.volume = 1.0\n ringtoneAudio.play().catch(() => {})\n }\n\n // Warm up mic permission in the background so the browser prompt (if\n // needed) overlaps with the API call. This is fire-and-forget: if the\n // session request fails we simply discard the stream without the user\n // noticing an unnecessary permission prompt — getUserMedia only shows\n // the prompt once per origin, so subsequent calls are instant.\n navigator.mediaDevices.getUserMedia({ audio: true }).then(\n (stream) => { stream.getTracks().forEach((t) => t.stop()) },\n () => {},\n )\n\n try {\n const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase)\n setSession(sess)\n } catch (err) {\n setState('error')\n if (err instanceof WidgetAPIError) {\n setError(err.message)\n opts.onError?.({ error: err.code, message: err.message })\n } else {\n setError('Unable to connect.')\n opts.onError?.({ error: 'unknown', message: 'Unable to connect.' })\n }\n }\n }, [opts.apiKey, opts.agentId, opts.apiBase, state, opts.onError])\n\n const disconnect = useCallback(() => {\n handleDisconnect()\n }, [handleDisconnect])\n\n const toggleMute = useCallback(() => setMuted(m => !m), [])\n\n const audio: ReactNode = session\n ? createElement(\n LiveKitRoom,\n {\n token: session.token,\n serverUrl: session.server_url,\n audio: !muted,\n video: false,\n connect: true,\n },\n createElement(AudioHandler, {\n onAgentConnected: handleAgentConnected,\n onDisconnected: handleDisconnect,\n }),\n )\n : null\n\n return {\n state,\n connect,\n disconnect,\n toggleMute,\n isMuted: muted,\n error,\n agentName: session?.agent_name,\n audio,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport { useRoomContext } from '@livekit/components-react'\nimport { RoomEvent, Track, type RemoteTrackPublication, type RemoteParticipant } from 'livekit-client'\n\ninterface AudioHandlerProps {\n onAgentConnected: () => void\n onDisconnected: () => void\n}\n\nexport function AudioHandler({ onAgentConnected, onDisconnected }: AudioHandlerProps) {\n const room = useRoomContext()\n const audioRef = useRef<HTMLAudioElement>(null)\n\n useEffect(() => {\n if (room.remoteParticipants.size > 0) {\n onAgentConnected()\n }\n\n const handleParticipantConnected = () => onAgentConnected()\n const handleDisconnect = () => onDisconnected()\n\n // The bot publishes multiple audio tracks (tts, bg noise, hold music).\n // Add each track to a single MediaStream so the browser mixes them.\n const attachTrack = (\n track: { kind: Track.Kind; mediaStreamTrack: MediaStreamTrack },\n _pub: RemoteTrackPublication,\n _participant: RemoteParticipant,\n ) => {\n if (track.kind === Track.Kind.Audio && audioRef.current) {\n const existing = audioRef.current.srcObject as MediaStream | null\n if (existing) {\n existing.addTrack(track.mediaStreamTrack)\n } else {\n audioRef.current.srcObject = new MediaStream([track.mediaStreamTrack])\n audioRef.current.play().catch(() => {})\n }\n }\n }\n\n room.on(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.on(RoomEvent.Disconnected, handleDisconnect)\n room.on(RoomEvent.TrackSubscribed, attachTrack)\n\n const initialTracks: MediaStreamTrack[] = []\n for (const participant of room.remoteParticipants.values()) {\n for (const pub of participant.trackPublications.values()) {\n if (pub.track && pub.isSubscribed && pub.track.kind === Track.Kind.Audio) {\n initialTracks.push(pub.track.mediaStreamTrack)\n }\n }\n }\n if (initialTracks.length > 0 && audioRef.current) {\n audioRef.current.srcObject = new MediaStream(initialTracks)\n audioRef.current.play().catch(() => {})\n }\n\n return () => {\n room.off(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.off(RoomEvent.Disconnected, handleDisconnect)\n room.off(RoomEvent.TrackSubscribed, attachTrack)\n }\n }, [room, onAgentConnected, onDisconnected])\n\n return <audio ref={audioRef} autoPlay />\n}\n","import type { WidgetSessionResponse, WidgetError } from './types'\n\nconst DEFAULT_API_BASE = 'https://api.thunderphone.com/v1'\n\nexport class WidgetAPIError extends Error {\n constructor(public code: string, message: string) {\n super(message)\n this.name = 'WidgetAPIError'\n }\n}\n\nexport async function createWidgetSession(\n apiKey: string,\n agentId: number,\n apiBase?: string,\n): Promise<WidgetSessionResponse> {\n const base = apiBase || DEFAULT_API_BASE\n const response = await fetch(`${base}/widget/session`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': apiKey,\n },\n body: JSON.stringify({ agent_id: agentId }),\n })\n\n if (!response.ok) {\n const data: WidgetError = await response.json().catch(() => ({\n error: 'unknown',\n message: 'Unable to connect.',\n }))\n throw new WidgetAPIError(data.error, data.message)\n }\n\n return response.json()\n}\n","import { WidgetButton } from './WidgetButton'\nimport { WidgetStatus } from './WidgetStatus'\nimport { useThunderPhone } from './useThunderPhone'\nimport type { ThunderPhoneWidgetProps } from './types'\n\nexport function ThunderPhoneWidget({\n apiKey,\n agentId,\n apiBase,\n onConnect,\n onDisconnect,\n onError,\n className,\n ringtone,\n}: ThunderPhoneWidgetProps) {\n const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, ringtone })\n\n const handleClick = () => {\n if (phone.state === 'connected') {\n phone.disconnect()\n } else if (phone.state === 'idle' || phone.state === 'error' || phone.state === 'disconnected') {\n phone.connect()\n }\n }\n\n return (\n <div className={`tp-widget ${className || ''}`}>\n <WidgetStatus\n state={phone.state}\n agentName={phone.agentName || null}\n errorMessage={phone.error}\n />\n <WidgetButton\n state={phone.state}\n muted={phone.isMuted}\n onClick={handleClick}\n onMuteToggle={phone.toggleMute}\n />\n {phone.audio}\n </div>\n )\n}\n"],"mappings":"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","names":["import_jsx_runtime","import_react","import_components_react","import_react","import_jsx_runtime","audio","import_jsx_runtime"]}
|
package/dist/index.mjs
CHANGED
|
@@ -70,7 +70,7 @@ function WidgetStatus({ state, agentName, errorMessage }) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// src/useThunderPhone.ts
|
|
73
|
-
import { useCallback, useState as useState2, createElement } from "react";
|
|
73
|
+
import { useCallback, useEffect as useEffect3, useRef as useRef2, useState as useState2, createElement } from "react";
|
|
74
74
|
import { LiveKitRoom } from "@livekit/components-react";
|
|
75
75
|
|
|
76
76
|
// src/AudioHandler.tsx
|
|
@@ -154,11 +154,68 @@ async function createWidgetSession(apiKey, agentId, apiBase) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// src/useThunderPhone.ts
|
|
157
|
+
var DEFAULT_RINGTONE_URL = "https://storage.googleapis.com/thunderphone-widget-cdn/widget/assets/ringtone-default.mp3";
|
|
158
|
+
function resolveRingtoneUrl(ringtone) {
|
|
159
|
+
if (ringtone === true || ringtone === "default") return DEFAULT_RINGTONE_URL;
|
|
160
|
+
if (typeof ringtone === "string" && ringtone.length > 0) return ringtone;
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function fadeOutAndStop(audio) {
|
|
164
|
+
if (audio.paused) return;
|
|
165
|
+
const fadeInterval = setInterval(() => {
|
|
166
|
+
const next = audio.volume - 0.1;
|
|
167
|
+
if (next <= 0) {
|
|
168
|
+
clearInterval(fadeInterval);
|
|
169
|
+
audio.pause();
|
|
170
|
+
audio.currentTime = 0;
|
|
171
|
+
audio.volume = 1;
|
|
172
|
+
} else {
|
|
173
|
+
audio.volume = next;
|
|
174
|
+
}
|
|
175
|
+
}, 20);
|
|
176
|
+
return fadeInterval;
|
|
177
|
+
}
|
|
157
178
|
function useThunderPhone(opts) {
|
|
158
179
|
const [state, setState] = useState2("idle");
|
|
159
180
|
const [session, setSession] = useState2(null);
|
|
160
181
|
const [muted, setMuted] = useState2(false);
|
|
161
182
|
const [error, setError] = useState2();
|
|
183
|
+
const ringtoneUrl = resolveRingtoneUrl(opts.ringtone);
|
|
184
|
+
const ringtoneRef = useRef2(null);
|
|
185
|
+
const fadeRef = useRef2(void 0);
|
|
186
|
+
useEffect3(() => {
|
|
187
|
+
if (!ringtoneUrl) {
|
|
188
|
+
ringtoneRef.current = null;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const audio2 = new Audio();
|
|
192
|
+
audio2.crossOrigin = "anonymous";
|
|
193
|
+
audio2.loop = true;
|
|
194
|
+
audio2.preload = "auto";
|
|
195
|
+
audio2.volume = 1;
|
|
196
|
+
audio2.src = ringtoneUrl;
|
|
197
|
+
ringtoneRef.current = audio2;
|
|
198
|
+
return () => {
|
|
199
|
+
audio2.pause();
|
|
200
|
+
audio2.src = "";
|
|
201
|
+
ringtoneRef.current = null;
|
|
202
|
+
};
|
|
203
|
+
}, [ringtoneUrl]);
|
|
204
|
+
useEffect3(() => {
|
|
205
|
+
if (state !== "connecting") {
|
|
206
|
+
const audio2 = ringtoneRef.current;
|
|
207
|
+
if (audio2 && !audio2.paused) {
|
|
208
|
+
if (fadeRef.current) clearInterval(fadeRef.current);
|
|
209
|
+
fadeRef.current = fadeOutAndStop(audio2);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return () => {
|
|
213
|
+
if (fadeRef.current) {
|
|
214
|
+
clearInterval(fadeRef.current);
|
|
215
|
+
fadeRef.current = void 0;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}, [state]);
|
|
162
219
|
const handleDisconnect = useCallback(() => {
|
|
163
220
|
setState("disconnected");
|
|
164
221
|
setSession(null);
|
|
@@ -174,6 +231,24 @@ function useThunderPhone(opts) {
|
|
|
174
231
|
if (state === "connecting" || state === "connected") return;
|
|
175
232
|
setState("connecting");
|
|
176
233
|
setError(void 0);
|
|
234
|
+
const ringtoneAudio = ringtoneRef.current;
|
|
235
|
+
if (ringtoneAudio) {
|
|
236
|
+
if (fadeRef.current) {
|
|
237
|
+
clearInterval(fadeRef.current);
|
|
238
|
+
fadeRef.current = void 0;
|
|
239
|
+
}
|
|
240
|
+
ringtoneAudio.currentTime = 0;
|
|
241
|
+
ringtoneAudio.volume = 1;
|
|
242
|
+
ringtoneAudio.play().catch(() => {
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
navigator.mediaDevices.getUserMedia({ audio: true }).then(
|
|
246
|
+
(stream) => {
|
|
247
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
248
|
+
},
|
|
249
|
+
() => {
|
|
250
|
+
}
|
|
251
|
+
);
|
|
177
252
|
try {
|
|
178
253
|
const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase);
|
|
179
254
|
setSession(sess);
|
|
@@ -227,9 +302,10 @@ function ThunderPhoneWidget({
|
|
|
227
302
|
onConnect,
|
|
228
303
|
onDisconnect,
|
|
229
304
|
onError,
|
|
230
|
-
className
|
|
305
|
+
className,
|
|
306
|
+
ringtone
|
|
231
307
|
}) {
|
|
232
|
-
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError });
|
|
308
|
+
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, ringtone });
|
|
233
309
|
const handleClick = () => {
|
|
234
310
|
if (phone.state === "connected") {
|
|
235
311
|
phone.disconnect();
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/WidgetButton.tsx","../src/WidgetStatus.tsx","../src/useThunderPhone.ts","../src/AudioHandler.tsx","../src/api.ts","../src/ThunderPhoneWidget.tsx"],"sourcesContent":["import type { WidgetState } from './types'\n\ninterface WidgetButtonProps {\n state: WidgetState\n muted: boolean\n onClick: () => void\n onMuteToggle: () => void\n}\n\nexport function WidgetButton({ state, muted, onClick, onMuteToggle }: WidgetButtonProps) {\n if (state === 'connected') {\n return (\n <div className=\"tp-button-group\">\n <button className=\"tp-button tp-button--mute\" onClick={onMuteToggle} type=\"button\">\n {muted ? (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\" />\n <path d=\"M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6\" />\n <path d=\"M17 16.95A7 7 0 0 1 5 12v-2m14 0v2c0 .76-.13 1.49-.36 2.18\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n <button className=\"tp-button tp-button--end\" onClick={onClick} type=\"button\">\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" />\n </svg>\n </button>\n </div>\n )\n }\n\n return (\n <button\n className={`tp-button tp-button--start ${state === 'connecting' ? 'tp-button--loading' : ''}`}\n onClick={onClick}\n disabled={state === 'connecting'}\n type=\"button\"\n >\n {state === 'connecting' ? (\n <svg className=\"tp-icon tp-spin\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n )\n}\n","import { useEffect, useState } from 'react'\nimport type { WidgetState } from './types'\n\ninterface WidgetStatusProps {\n state: WidgetState\n agentName: string | null\n errorMessage?: string\n}\n\nexport function WidgetStatus({ state, agentName, errorMessage }: WidgetStatusProps) {\n const [elapsed, setElapsed] = useState(0)\n\n useEffect(() => {\n if (state !== 'connected') {\n setElapsed(0)\n return\n }\n const interval = setInterval(() => setElapsed(s => s + 1), 1000)\n return () => clearInterval(interval)\n }, [state])\n\n const formatTime = (seconds: number) => {\n const m = Math.floor(seconds / 60)\n const s = seconds % 60\n return `${m}:${s.toString().padStart(2, '0')}`\n }\n\n const statusText: Record<WidgetState, string> = {\n idle: 'Ready',\n connecting: 'Connecting...',\n connected: formatTime(elapsed),\n disconnected: 'Disconnected',\n error: 'Unable to connect',\n }\n\n return (\n <div className=\"tp-status\">\n {agentName && <div className=\"tp-status__name\">{agentName}</div>}\n <div className={`tp-status__text tp-status--${state}`}>\n {state === 'connected' && <span className=\"tp-status__dot\" />}\n {errorMessage && state === 'error' ? errorMessage : statusText[state]}\n </div>\n </div>\n )\n}\n","import { useCallback, useState, type ReactNode, createElement } from 'react'\nimport { LiveKitRoom } from '@livekit/components-react'\nimport { AudioHandler } from './AudioHandler'\nimport { createWidgetSession, WidgetAPIError } from './api'\nimport type { WidgetState, WidgetSessionResponse } from './types'\n\nexport interface UseThunderPhoneOptions {\n apiKey: string\n agentId: number\n apiBase?: string\n onConnect?: () => void\n onDisconnect?: () => void\n onError?: (error: { error: string; message: string }) => void\n}\n\nexport interface UseThunderPhoneReturn {\n state: WidgetState\n connect: () => void\n disconnect: () => void\n toggleMute: () => void\n isMuted: boolean\n error: string | undefined\n agentName: string | undefined\n /** Render this somewhere in your tree — it's invisible but handles audio. */\n audio: ReactNode\n}\n\nexport function useThunderPhone(opts: UseThunderPhoneOptions): UseThunderPhoneReturn {\n const [state, setState] = useState<WidgetState>('idle')\n const [session, setSession] = useState<WidgetSessionResponse | null>(null)\n const [muted, setMuted] = useState(false)\n const [error, setError] = useState<string | undefined>()\n\n const handleDisconnect = useCallback(() => {\n setState('disconnected')\n setSession(null)\n setMuted(false)\n opts.onDisconnect?.()\n setTimeout(() => setState('idle'), 1500)\n }, [opts.onDisconnect])\n\n const handleAgentConnected = useCallback(() => {\n setState('connected')\n opts.onConnect?.()\n }, [opts.onConnect])\n\n const connect = useCallback(async () => {\n if (state === 'connecting' || state === 'connected') return\n setState('connecting')\n setError(undefined)\n try {\n const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase)\n setSession(sess)\n } catch (err) {\n setState('error')\n if (err instanceof WidgetAPIError) {\n setError(err.message)\n opts.onError?.({ error: err.code, message: err.message })\n } else {\n setError('Unable to connect.')\n opts.onError?.({ error: 'unknown', message: 'Unable to connect.' })\n }\n }\n }, [opts.apiKey, opts.agentId, opts.apiBase, state, opts.onError])\n\n const disconnect = useCallback(() => {\n handleDisconnect()\n }, [handleDisconnect])\n\n const toggleMute = useCallback(() => setMuted(m => !m), [])\n\n const audio: ReactNode = session\n ? createElement(\n LiveKitRoom,\n {\n token: session.token,\n serverUrl: session.server_url,\n audio: !muted,\n video: false,\n connect: true,\n },\n createElement(AudioHandler, {\n onAgentConnected: handleAgentConnected,\n onDisconnected: handleDisconnect,\n }),\n )\n : null\n\n return {\n state,\n connect,\n disconnect,\n toggleMute,\n isMuted: muted,\n error,\n agentName: session?.agent_name,\n audio,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport { useRoomContext } from '@livekit/components-react'\nimport { RoomEvent, Track, type RemoteTrackPublication, type RemoteParticipant } from 'livekit-client'\n\ninterface AudioHandlerProps {\n onAgentConnected: () => void\n onDisconnected: () => void\n}\n\nexport function AudioHandler({ onAgentConnected, onDisconnected }: AudioHandlerProps) {\n const room = useRoomContext()\n const audioRef = useRef<HTMLAudioElement>(null)\n\n useEffect(() => {\n if (room.remoteParticipants.size > 0) {\n onAgentConnected()\n }\n\n const handleParticipantConnected = () => onAgentConnected()\n const handleDisconnect = () => onDisconnected()\n\n // The bot publishes multiple audio tracks (tts, bg noise, hold music).\n // Add each track to a single MediaStream so the browser mixes them.\n const attachTrack = (\n track: { kind: Track.Kind; mediaStreamTrack: MediaStreamTrack },\n _pub: RemoteTrackPublication,\n _participant: RemoteParticipant,\n ) => {\n if (track.kind === Track.Kind.Audio && audioRef.current) {\n const existing = audioRef.current.srcObject as MediaStream | null\n if (existing) {\n existing.addTrack(track.mediaStreamTrack)\n } else {\n audioRef.current.srcObject = new MediaStream([track.mediaStreamTrack])\n audioRef.current.play().catch(() => {})\n }\n }\n }\n\n room.on(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.on(RoomEvent.Disconnected, handleDisconnect)\n room.on(RoomEvent.TrackSubscribed, attachTrack)\n\n const initialTracks: MediaStreamTrack[] = []\n for (const participant of room.remoteParticipants.values()) {\n for (const pub of participant.trackPublications.values()) {\n if (pub.track && pub.isSubscribed && pub.track.kind === Track.Kind.Audio) {\n initialTracks.push(pub.track.mediaStreamTrack)\n }\n }\n }\n if (initialTracks.length > 0 && audioRef.current) {\n audioRef.current.srcObject = new MediaStream(initialTracks)\n audioRef.current.play().catch(() => {})\n }\n\n return () => {\n room.off(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.off(RoomEvent.Disconnected, handleDisconnect)\n room.off(RoomEvent.TrackSubscribed, attachTrack)\n }\n }, [room, onAgentConnected, onDisconnected])\n\n return <audio ref={audioRef} autoPlay />\n}\n","import type { WidgetSessionResponse, WidgetError } from './types'\n\nconst DEFAULT_API_BASE = 'https://api.thunderphone.com/v1'\n\nexport class WidgetAPIError extends Error {\n constructor(public code: string, message: string) {\n super(message)\n this.name = 'WidgetAPIError'\n }\n}\n\nexport async function createWidgetSession(\n apiKey: string,\n agentId: number,\n apiBase?: string,\n): Promise<WidgetSessionResponse> {\n const base = apiBase || DEFAULT_API_BASE\n const response = await fetch(`${base}/widget/session`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': apiKey,\n },\n body: JSON.stringify({ agent_id: agentId }),\n })\n\n if (!response.ok) {\n const data: WidgetError = await response.json().catch(() => ({\n error: 'unknown',\n message: 'Unable to connect.',\n }))\n throw new WidgetAPIError(data.error, data.message)\n }\n\n return response.json()\n}\n","import { WidgetButton } from './WidgetButton'\nimport { WidgetStatus } from './WidgetStatus'\nimport { useThunderPhone } from './useThunderPhone'\nimport type { ThunderPhoneWidgetProps } from './types'\n\nexport function ThunderPhoneWidget({\n apiKey,\n agentId,\n apiBase,\n onConnect,\n onDisconnect,\n onError,\n className,\n}: ThunderPhoneWidgetProps) {\n const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError })\n\n const handleClick = () => {\n if (phone.state === 'connected') {\n phone.disconnect()\n } else if (phone.state === 'idle' || phone.state === 'error' || phone.state === 'disconnected') {\n phone.connect()\n }\n }\n\n return (\n <div className={`tp-widget ${className || ''}`}>\n <WidgetStatus\n state={phone.state}\n agentName={phone.agentName || null}\n errorMessage={phone.error}\n />\n <WidgetButton\n state={phone.state}\n muted={phone.isMuted}\n onClick={handleClick}\n onMuteToggle={phone.toggleMute}\n />\n {phone.audio}\n </div>\n )\n}\n"],"mappings":"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","names":["jsx","jsxs","useState","useEffect","jsx","useState","jsx","jsxs"]}
|
|
1
|
+
{"version":3,"sources":["../src/WidgetButton.tsx","../src/WidgetStatus.tsx","../src/useThunderPhone.ts","../src/AudioHandler.tsx","../src/api.ts","../src/ThunderPhoneWidget.tsx"],"sourcesContent":["import type { WidgetState } from './types'\n\ninterface WidgetButtonProps {\n state: WidgetState\n muted: boolean\n onClick: () => void\n onMuteToggle: () => void\n}\n\nexport function WidgetButton({ state, muted, onClick, onMuteToggle }: WidgetButtonProps) {\n if (state === 'connected') {\n return (\n <div className=\"tp-button-group\">\n <button className=\"tp-button tp-button--mute\" onClick={onMuteToggle} type=\"button\">\n {muted ? (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\" />\n <path d=\"M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6\" />\n <path d=\"M17 16.95A7 7 0 0 1 5 12v-2m14 0v2c0 .76-.13 1.49-.36 2.18\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n <button className=\"tp-button tp-button--end\" onClick={onClick} type=\"button\">\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" />\n </svg>\n </button>\n </div>\n )\n }\n\n return (\n <button\n className={`tp-button tp-button--start ${state === 'connecting' ? 'tp-button--loading' : ''}`}\n onClick={onClick}\n disabled={state === 'connecting'}\n type=\"button\"\n >\n {state === 'connecting' ? (\n <svg className=\"tp-icon tp-spin\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n )\n}\n","import { useEffect, useState } from 'react'\nimport type { WidgetState } from './types'\n\ninterface WidgetStatusProps {\n state: WidgetState\n agentName: string | null\n errorMessage?: string\n}\n\nexport function WidgetStatus({ state, agentName, errorMessage }: WidgetStatusProps) {\n const [elapsed, setElapsed] = useState(0)\n\n useEffect(() => {\n if (state !== 'connected') {\n setElapsed(0)\n return\n }\n const interval = setInterval(() => setElapsed(s => s + 1), 1000)\n return () => clearInterval(interval)\n }, [state])\n\n const formatTime = (seconds: number) => {\n const m = Math.floor(seconds / 60)\n const s = seconds % 60\n return `${m}:${s.toString().padStart(2, '0')}`\n }\n\n const statusText: Record<WidgetState, string> = {\n idle: 'Ready',\n connecting: 'Connecting...',\n connected: formatTime(elapsed),\n disconnected: 'Disconnected',\n error: 'Unable to connect',\n }\n\n return (\n <div className=\"tp-status\">\n {agentName && <div className=\"tp-status__name\">{agentName}</div>}\n <div className={`tp-status__text tp-status--${state}`}>\n {state === 'connected' && <span className=\"tp-status__dot\" />}\n {errorMessage && state === 'error' ? errorMessage : statusText[state]}\n </div>\n </div>\n )\n}\n","import { useCallback, useEffect, useRef, useState, type ReactNode, createElement } from 'react'\nimport { LiveKitRoom } from '@livekit/components-react'\nimport { AudioHandler } from './AudioHandler'\nimport { createWidgetSession, WidgetAPIError } from './api'\nimport type { WidgetState, WidgetSessionResponse } from './types'\n\nconst DEFAULT_RINGTONE_URL = 'https://storage.googleapis.com/thunderphone-widget-cdn/widget/assets/ringtone-default.mp3'\n\nexport interface UseThunderPhoneOptions {\n apiKey: string\n agentId: number\n apiBase?: string\n onConnect?: () => void\n onDisconnect?: () => void\n onError?: (error: { error: string; message: string }) => void\n /**\n * Play a ringtone while connecting. Opt-in — disabled by default.\n * - `true` plays the default ringtone from the ThunderPhone CDN.\n * - A string URL plays a custom audio file.\n * - `false` or omitted disables the ringtone.\n */\n ringtone?: boolean | string\n}\n\nexport interface UseThunderPhoneReturn {\n state: WidgetState\n connect: () => void\n disconnect: () => void\n toggleMute: () => void\n isMuted: boolean\n error: string | undefined\n agentName: string | undefined\n /** Render this somewhere in your tree — it's invisible but handles audio. */\n audio: ReactNode\n}\n\n/** Resolve the ringtone option to a URL or null. */\nfunction resolveRingtoneUrl(ringtone: boolean | string | undefined): string | null {\n if (ringtone === true || ringtone === 'default') return DEFAULT_RINGTONE_URL\n if (typeof ringtone === 'string' && ringtone.length > 0) return ringtone\n return null\n}\n\n/** Fade out an audio element over ~200ms, then pause and reset it. */\nfunction fadeOutAndStop(audio: HTMLAudioElement) {\n if (audio.paused) return\n const fadeInterval = setInterval(() => {\n const next = audio.volume - 0.1\n if (next <= 0) {\n clearInterval(fadeInterval)\n audio.pause()\n audio.currentTime = 0\n audio.volume = 1.0\n } else {\n audio.volume = next\n }\n }, 20) // 10 steps × 20ms = 200ms fade\n // Return the interval ID so callers can cancel if needed.\n return fadeInterval\n}\n\nexport function useThunderPhone(opts: UseThunderPhoneOptions): UseThunderPhoneReturn {\n const [state, setState] = useState<WidgetState>('idle')\n const [session, setSession] = useState<WidgetSessionResponse | null>(null)\n const [muted, setMuted] = useState(false)\n const [error, setError] = useState<string | undefined>()\n\n // --- Ringtone management ---\n const ringtoneUrl = resolveRingtoneUrl(opts.ringtone)\n const ringtoneRef = useRef<HTMLAudioElement | null>(null)\n const fadeRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined)\n\n // Preload the ringtone audio element once when configured.\n useEffect(() => {\n if (!ringtoneUrl) {\n ringtoneRef.current = null\n return\n }\n const audio = new Audio()\n audio.crossOrigin = 'anonymous'\n audio.loop = true\n audio.preload = 'auto'\n audio.volume = 1.0\n audio.src = ringtoneUrl\n ringtoneRef.current = audio\n return () => {\n audio.pause()\n audio.src = ''\n ringtoneRef.current = null\n }\n }, [ringtoneUrl])\n\n // Stop ringtone when leaving the 'connecting' state.\n // Starting the ringtone is done synchronously inside connect() so it\n // executes within the user-gesture call stack (required by browsers).\n useEffect(() => {\n if (state !== 'connecting') {\n const audio = ringtoneRef.current\n if (audio && !audio.paused) {\n // Cancel any previous fade that's still running.\n if (fadeRef.current) clearInterval(fadeRef.current)\n fadeRef.current = fadeOutAndStop(audio)\n }\n }\n\n return () => {\n if (fadeRef.current) {\n clearInterval(fadeRef.current)\n fadeRef.current = undefined\n }\n }\n }, [state])\n\n const handleDisconnect = useCallback(() => {\n setState('disconnected')\n setSession(null)\n setMuted(false)\n opts.onDisconnect?.()\n setTimeout(() => setState('idle'), 1500)\n }, [opts.onDisconnect])\n\n const handleAgentConnected = useCallback(() => {\n setState('connected')\n opts.onConnect?.()\n }, [opts.onConnect])\n\n const connect = useCallback(async () => {\n if (state === 'connecting' || state === 'connected') return\n setState('connecting')\n setError(undefined)\n\n // Start ringtone immediately — this MUST happen synchronously within\n // the user-gesture (click) call stack or the browser will block it.\n const ringtoneAudio = ringtoneRef.current\n if (ringtoneAudio) {\n // Cancel any lingering fade from a previous attempt.\n if (fadeRef.current) {\n clearInterval(fadeRef.current)\n fadeRef.current = undefined\n }\n ringtoneAudio.currentTime = 0\n ringtoneAudio.volume = 1.0\n ringtoneAudio.play().catch(() => {})\n }\n\n // Warm up mic permission in the background so the browser prompt (if\n // needed) overlaps with the API call. This is fire-and-forget: if the\n // session request fails we simply discard the stream without the user\n // noticing an unnecessary permission prompt — getUserMedia only shows\n // the prompt once per origin, so subsequent calls are instant.\n navigator.mediaDevices.getUserMedia({ audio: true }).then(\n (stream) => { stream.getTracks().forEach((t) => t.stop()) },\n () => {},\n )\n\n try {\n const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase)\n setSession(sess)\n } catch (err) {\n setState('error')\n if (err instanceof WidgetAPIError) {\n setError(err.message)\n opts.onError?.({ error: err.code, message: err.message })\n } else {\n setError('Unable to connect.')\n opts.onError?.({ error: 'unknown', message: 'Unable to connect.' })\n }\n }\n }, [opts.apiKey, opts.agentId, opts.apiBase, state, opts.onError])\n\n const disconnect = useCallback(() => {\n handleDisconnect()\n }, [handleDisconnect])\n\n const toggleMute = useCallback(() => setMuted(m => !m), [])\n\n const audio: ReactNode = session\n ? createElement(\n LiveKitRoom,\n {\n token: session.token,\n serverUrl: session.server_url,\n audio: !muted,\n video: false,\n connect: true,\n },\n createElement(AudioHandler, {\n onAgentConnected: handleAgentConnected,\n onDisconnected: handleDisconnect,\n }),\n )\n : null\n\n return {\n state,\n connect,\n disconnect,\n toggleMute,\n isMuted: muted,\n error,\n agentName: session?.agent_name,\n audio,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport { useRoomContext } from '@livekit/components-react'\nimport { RoomEvent, Track, type RemoteTrackPublication, type RemoteParticipant } from 'livekit-client'\n\ninterface AudioHandlerProps {\n onAgentConnected: () => void\n onDisconnected: () => void\n}\n\nexport function AudioHandler({ onAgentConnected, onDisconnected }: AudioHandlerProps) {\n const room = useRoomContext()\n const audioRef = useRef<HTMLAudioElement>(null)\n\n useEffect(() => {\n if (room.remoteParticipants.size > 0) {\n onAgentConnected()\n }\n\n const handleParticipantConnected = () => onAgentConnected()\n const handleDisconnect = () => onDisconnected()\n\n // The bot publishes multiple audio tracks (tts, bg noise, hold music).\n // Add each track to a single MediaStream so the browser mixes them.\n const attachTrack = (\n track: { kind: Track.Kind; mediaStreamTrack: MediaStreamTrack },\n _pub: RemoteTrackPublication,\n _participant: RemoteParticipant,\n ) => {\n if (track.kind === Track.Kind.Audio && audioRef.current) {\n const existing = audioRef.current.srcObject as MediaStream | null\n if (existing) {\n existing.addTrack(track.mediaStreamTrack)\n } else {\n audioRef.current.srcObject = new MediaStream([track.mediaStreamTrack])\n audioRef.current.play().catch(() => {})\n }\n }\n }\n\n room.on(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.on(RoomEvent.Disconnected, handleDisconnect)\n room.on(RoomEvent.TrackSubscribed, attachTrack)\n\n const initialTracks: MediaStreamTrack[] = []\n for (const participant of room.remoteParticipants.values()) {\n for (const pub of participant.trackPublications.values()) {\n if (pub.track && pub.isSubscribed && pub.track.kind === Track.Kind.Audio) {\n initialTracks.push(pub.track.mediaStreamTrack)\n }\n }\n }\n if (initialTracks.length > 0 && audioRef.current) {\n audioRef.current.srcObject = new MediaStream(initialTracks)\n audioRef.current.play().catch(() => {})\n }\n\n return () => {\n room.off(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.off(RoomEvent.Disconnected, handleDisconnect)\n room.off(RoomEvent.TrackSubscribed, attachTrack)\n }\n }, [room, onAgentConnected, onDisconnected])\n\n return <audio ref={audioRef} autoPlay />\n}\n","import type { WidgetSessionResponse, WidgetError } from './types'\n\nconst DEFAULT_API_BASE = 'https://api.thunderphone.com/v1'\n\nexport class WidgetAPIError extends Error {\n constructor(public code: string, message: string) {\n super(message)\n this.name = 'WidgetAPIError'\n }\n}\n\nexport async function createWidgetSession(\n apiKey: string,\n agentId: number,\n apiBase?: string,\n): Promise<WidgetSessionResponse> {\n const base = apiBase || DEFAULT_API_BASE\n const response = await fetch(`${base}/widget/session`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': apiKey,\n },\n body: JSON.stringify({ agent_id: agentId }),\n })\n\n if (!response.ok) {\n const data: WidgetError = await response.json().catch(() => ({\n error: 'unknown',\n message: 'Unable to connect.',\n }))\n throw new WidgetAPIError(data.error, data.message)\n }\n\n return response.json()\n}\n","import { WidgetButton } from './WidgetButton'\nimport { WidgetStatus } from './WidgetStatus'\nimport { useThunderPhone } from './useThunderPhone'\nimport type { ThunderPhoneWidgetProps } from './types'\n\nexport function ThunderPhoneWidget({\n apiKey,\n agentId,\n apiBase,\n onConnect,\n onDisconnect,\n onError,\n className,\n ringtone,\n}: ThunderPhoneWidgetProps) {\n const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, ringtone })\n\n const handleClick = () => {\n if (phone.state === 'connected') {\n phone.disconnect()\n } else if (phone.state === 'idle' || phone.state === 'error' || phone.state === 'disconnected') {\n phone.connect()\n }\n }\n\n return (\n <div className={`tp-widget ${className || ''}`}>\n <WidgetStatus\n state={phone.state}\n agentName={phone.agentName || null}\n errorMessage={phone.error}\n />\n <WidgetButton\n state={phone.state}\n muted={phone.isMuted}\n onClick={handleClick}\n onMuteToggle={phone.toggleMute}\n />\n {phone.audio}\n </div>\n )\n}\n"],"mappings":"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","names":["jsx","jsxs","useEffect","useRef","useState","useEffect","jsx","useState","useRef","useEffect","audio","jsx","jsxs"]}
|