@thunderphone/widget 0.3.1 → 0.4.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/README.md +39 -1
- package/dist/index.d.mts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +79 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +80 -12
- package/dist/index.mjs.map +1 -1
- package/dist/mount.global.js +83 -15
- package/dist/mount.global.js.map +1 -1
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ThunderPhone Widget
|
|
2
2
|
|
|
3
3
|
Embed a ThunderPhone voice AI agent on any website. Users can talk to your agent directly from your site using their browser microphone.
|
|
4
4
|
|
|
@@ -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
|
@@ -116,25 +116,32 @@ function AudioHandler({ onAgentConnected, onDisconnected }) {
|
|
|
116
116
|
const handleDisconnect = () => onDisconnected();
|
|
117
117
|
const attachTrack = (track, _pub, _participant) => {
|
|
118
118
|
if (track.kind === import_livekit_client.Track.Kind.Audio && audioRef.current) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
119
|
+
const existing = audioRef.current.srcObject;
|
|
120
|
+
if (existing) {
|
|
121
|
+
existing.addTrack(track.mediaStreamTrack);
|
|
122
|
+
} else {
|
|
123
|
+
audioRef.current.srcObject = new MediaStream([track.mediaStreamTrack]);
|
|
124
|
+
audioRef.current.play().catch(() => {
|
|
125
|
+
});
|
|
126
|
+
}
|
|
123
127
|
}
|
|
124
128
|
};
|
|
125
129
|
room.on(import_livekit_client.RoomEvent.ParticipantConnected, handleParticipantConnected);
|
|
126
130
|
room.on(import_livekit_client.RoomEvent.Disconnected, handleDisconnect);
|
|
127
131
|
room.on(import_livekit_client.RoomEvent.TrackSubscribed, attachTrack);
|
|
132
|
+
const initialTracks = [];
|
|
128
133
|
for (const participant of room.remoteParticipants.values()) {
|
|
129
134
|
for (const pub of participant.trackPublications.values()) {
|
|
130
|
-
if (pub.track && pub.isSubscribed && pub.track.kind === import_livekit_client.Track.Kind.Audio
|
|
131
|
-
|
|
132
|
-
audioRef.current.srcObject = stream;
|
|
133
|
-
audioRef.current.play().catch(() => {
|
|
134
|
-
});
|
|
135
|
+
if (pub.track && pub.isSubscribed && pub.track.kind === import_livekit_client.Track.Kind.Audio) {
|
|
136
|
+
initialTracks.push(pub.track.mediaStreamTrack);
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
}
|
|
140
|
+
if (initialTracks.length > 0 && audioRef.current) {
|
|
141
|
+
audioRef.current.srcObject = new MediaStream(initialTracks);
|
|
142
|
+
audioRef.current.play().catch(() => {
|
|
143
|
+
});
|
|
144
|
+
}
|
|
138
145
|
return () => {
|
|
139
146
|
room.off(import_livekit_client.RoomEvent.ParticipantConnected, handleParticipantConnected);
|
|
140
147
|
room.off(import_livekit_client.RoomEvent.Disconnected, handleDisconnect);
|
|
@@ -174,11 +181,64 @@ async function createWidgetSession(apiKey, agentId, apiBase) {
|
|
|
174
181
|
}
|
|
175
182
|
|
|
176
183
|
// src/useThunderPhone.ts
|
|
184
|
+
var DEFAULT_RINGTONE_URL = "https://cdn.thunderphone.com/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
|
+
}
|
|
177
190
|
function useThunderPhone(opts) {
|
|
178
191
|
const [state, setState] = (0, import_react3.useState)("idle");
|
|
179
192
|
const [session, setSession] = (0, import_react3.useState)(null);
|
|
180
193
|
const [muted, setMuted] = (0, import_react3.useState)(false);
|
|
181
194
|
const [error, setError] = (0, import_react3.useState)();
|
|
195
|
+
const ringtoneUrl = resolveRingtoneUrl(opts.ringtone);
|
|
196
|
+
const ringtoneRef = (0, import_react3.useRef)(null);
|
|
197
|
+
(0, import_react3.useEffect)(() => {
|
|
198
|
+
if (!ringtoneUrl) {
|
|
199
|
+
ringtoneRef.current = null;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const audio2 = new Audio(ringtoneUrl);
|
|
203
|
+
audio2.loop = true;
|
|
204
|
+
audio2.preload = "auto";
|
|
205
|
+
audio2.volume = 1;
|
|
206
|
+
ringtoneRef.current = audio2;
|
|
207
|
+
return () => {
|
|
208
|
+
audio2.pause();
|
|
209
|
+
audio2.src = "";
|
|
210
|
+
ringtoneRef.current = null;
|
|
211
|
+
};
|
|
212
|
+
}, [ringtoneUrl]);
|
|
213
|
+
(0, import_react3.useEffect)(() => {
|
|
214
|
+
const audio2 = ringtoneRef.current;
|
|
215
|
+
if (!audio2) return;
|
|
216
|
+
let fadeInterval;
|
|
217
|
+
if (state === "connecting") {
|
|
218
|
+
audio2.currentTime = 0;
|
|
219
|
+
audio2.volume = 1;
|
|
220
|
+
audio2.play().catch(() => {
|
|
221
|
+
});
|
|
222
|
+
} else {
|
|
223
|
+
if (!audio2.paused) {
|
|
224
|
+
fadeInterval = setInterval(() => {
|
|
225
|
+
const next = audio2.volume - 0.1;
|
|
226
|
+
if (next <= 0) {
|
|
227
|
+
clearInterval(fadeInterval);
|
|
228
|
+
fadeInterval = void 0;
|
|
229
|
+
audio2.pause();
|
|
230
|
+
audio2.currentTime = 0;
|
|
231
|
+
audio2.volume = 1;
|
|
232
|
+
} else {
|
|
233
|
+
audio2.volume = next;
|
|
234
|
+
}
|
|
235
|
+
}, 20);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return () => {
|
|
239
|
+
if (fadeInterval) clearInterval(fadeInterval);
|
|
240
|
+
};
|
|
241
|
+
}, [state]);
|
|
182
242
|
const handleDisconnect = (0, import_react3.useCallback)(() => {
|
|
183
243
|
setState("disconnected");
|
|
184
244
|
setSession(null);
|
|
@@ -194,6 +254,13 @@ function useThunderPhone(opts) {
|
|
|
194
254
|
if (state === "connecting" || state === "connected") return;
|
|
195
255
|
setState("connecting");
|
|
196
256
|
setError(void 0);
|
|
257
|
+
navigator.mediaDevices.getUserMedia({ audio: true }).then(
|
|
258
|
+
(stream) => {
|
|
259
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
260
|
+
},
|
|
261
|
+
() => {
|
|
262
|
+
}
|
|
263
|
+
);
|
|
197
264
|
try {
|
|
198
265
|
const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase);
|
|
199
266
|
setSession(sess);
|
|
@@ -247,9 +314,10 @@ function ThunderPhoneWidget({
|
|
|
247
314
|
onConnect,
|
|
248
315
|
onDisconnect,
|
|
249
316
|
onError,
|
|
250
|
-
className
|
|
317
|
+
className,
|
|
318
|
+
ringtone
|
|
251
319
|
}) {
|
|
252
|
-
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError });
|
|
320
|
+
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, ringtone });
|
|
253
321
|
const handleClick = () => {
|
|
254
322
|
if (phone.state === "connected") {
|
|
255
323
|
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 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 stream = new MediaStream([track.mediaStreamTrack])\n audioRef.current.srcObject = stream\n audioRef.current.play().catch(() => {})\n }\n }\n\n room.on(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.on(RoomEvent.Disconnected, handleDisconnect)\n room.on(RoomEvent.TrackSubscribed, attachTrack)\n\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 && audioRef.current) {\n const stream = new MediaStream([pub.track.mediaStreamTrack])\n audioRef.current.srcObject = stream\n audioRef.current.play().catch(() => {})\n }\n }\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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACeY;AANL,SAAS,aAAa,EAAE,OAAO,OAAO,SAAS,aAAa,GAAsB;AACvF,MAAI,UAAU,aAAa;AACzB,WACE,6CAAC,SAAI,WAAU,mBACb;AAAA,kDAAC,YAAO,WAAU,6BAA4B,SAAS,cAAc,MAAK,UACvE,kBACC,6CAAC,SAAI,WAAU,WAAU,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KACzF;AAAA,oDAAC,UAAK,IAAG,KAAI,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK;AAAA,QACpC,4CAAC,UAAK,GAAE,0DAAyD;AAAA,QACjE,4CAAC,UAAK,GAAE,8DAA6D;AAAA,QACrE,4CAAC,UAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,QACtC,4CAAC,UAAK,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,SACvC,IAEA,6CAAC,SAAI,WAAU,WAAU,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KACzF;AAAA,oDAAC,UAAK,GAAE,wDAAuD;AAAA,QAC/D,4CAAC,UAAK,GAAE,8BAA6B;AAAA,QACrC,4CAAC,UAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,QACtC,4CAAC,UAAK,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,SACvC,GAEJ;AAAA,MACA,4CAAC,YAAO,WAAU,4BAA2B,SAAkB,MAAK,UAClE,sDAAC,SAAI,WAAU,WAAU,SAAQ,aAAY,MAAK,gBAChD,sDAAC,UAAK,GAAE,KAAI,GAAE,KAAI,OAAM,MAAK,QAAO,MAAK,IAAG,KAAI,GAClD,GACF;AAAA,OACF;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,8BAA8B,UAAU,eAAe,uBAAuB,EAAE;AAAA,MAC3F;AAAA,MACA,UAAU,UAAU;AAAA,MACpB,MAAK;AAAA,MAEJ,oBAAU,eACT,4CAAC,SAAI,WAAU,mBAAkB,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KACjG,sDAAC,UAAK,GAAE,+BAA8B,GACxC,IAEA,6CAAC,SAAI,WAAU,WAAU,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KACzF;AAAA,oDAAC,UAAK,GAAE,wDAAuD;AAAA,QAC/D,4CAAC,UAAK,GAAE,8BAA6B;AAAA,QACrC,4CAAC,UAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,QACtC,4CAAC,UAAK,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,SACvC;AAAA;AAAA,EAEJ;AAEJ;;;AC7DA,mBAAoC;AAqChB,IAAAA,sBAAA;AA5Bb,SAAS,aAAa,EAAE,OAAO,WAAW,aAAa,GAAsB;AAClF,QAAM,CAAC,SAAS,UAAU,QAAI,uBAAS,CAAC;AAExC,8BAAU,MAAM;AACd,QAAI,UAAU,aAAa;AACzB,iBAAW,CAAC;AACZ;AAAA,IACF;AACA,UAAM,WAAW,YAAY,MAAM,WAAW,OAAK,IAAI,CAAC,GAAG,GAAI;AAC/D,WAAO,MAAM,cAAc,QAAQ;AAAA,EACrC,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,aAAa,CAAC,YAAoB;AACtC,UAAM,IAAI,KAAK,MAAM,UAAU,EAAE;AACjC,UAAM,IAAI,UAAU;AACpB,WAAO,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,EAC9C;AAEA,QAAM,aAA0C;AAAA,IAC9C,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,WAAW,WAAW,OAAO;AAAA,IAC7B,cAAc;AAAA,IACd,OAAO;AAAA,EACT;AAEA,SACE,8CAAC,SAAI,WAAU,aACZ;AAAA,iBAAa,6CAAC,SAAI,WAAU,mBAAmB,qBAAU;AAAA,IAC1D,8CAAC,SAAI,WAAW,8BAA8B,KAAK,IAChD;AAAA,gBAAU,eAAe,6CAAC,UAAK,WAAU,kBAAiB;AAAA,MAC1D,gBAAgB,UAAU,UAAU,eAAe,WAAW,KAAK;AAAA,OACtE;AAAA,KACF;AAEJ;;;AC5CA,IAAAC,gBAAqE;AACrE,IAAAC,2BAA4B;;;ACD5B,IAAAC,gBAAkC;AAClC,8BAA+B;AAC/B,4BAAsF;AAoD7E,IAAAC,sBAAA;AA7CF,SAAS,aAAa,EAAE,kBAAkB,eAAe,GAAsB;AACpF,QAAM,WAAO,wCAAe;AAC5B,QAAM,eAAW,sBAAyB,IAAI;AAE9C,+BAAU,MAAM;AACd,QAAI,KAAK,mBAAmB,OAAO,GAAG;AACpC,uBAAiB;AAAA,IACnB;AAEA,UAAM,6BAA6B,MAAM,iBAAiB;AAC1D,UAAM,mBAAmB,MAAM,eAAe;AAE9C,UAAM,cAAc,CAClB,OACA,MACA,iBACG;AACH,UAAI,MAAM,SAAS,4BAAM,KAAK,SAAS,SAAS,SAAS;AACvD,cAAM,SAAS,IAAI,YAAY,CAAC,MAAM,gBAAgB,CAAC;AACvD,iBAAS,QAAQ,YAAY;AAC7B,iBAAS,QAAQ,KAAK,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MACxC;AAAA,IACF;AAEA,SAAK,GAAG,gCAAU,sBAAsB,0BAA0B;AAClE,SAAK,GAAG,gCAAU,cAAc,gBAAgB;AAChD,SAAK,GAAG,gCAAU,iBAAiB,WAAW;AAE9C,eAAW,eAAe,KAAK,mBAAmB,OAAO,GAAG;AAC1D,iBAAW,OAAO,YAAY,kBAAkB,OAAO,GAAG;AACxD,YAAI,IAAI,SAAS,IAAI,gBAAgB,IAAI,MAAM,SAAS,4BAAM,KAAK,SAAS,SAAS,SAAS;AAC5F,gBAAM,SAAS,IAAI,YAAY,CAAC,IAAI,MAAM,gBAAgB,CAAC;AAC3D,mBAAS,QAAQ,YAAY;AAC7B,mBAAS,QAAQ,KAAK,EAAE,MAAM,MAAM;AAAA,UAAC,CAAC;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAEA,WAAO,MAAM;AACX,WAAK,IAAI,gCAAU,sBAAsB,0BAA0B;AACnE,WAAK,IAAI,gCAAU,cAAc,gBAAgB;AACjD,WAAK,IAAI,gCAAU,iBAAiB,WAAW;AAAA,IACjD;AAAA,EACF,GAAG,CAAC,MAAM,kBAAkB,cAAc,CAAC;AAE3C,SAAO,6CAAC,WAAM,KAAK,UAAU,UAAQ,MAAC;AACxC;;;ACrDA,IAAM,mBAAmB;AAElB,IAAM,iBAAN,cAA6B,MAAM;AAAA,EACxC,YAAmB,MAAc,SAAiB;AAChD,UAAM,OAAO;AADI;AAEjB,SAAK,OAAO;AAAA,EACd;AACF;AAEA,eAAsB,oBACpB,QACA,SACA,SACgC;AAChC,QAAM,OAAO,WAAW;AACxB,QAAM,WAAW,MAAM,MAAM,GAAG,IAAI,mBAAmB;AAAA,IACrD,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,aAAa;AAAA,IACf;AAAA,IACA,MAAM,KAAK,UAAU,EAAE,UAAU,QAAQ,CAAC;AAAA,EAC5C,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,OAAoB,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO;AAAA,MAC3D,OAAO;AAAA,MACP,SAAS;AAAA,IACX,EAAE;AACF,UAAM,IAAI,eAAe,KAAK,OAAO,KAAK,OAAO;AAAA,EACnD;AAEA,SAAO,SAAS,KAAK;AACvB;;;AFRO,SAAS,gBAAgB,MAAqD;AACnF,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAAsB,MAAM;AACtD,QAAM,CAAC,SAAS,UAAU,QAAI,wBAAuC,IAAI;AACzE,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAAS,KAAK;AACxC,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAA6B;AAEvD,QAAM,uBAAmB,2BAAY,MAAM;AACzC,aAAS,cAAc;AACvB,eAAW,IAAI;AACf,aAAS,KAAK;AACd,SAAK,eAAe;AACpB,eAAW,MAAM,SAAS,MAAM,GAAG,IAAI;AAAA,EACzC,GAAG,CAAC,KAAK,YAAY,CAAC;AAEtB,QAAM,2BAAuB,2BAAY,MAAM;AAC7C,aAAS,WAAW;AACpB,SAAK,YAAY;AAAA,EACnB,GAAG,CAAC,KAAK,SAAS,CAAC;AAEnB,QAAM,cAAU,2BAAY,YAAY;AACtC,QAAI,UAAU,gBAAgB,UAAU,YAAa;AACrD,aAAS,YAAY;AACrB,aAAS,MAAS;AAClB,QAAI;AACF,YAAM,OAAO,MAAM,oBAAoB,KAAK,QAAQ,KAAK,SAAS,KAAK,OAAO;AAC9E,iBAAW,IAAI;AAAA,IACjB,SAAS,KAAK;AACZ,eAAS,OAAO;AAChB,UAAI,eAAe,gBAAgB;AACjC,iBAAS,IAAI,OAAO;AACpB,aAAK,UAAU,EAAE,OAAO,IAAI,MAAM,SAAS,IAAI,QAAQ,CAAC;AAAA,MAC1D,OAAO;AACL,iBAAS,oBAAoB;AAC7B,aAAK,UAAU,EAAE,OAAO,WAAW,SAAS,qBAAqB,CAAC;AAAA,MACpE;AAAA,IACF;AAAA,EACF,GAAG,CAAC,KAAK,QAAQ,KAAK,SAAS,KAAK,SAAS,OAAO,KAAK,OAAO,CAAC;AAEjE,QAAM,iBAAa,2BAAY,MAAM;AACnC,qBAAiB;AAAA,EACnB,GAAG,CAAC,gBAAgB,CAAC;AAErB,QAAM,iBAAa,2BAAY,MAAM,SAAS,OAAK,CAAC,CAAC,GAAG,CAAC,CAAC;AAE1D,QAAM,QAAmB,cACrB;AAAA,IACE;AAAA,IACA;AAAA,MACE,OAAO,QAAQ;AAAA,MACf,WAAW,QAAQ;AAAA,MACnB,OAAO,CAAC;AAAA,MACR,OAAO;AAAA,MACP,SAAS;AAAA,IACX;AAAA,QACA,6BAAc,cAAc;AAAA,MAC1B,kBAAkB;AAAA,MAClB,gBAAgB;AAAA,IAClB,CAAC;AAAA,EACH,IACA;AAEJ,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA,WAAW,SAAS;AAAA,IACpB;AAAA,EACF;AACF;;;AGzEI,IAAAC,sBAAA;AApBG,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,QAAQ,gBAAgB,EAAE,QAAQ,SAAS,SAAS,WAAW,cAAc,QAAQ,CAAC;AAE5F,QAAM,cAAc,MAAM;AACxB,QAAI,MAAM,UAAU,aAAa;AAC/B,YAAM,WAAW;AAAA,IACnB,WAAW,MAAM,UAAU,UAAU,MAAM,UAAU,WAAW,MAAM,UAAU,gBAAgB;AAC9F,YAAM,QAAQ;AAAA,IAChB;AAAA,EACF;AAEA,SACE,8CAAC,SAAI,WAAW,aAAa,aAAa,EAAE,IAC1C;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,MAAM;AAAA,QACb,WAAW,MAAM,aAAa;AAAA,QAC9B,cAAc,MAAM;AAAA;AAAA,IACtB;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,MAAM;AAAA,QACb,OAAO,MAAM;AAAA,QACb,SAAS;AAAA,QACT,cAAc,MAAM;AAAA;AAAA,IACtB;AAAA,IACC,MAAM;AAAA,KACT;AAEJ;","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://cdn.thunderphone.com/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\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\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(ringtoneUrl)\n audio.loop = true\n audio.preload = 'auto'\n audio.volume = 1.0\n ringtoneRef.current = audio\n return () => {\n audio.pause()\n audio.src = ''\n ringtoneRef.current = null\n }\n }, [ringtoneUrl])\n\n // Start/stop ringtone based on state.\n useEffect(() => {\n const audio = ringtoneRef.current\n if (!audio) return\n\n let fadeInterval: ReturnType<typeof setInterval> | undefined\n\n if (state === 'connecting') {\n // Reset to start and play (catch handles browsers that block autoplay\n // before a user gesture — unlikely here since connect() is click-driven).\n audio.currentTime = 0\n audio.volume = 1.0\n audio.play().catch(() => {})\n } else {\n // Fade out briefly (~200ms) for a smooth stop.\n if (!audio.paused) {\n fadeInterval = setInterval(() => {\n const next = audio.volume - 0.1\n if (next <= 0) {\n clearInterval(fadeInterval)\n fadeInterval = undefined\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 }\n }\n\n // Clean up any in-progress fade if state changes again before it completes.\n return () => {\n if (fadeInterval) clearInterval(fadeInterval)\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 // 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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACeY;AANL,SAAS,aAAa,EAAE,OAAO,OAAO,SAAS,aAAa,GAAsB;AACvF,MAAI,UAAU,aAAa;AACzB,WACE,6CAAC,SAAI,WAAU,mBACb;AAAA,kDAAC,YAAO,WAAU,6BAA4B,SAAS,cAAc,MAAK,UACvE,kBACC,6CAAC,SAAI,WAAU,WAAU,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KACzF;AAAA,oDAAC,UAAK,IAAG,KAAI,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK;AAAA,QACpC,4CAAC,UAAK,GAAE,0DAAyD;AAAA,QACjE,4CAAC,UAAK,GAAE,8DAA6D;AAAA,QACrE,4CAAC,UAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,QACtC,4CAAC,UAAK,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,SACvC,IAEA,6CAAC,SAAI,WAAU,WAAU,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KACzF;AAAA,oDAAC,UAAK,GAAE,wDAAuD;AAAA,QAC/D,4CAAC,UAAK,GAAE,8BAA6B;AAAA,QACrC,4CAAC,UAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,QACtC,4CAAC,UAAK,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,SACvC,GAEJ;AAAA,MACA,4CAAC,YAAO,WAAU,4BAA2B,SAAkB,MAAK,UAClE,sDAAC,SAAI,WAAU,WAAU,SAAQ,aAAY,MAAK,gBAChD,sDAAC,UAAK,GAAE,KAAI,GAAE,KAAI,OAAM,MAAK,QAAO,MAAK,IAAG,KAAI,GAClD,GACF;AAAA,OACF;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,8BAA8B,UAAU,eAAe,uBAAuB,EAAE;AAAA,MAC3F;AAAA,MACA,UAAU,UAAU;AAAA,MACpB,MAAK;AAAA,MAEJ,oBAAU,eACT,4CAAC,SAAI,WAAU,mBAAkB,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KACjG,sDAAC,UAAK,GAAE,+BAA8B,GACxC,IAEA,6CAAC,SAAI,WAAU,WAAU,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KACzF;AAAA,oDAAC,UAAK,GAAE,wDAAuD;AAAA,QAC/D,4CAAC,UAAK,GAAE,8BAA6B;AAAA,QACrC,4CAAC,UAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,QACtC,4CAAC,UAAK,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,SACvC;AAAA;AAAA,EAEJ;AAEJ;;;AC7DA,mBAAoC;AAqChB,IAAAA,sBAAA;AA5Bb,SAAS,aAAa,EAAE,OAAO,WAAW,aAAa,GAAsB;AAClF,QAAM,CAAC,SAAS,UAAU,QAAI,uBAAS,CAAC;AAExC,8BAAU,MAAM;AACd,QAAI,UAAU,aAAa;AACzB,iBAAW,CAAC;AACZ;AAAA,IACF;AACA,UAAM,WAAW,YAAY,MAAM,WAAW,OAAK,IAAI,CAAC,GAAG,GAAI;AAC/D,WAAO,MAAM,cAAc,QAAQ;AAAA,EACrC,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,aAAa,CAAC,YAAoB;AACtC,UAAM,IAAI,KAAK,MAAM,UAAU,EAAE;AACjC,UAAM,IAAI,UAAU;AACpB,WAAO,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,EAC9C;AAEA,QAAM,aAA0C;AAAA,IAC9C,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,WAAW,WAAW,OAAO;AAAA,IAC7B,cAAc;AAAA,IACd,OAAO;AAAA,EACT;AAEA,SACE,8CAAC,SAAI,WAAU,aACZ;AAAA,iBAAa,6CAAC,SAAI,WAAU,mBAAmB,qBAAU;AAAA,IAC1D,8CAAC,SAAI,WAAW,8BAA8B,KAAK,IAChD;AAAA,gBAAU,eAAe,6CAAC,UAAK,WAAU,kBAAiB;AAAA,MAC1D,gBAAgB,UAAU,UAAU,eAAe,WAAW,KAAK;AAAA,OACtE;AAAA,KACF;AAEJ;;;AC5CA,IAAAC,gBAAwF;AACxF,IAAAC,2BAA4B;;;ACD5B,IAAAC,gBAAkC;AAClC,8BAA+B;AAC/B,4BAAsF;AA6D7E,IAAAC,sBAAA;AAtDF,SAAS,aAAa,EAAE,kBAAkB,eAAe,GAAsB;AACpF,QAAM,WAAO,wCAAe;AAC5B,QAAM,eAAW,sBAAyB,IAAI;AAE9C,+BAAU,MAAM;AACd,QAAI,KAAK,mBAAmB,OAAO,GAAG;AACpC,uBAAiB;AAAA,IACnB;AAEA,UAAM,6BAA6B,MAAM,iBAAiB;AAC1D,UAAM,mBAAmB,MAAM,eAAe;AAI9C,UAAM,cAAc,CAClB,OACA,MACA,iBACG;AACH,UAAI,MAAM,SAAS,4BAAM,KAAK,SAAS,SAAS,SAAS;AACvD,cAAM,WAAW,SAAS,QAAQ;AAClC,YAAI,UAAU;AACZ,mBAAS,SAAS,MAAM,gBAAgB;AAAA,QAC1C,OAAO;AACL,mBAAS,QAAQ,YAAY,IAAI,YAAY,CAAC,MAAM,gBAAgB,CAAC;AACrE,mBAAS,QAAQ,KAAK,EAAE,MAAM,MAAM;AAAA,UAAC,CAAC;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAEA,SAAK,GAAG,gCAAU,sBAAsB,0BAA0B;AAClE,SAAK,GAAG,gCAAU,cAAc,gBAAgB;AAChD,SAAK,GAAG,gCAAU,iBAAiB,WAAW;AAE9C,UAAM,gBAAoC,CAAC;AAC3C,eAAW,eAAe,KAAK,mBAAmB,OAAO,GAAG;AAC1D,iBAAW,OAAO,YAAY,kBAAkB,OAAO,GAAG;AACxD,YAAI,IAAI,SAAS,IAAI,gBAAgB,IAAI,MAAM,SAAS,4BAAM,KAAK,OAAO;AACxE,wBAAc,KAAK,IAAI,MAAM,gBAAgB;AAAA,QAC/C;AAAA,MACF;AAAA,IACF;AACA,QAAI,cAAc,SAAS,KAAK,SAAS,SAAS;AAChD,eAAS,QAAQ,YAAY,IAAI,YAAY,aAAa;AAC1D,eAAS,QAAQ,KAAK,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IACxC;AAEA,WAAO,MAAM;AACX,WAAK,IAAI,gCAAU,sBAAsB,0BAA0B;AACnE,WAAK,IAAI,gCAAU,cAAc,gBAAgB;AACjD,WAAK,IAAI,gCAAU,iBAAiB,WAAW;AAAA,IACjD;AAAA,EACF,GAAG,CAAC,MAAM,kBAAkB,cAAc,CAAC;AAE3C,SAAO,6CAAC,WAAM,KAAK,UAAU,UAAQ,MAAC;AACxC;;;AC9DA,IAAM,mBAAmB;AAElB,IAAM,iBAAN,cAA6B,MAAM;AAAA,EACxC,YAAmB,MAAc,SAAiB;AAChD,UAAM,OAAO;AADI;AAEjB,SAAK,OAAO;AAAA,EACd;AACF;AAEA,eAAsB,oBACpB,QACA,SACA,SACgC;AAChC,QAAM,OAAO,WAAW;AACxB,QAAM,WAAW,MAAM,MAAM,GAAG,IAAI,mBAAmB;AAAA,IACrD,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,aAAa;AAAA,IACf;AAAA,IACA,MAAM,KAAK,UAAU,EAAE,UAAU,QAAQ,CAAC;AAAA,EAC5C,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,OAAoB,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO;AAAA,MAC3D,OAAO;AAAA,MACP,SAAS;AAAA,IACX,EAAE;AACF,UAAM,IAAI,eAAe,KAAK,OAAO,KAAK,OAAO;AAAA,EACnD;AAEA,SAAO,SAAS,KAAK;AACvB;;;AF7BA,IAAM,uBAAuB;AA+B7B,SAAS,mBAAmB,UAAuD;AACjF,MAAI,aAAa,QAAQ,aAAa,UAAW,QAAO;AACxD,MAAI,OAAO,aAAa,YAAY,SAAS,SAAS,EAAG,QAAO;AAChE,SAAO;AACT;AAEO,SAAS,gBAAgB,MAAqD;AACnF,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAAsB,MAAM;AACtD,QAAM,CAAC,SAAS,UAAU,QAAI,wBAAuC,IAAI;AACzE,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAAS,KAAK;AACxC,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAA6B;AAGvD,QAAM,cAAc,mBAAmB,KAAK,QAAQ;AACpD,QAAM,kBAAc,sBAAgC,IAAI;AAGxD,+BAAU,MAAM;AACd,QAAI,CAAC,aAAa;AAChB,kBAAY,UAAU;AACtB;AAAA,IACF;AACA,UAAMC,SAAQ,IAAI,MAAM,WAAW;AACnC,IAAAA,OAAM,OAAO;AACb,IAAAA,OAAM,UAAU;AAChB,IAAAA,OAAM,SAAS;AACf,gBAAY,UAAUA;AACtB,WAAO,MAAM;AACX,MAAAA,OAAM,MAAM;AACZ,MAAAA,OAAM,MAAM;AACZ,kBAAY,UAAU;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,WAAW,CAAC;AAGhB,+BAAU,MAAM;AACd,UAAMA,SAAQ,YAAY;AAC1B,QAAI,CAACA,OAAO;AAEZ,QAAI;AAEJ,QAAI,UAAU,cAAc;AAG1B,MAAAA,OAAM,cAAc;AACpB,MAAAA,OAAM,SAAS;AACf,MAAAA,OAAM,KAAK,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAC7B,OAAO;AAEL,UAAI,CAACA,OAAM,QAAQ;AACjB,uBAAe,YAAY,MAAM;AAC/B,gBAAM,OAAOA,OAAM,SAAS;AAC5B,cAAI,QAAQ,GAAG;AACb,0BAAc,YAAY;AAC1B,2BAAe;AACf,YAAAA,OAAM,MAAM;AACZ,YAAAA,OAAM,cAAc;AACpB,YAAAA,OAAM,SAAS;AAAA,UACjB,OAAO;AACL,YAAAA,OAAM,SAAS;AAAA,UACjB;AAAA,QACF,GAAG,EAAE;AAAA,MACP;AAAA,IACF;AAGA,WAAO,MAAM;AACX,UAAI,aAAc,eAAc,YAAY;AAAA,IAC9C;AAAA,EACF,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,uBAAmB,2BAAY,MAAM;AACzC,aAAS,cAAc;AACvB,eAAW,IAAI;AACf,aAAS,KAAK;AACd,SAAK,eAAe;AACpB,eAAW,MAAM,SAAS,MAAM,GAAG,IAAI;AAAA,EACzC,GAAG,CAAC,KAAK,YAAY,CAAC;AAEtB,QAAM,2BAAuB,2BAAY,MAAM;AAC7C,aAAS,WAAW;AACpB,SAAK,YAAY;AAAA,EACnB,GAAG,CAAC,KAAK,SAAS,CAAC;AAEnB,QAAM,cAAU,2BAAY,YAAY;AACtC,QAAI,UAAU,gBAAgB,UAAU,YAAa;AACrD,aAAS,YAAY;AACrB,aAAS,MAAS;AAMlB,cAAU,aAAa,aAAa,EAAE,OAAO,KAAK,CAAC,EAAE;AAAA,MACnD,CAAC,WAAW;AAAE,eAAO,UAAU,EAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;AAAA,MAAE;AAAA,MAC1D,MAAM;AAAA,MAAC;AAAA,IACT;AAEA,QAAI;AACF,YAAM,OAAO,MAAM,oBAAoB,KAAK,QAAQ,KAAK,SAAS,KAAK,OAAO;AAC9E,iBAAW,IAAI;AAAA,IACjB,SAAS,KAAK;AACZ,eAAS,OAAO;AAChB,UAAI,eAAe,gBAAgB;AACjC,iBAAS,IAAI,OAAO;AACpB,aAAK,UAAU,EAAE,OAAO,IAAI,MAAM,SAAS,IAAI,QAAQ,CAAC;AAAA,MAC1D,OAAO;AACL,iBAAS,oBAAoB;AAC7B,aAAK,UAAU,EAAE,OAAO,WAAW,SAAS,qBAAqB,CAAC;AAAA,MACpE;AAAA,IACF;AAAA,EACF,GAAG,CAAC,KAAK,QAAQ,KAAK,SAAS,KAAK,SAAS,OAAO,KAAK,OAAO,CAAC;AAEjE,QAAM,iBAAa,2BAAY,MAAM;AACnC,qBAAiB;AAAA,EACnB,GAAG,CAAC,gBAAgB,CAAC;AAErB,QAAM,iBAAa,2BAAY,MAAM,SAAS,OAAK,CAAC,CAAC,GAAG,CAAC,CAAC;AAE1D,QAAM,QAAmB,cACrB;AAAA,IACE;AAAA,IACA;AAAA,MACE,OAAO,QAAQ;AAAA,MACf,WAAW,QAAQ;AAAA,MACnB,OAAO,CAAC;AAAA,MACR,OAAO;AAAA,MACP,SAAS;AAAA,IACX;AAAA,QACA,6BAAc,cAAc;AAAA,MAC1B,kBAAkB;AAAA,MAClB,gBAAgB;AAAA,IAClB,CAAC;AAAA,EACH,IACA;AAEJ,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA,WAAW,SAAS;AAAA,IACpB;AAAA,EACF;AACF;;;AG7JI,IAAAC,sBAAA;AArBG,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,QAAQ,gBAAgB,EAAE,QAAQ,SAAS,SAAS,WAAW,cAAc,SAAS,SAAS,CAAC;AAEtG,QAAM,cAAc,MAAM;AACxB,QAAI,MAAM,UAAU,aAAa;AAC/B,YAAM,WAAW;AAAA,IACnB,WAAW,MAAM,UAAU,UAAU,MAAM,UAAU,WAAW,MAAM,UAAU,gBAAgB;AAC9F,YAAM,QAAQ;AAAA,IAChB;AAAA,EACF;AAEA,SACE,8CAAC,SAAI,WAAW,aAAa,aAAa,EAAE,IAC1C;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,MAAM;AAAA,QACb,WAAW,MAAM,aAAa;AAAA,QAC9B,cAAc,MAAM;AAAA;AAAA,IACtB;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,MAAM;AAAA,QACb,OAAO,MAAM;AAAA,QACb,SAAS;AAAA,QACT,cAAc,MAAM;AAAA;AAAA,IACtB;AAAA,IACC,MAAM;AAAA,KACT;AAEJ;","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
|
|
@@ -89,25 +89,32 @@ function AudioHandler({ onAgentConnected, onDisconnected }) {
|
|
|
89
89
|
const handleDisconnect = () => onDisconnected();
|
|
90
90
|
const attachTrack = (track, _pub, _participant) => {
|
|
91
91
|
if (track.kind === Track.Kind.Audio && audioRef.current) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
92
|
+
const existing = audioRef.current.srcObject;
|
|
93
|
+
if (existing) {
|
|
94
|
+
existing.addTrack(track.mediaStreamTrack);
|
|
95
|
+
} else {
|
|
96
|
+
audioRef.current.srcObject = new MediaStream([track.mediaStreamTrack]);
|
|
97
|
+
audioRef.current.play().catch(() => {
|
|
98
|
+
});
|
|
99
|
+
}
|
|
96
100
|
}
|
|
97
101
|
};
|
|
98
102
|
room.on(RoomEvent.ParticipantConnected, handleParticipantConnected);
|
|
99
103
|
room.on(RoomEvent.Disconnected, handleDisconnect);
|
|
100
104
|
room.on(RoomEvent.TrackSubscribed, attachTrack);
|
|
105
|
+
const initialTracks = [];
|
|
101
106
|
for (const participant of room.remoteParticipants.values()) {
|
|
102
107
|
for (const pub of participant.trackPublications.values()) {
|
|
103
|
-
if (pub.track && pub.isSubscribed && pub.track.kind === Track.Kind.Audio
|
|
104
|
-
|
|
105
|
-
audioRef.current.srcObject = stream;
|
|
106
|
-
audioRef.current.play().catch(() => {
|
|
107
|
-
});
|
|
108
|
+
if (pub.track && pub.isSubscribed && pub.track.kind === Track.Kind.Audio) {
|
|
109
|
+
initialTracks.push(pub.track.mediaStreamTrack);
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
}
|
|
113
|
+
if (initialTracks.length > 0 && audioRef.current) {
|
|
114
|
+
audioRef.current.srcObject = new MediaStream(initialTracks);
|
|
115
|
+
audioRef.current.play().catch(() => {
|
|
116
|
+
});
|
|
117
|
+
}
|
|
111
118
|
return () => {
|
|
112
119
|
room.off(RoomEvent.ParticipantConnected, handleParticipantConnected);
|
|
113
120
|
room.off(RoomEvent.Disconnected, handleDisconnect);
|
|
@@ -147,11 +154,64 @@ async function createWidgetSession(apiKey, agentId, apiBase) {
|
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
// src/useThunderPhone.ts
|
|
157
|
+
var DEFAULT_RINGTONE_URL = "https://cdn.thunderphone.com/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
|
+
}
|
|
150
163
|
function useThunderPhone(opts) {
|
|
151
164
|
const [state, setState] = useState2("idle");
|
|
152
165
|
const [session, setSession] = useState2(null);
|
|
153
166
|
const [muted, setMuted] = useState2(false);
|
|
154
167
|
const [error, setError] = useState2();
|
|
168
|
+
const ringtoneUrl = resolveRingtoneUrl(opts.ringtone);
|
|
169
|
+
const ringtoneRef = useRef2(null);
|
|
170
|
+
useEffect3(() => {
|
|
171
|
+
if (!ringtoneUrl) {
|
|
172
|
+
ringtoneRef.current = null;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const audio2 = new Audio(ringtoneUrl);
|
|
176
|
+
audio2.loop = true;
|
|
177
|
+
audio2.preload = "auto";
|
|
178
|
+
audio2.volume = 1;
|
|
179
|
+
ringtoneRef.current = audio2;
|
|
180
|
+
return () => {
|
|
181
|
+
audio2.pause();
|
|
182
|
+
audio2.src = "";
|
|
183
|
+
ringtoneRef.current = null;
|
|
184
|
+
};
|
|
185
|
+
}, [ringtoneUrl]);
|
|
186
|
+
useEffect3(() => {
|
|
187
|
+
const audio2 = ringtoneRef.current;
|
|
188
|
+
if (!audio2) return;
|
|
189
|
+
let fadeInterval;
|
|
190
|
+
if (state === "connecting") {
|
|
191
|
+
audio2.currentTime = 0;
|
|
192
|
+
audio2.volume = 1;
|
|
193
|
+
audio2.play().catch(() => {
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
if (!audio2.paused) {
|
|
197
|
+
fadeInterval = setInterval(() => {
|
|
198
|
+
const next = audio2.volume - 0.1;
|
|
199
|
+
if (next <= 0) {
|
|
200
|
+
clearInterval(fadeInterval);
|
|
201
|
+
fadeInterval = void 0;
|
|
202
|
+
audio2.pause();
|
|
203
|
+
audio2.currentTime = 0;
|
|
204
|
+
audio2.volume = 1;
|
|
205
|
+
} else {
|
|
206
|
+
audio2.volume = next;
|
|
207
|
+
}
|
|
208
|
+
}, 20);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return () => {
|
|
212
|
+
if (fadeInterval) clearInterval(fadeInterval);
|
|
213
|
+
};
|
|
214
|
+
}, [state]);
|
|
155
215
|
const handleDisconnect = useCallback(() => {
|
|
156
216
|
setState("disconnected");
|
|
157
217
|
setSession(null);
|
|
@@ -167,6 +227,13 @@ function useThunderPhone(opts) {
|
|
|
167
227
|
if (state === "connecting" || state === "connected") return;
|
|
168
228
|
setState("connecting");
|
|
169
229
|
setError(void 0);
|
|
230
|
+
navigator.mediaDevices.getUserMedia({ audio: true }).then(
|
|
231
|
+
(stream) => {
|
|
232
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
233
|
+
},
|
|
234
|
+
() => {
|
|
235
|
+
}
|
|
236
|
+
);
|
|
170
237
|
try {
|
|
171
238
|
const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase);
|
|
172
239
|
setSession(sess);
|
|
@@ -220,9 +287,10 @@ function ThunderPhoneWidget({
|
|
|
220
287
|
onConnect,
|
|
221
288
|
onDisconnect,
|
|
222
289
|
onError,
|
|
223
|
-
className
|
|
290
|
+
className,
|
|
291
|
+
ringtone
|
|
224
292
|
}) {
|
|
225
|
-
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError });
|
|
293
|
+
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, ringtone });
|
|
226
294
|
const handleClick = () => {
|
|
227
295
|
if (phone.state === "connected") {
|
|
228
296
|
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 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 stream = new MediaStream([track.mediaStreamTrack])\n audioRef.current.srcObject = stream\n audioRef.current.play().catch(() => {})\n }\n }\n\n room.on(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.on(RoomEvent.Disconnected, handleDisconnect)\n room.on(RoomEvent.TrackSubscribed, attachTrack)\n\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 && audioRef.current) {\n const stream = new MediaStream([pub.track.mediaStreamTrack])\n audioRef.current.srcObject = stream\n audioRef.current.play().catch(() => {})\n }\n }\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://cdn.thunderphone.com/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\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\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(ringtoneUrl)\n audio.loop = true\n audio.preload = 'auto'\n audio.volume = 1.0\n ringtoneRef.current = audio\n return () => {\n audio.pause()\n audio.src = ''\n ringtoneRef.current = null\n }\n }, [ringtoneUrl])\n\n // Start/stop ringtone based on state.\n useEffect(() => {\n const audio = ringtoneRef.current\n if (!audio) return\n\n let fadeInterval: ReturnType<typeof setInterval> | undefined\n\n if (state === 'connecting') {\n // Reset to start and play (catch handles browsers that block autoplay\n // before a user gesture — unlikely here since connect() is click-driven).\n audio.currentTime = 0\n audio.volume = 1.0\n audio.play().catch(() => {})\n } else {\n // Fade out briefly (~200ms) for a smooth stop.\n if (!audio.paused) {\n fadeInterval = setInterval(() => {\n const next = audio.volume - 0.1\n if (next <= 0) {\n clearInterval(fadeInterval)\n fadeInterval = undefined\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 }\n }\n\n // Clean up any in-progress fade if state changes again before it completes.\n return () => {\n if (fadeInterval) clearInterval(fadeInterval)\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 // 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"]}
|