@thunderphone/widget 0.4.1 → 1.0.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 +14 -21
- package/dist/index.d.mts +3 -5
- package/dist/index.d.ts +3 -5
- package/dist/index.js +17 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +17 -10
- package/dist/index.mjs.map +1 -1
- package/dist/mount.global.js +17 -10
- package/dist/mount.global.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,8 +17,7 @@ import '@thunderphone/widget/style.css'
|
|
|
17
17
|
function App() {
|
|
18
18
|
return (
|
|
19
19
|
<ThunderPhoneWidget
|
|
20
|
-
|
|
21
|
-
agentId={123}
|
|
20
|
+
publishableKey="pk_live_your_publishable_key"
|
|
22
21
|
/>
|
|
23
22
|
)
|
|
24
23
|
}
|
|
@@ -28,15 +27,14 @@ function App() {
|
|
|
28
27
|
|
|
29
28
|
1. Log in to [app.thunderphone.com](https://app.thunderphone.com)
|
|
30
29
|
2. Go to **Developers** to create a publishable API key and configure allowed domains
|
|
31
|
-
3.
|
|
32
|
-
4. Use
|
|
30
|
+
3. Create a **Web Widget** and select the agent you want to expose
|
|
31
|
+
4. Use your publishable key in the widget code above
|
|
33
32
|
|
|
34
33
|
## Props
|
|
35
34
|
|
|
36
35
|
| Prop | Type | Required | Description |
|
|
37
36
|
|------|------|----------|-------------|
|
|
38
|
-
| `
|
|
39
|
-
| `agentId` | `number` | Yes | ID of the agent to connect to |
|
|
37
|
+
| `publishableKey` | `string` | Yes | Your publishable API key (`pk_live_...`). The agent is resolved from the key's configuration. |
|
|
40
38
|
| `apiBase` | `string` | No | API base URL (defaults to `https://api.thunderphone.com/v1`) |
|
|
41
39
|
| `onConnect` | `() => void` | No | Called when the voice session connects |
|
|
42
40
|
| `onDisconnect` | `() => void` | No | Called when the session ends |
|
|
@@ -53,8 +51,7 @@ import { useThunderPhone } from '@thunderphone/widget'
|
|
|
53
51
|
|
|
54
52
|
function CustomCallButton() {
|
|
55
53
|
const phone = useThunderPhone({
|
|
56
|
-
|
|
57
|
-
agentId: 123,
|
|
54
|
+
publishableKey: 'pk_live_your_publishable_key',
|
|
58
55
|
})
|
|
59
56
|
|
|
60
57
|
const handleClick = () => {
|
|
@@ -86,8 +83,7 @@ function CustomCallButton() {
|
|
|
86
83
|
|
|
87
84
|
| Option | Type | Required | Description |
|
|
88
85
|
|--------|------|----------|-------------|
|
|
89
|
-
| `
|
|
90
|
-
| `agentId` | `number` | Yes | ID of the agent to connect to |
|
|
86
|
+
| `publishableKey` | `string` | Yes | Your publishable API key |
|
|
91
87
|
| `apiBase` | `string` | No | API base URL override |
|
|
92
88
|
| `onConnect` | `() => void` | No | Called when the voice session connects |
|
|
93
89
|
| `onDisconnect` | `() => void` | No | Called when the session ends |
|
|
@@ -113,8 +109,7 @@ Play a ringing sound while the widget connects, to simulate a phone call:
|
|
|
113
109
|
|
|
114
110
|
```tsx
|
|
115
111
|
<ThunderPhoneWidget
|
|
116
|
-
|
|
117
|
-
agentId={123}
|
|
112
|
+
publishableKey="pk_live_your_publishable_key"
|
|
118
113
|
ringtone={true}
|
|
119
114
|
/>
|
|
120
115
|
```
|
|
@@ -123,8 +118,7 @@ Use a custom audio file by passing a URL:
|
|
|
123
118
|
|
|
124
119
|
```tsx
|
|
125
120
|
<ThunderPhoneWidget
|
|
126
|
-
|
|
127
|
-
agentId={123}
|
|
121
|
+
publishableKey="pk_live_your_publishable_key"
|
|
128
122
|
ringtone="https://example.com/my-ringtone.mp3"
|
|
129
123
|
/>
|
|
130
124
|
```
|
|
@@ -134,13 +128,13 @@ The ringtone loops during the `connecting` state and fades out when the agent co
|
|
|
134
128
|
The headless hook accepts the same option:
|
|
135
129
|
|
|
136
130
|
```tsx
|
|
137
|
-
const phone = useThunderPhone({
|
|
131
|
+
const phone = useThunderPhone({ publishableKey, ringtone: true })
|
|
138
132
|
```
|
|
139
133
|
|
|
140
134
|
And the script-tag mount API:
|
|
141
135
|
|
|
142
136
|
```js
|
|
143
|
-
ThunderPhone.mount({ element: '#thunderphone',
|
|
137
|
+
ThunderPhone.mount({ element: '#thunderphone', publishableKey: '...', ringtone: true })
|
|
144
138
|
```
|
|
145
139
|
|
|
146
140
|
## Styling
|
|
@@ -180,16 +174,15 @@ Wildcard subdomains are supported: `*.example.com` matches `app.example.com`, `d
|
|
|
180
174
|
If you're not using a bundler, you can load the widget via script tag. This version bundles React internally so no dependencies are needed.
|
|
181
175
|
|
|
182
176
|
```html
|
|
183
|
-
<link rel="stylesheet" href="https://cdn.thunderphone.com/widget/v0.
|
|
184
|
-
<script src="https://cdn.thunderphone.com/widget/v0.
|
|
177
|
+
<link rel="stylesheet" href="https://cdn.thunderphone.com/widget/v0.4.0/style.css" />
|
|
178
|
+
<script src="https://cdn.thunderphone.com/widget/v0.4.0/widget.js"></script>
|
|
185
179
|
|
|
186
180
|
<div id="thunderphone"></div>
|
|
187
181
|
|
|
188
182
|
<script>
|
|
189
183
|
ThunderPhone.mount({
|
|
190
184
|
element: '#thunderphone',
|
|
191
|
-
|
|
192
|
-
agentId: 123,
|
|
185
|
+
publishableKey: 'pk_live_your_publishable_key',
|
|
193
186
|
})
|
|
194
187
|
</script>
|
|
195
188
|
```
|
|
@@ -204,7 +197,7 @@ https://cdn.thunderphone.com/widget/latest/style.css
|
|
|
204
197
|
`ThunderPhone.mount()` accepts the same options as the React component props above, plus `element` (CSS selector or DOM element). It returns a handle for cleanup:
|
|
205
198
|
|
|
206
199
|
```js
|
|
207
|
-
const widget = ThunderPhone.mount({ element: '#thunderphone',
|
|
200
|
+
const widget = ThunderPhone.mount({ element: '#thunderphone', publishableKey: '...' })
|
|
208
201
|
|
|
209
202
|
// Later, to remove the widget:
|
|
210
203
|
widget.unmount()
|
package/dist/index.d.mts
CHANGED
|
@@ -7,8 +7,7 @@ interface WidgetError {
|
|
|
7
7
|
}
|
|
8
8
|
type WidgetState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
9
9
|
interface ThunderPhoneWidgetProps {
|
|
10
|
-
|
|
11
|
-
agentId: number;
|
|
10
|
+
publishableKey: string;
|
|
12
11
|
apiBase?: string;
|
|
13
12
|
onConnect?: () => void;
|
|
14
13
|
onDisconnect?: () => void;
|
|
@@ -23,11 +22,10 @@ interface ThunderPhoneWidgetProps {
|
|
|
23
22
|
ringtone?: boolean | string;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
declare function ThunderPhoneWidget({
|
|
25
|
+
declare function ThunderPhoneWidget({ publishableKey, apiBase, onConnect, onDisconnect, onError, className, ringtone, }: ThunderPhoneWidgetProps): react_jsx_runtime.JSX.Element;
|
|
27
26
|
|
|
28
27
|
interface UseThunderPhoneOptions {
|
|
29
|
-
|
|
30
|
-
agentId: number;
|
|
28
|
+
publishableKey: string;
|
|
31
29
|
apiBase?: string;
|
|
32
30
|
onConnect?: () => void;
|
|
33
31
|
onDisconnect?: () => void;
|
package/dist/index.d.ts
CHANGED
|
@@ -7,8 +7,7 @@ interface WidgetError {
|
|
|
7
7
|
}
|
|
8
8
|
type WidgetState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
9
9
|
interface ThunderPhoneWidgetProps {
|
|
10
|
-
|
|
11
|
-
agentId: number;
|
|
10
|
+
publishableKey: string;
|
|
12
11
|
apiBase?: string;
|
|
13
12
|
onConnect?: () => void;
|
|
14
13
|
onDisconnect?: () => void;
|
|
@@ -23,11 +22,10 @@ interface ThunderPhoneWidgetProps {
|
|
|
23
22
|
ringtone?: boolean | string;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
declare function ThunderPhoneWidget({
|
|
25
|
+
declare function ThunderPhoneWidget({ publishableKey, apiBase, onConnect, onDisconnect, onError, className, ringtone, }: ThunderPhoneWidgetProps): react_jsx_runtime.JSX.Element;
|
|
27
26
|
|
|
28
27
|
interface UseThunderPhoneOptions {
|
|
29
|
-
|
|
30
|
-
agentId: number;
|
|
28
|
+
publishableKey: string;
|
|
31
29
|
apiBase?: string;
|
|
32
30
|
onConnect?: () => void;
|
|
33
31
|
onDisconnect?: () => void;
|
package/dist/index.js
CHANGED
|
@@ -109,10 +109,16 @@ function AudioHandler({ onAgentConnected, onDisconnected }) {
|
|
|
109
109
|
const room = (0, import_components_react.useRoomContext)();
|
|
110
110
|
const audioRef = (0, import_react2.useRef)(null);
|
|
111
111
|
(0, import_react2.useEffect)(() => {
|
|
112
|
-
|
|
112
|
+
let didSignalConnected = false;
|
|
113
|
+
const signalConnected = () => {
|
|
114
|
+
if (didSignalConnected) return;
|
|
115
|
+
didSignalConnected = true;
|
|
113
116
|
onAgentConnected();
|
|
117
|
+
};
|
|
118
|
+
if (room.remoteParticipants.size > 0) {
|
|
119
|
+
signalConnected();
|
|
114
120
|
}
|
|
115
|
-
const handleParticipantConnected = () =>
|
|
121
|
+
const handleParticipantConnected = () => signalConnected();
|
|
116
122
|
const handleDisconnect = () => onDisconnected();
|
|
117
123
|
const attachTrack = (track, _pub, _participant) => {
|
|
118
124
|
if (track.kind === import_livekit_client.Track.Kind.Audio && audioRef.current) {
|
|
@@ -124,6 +130,7 @@ function AudioHandler({ onAgentConnected, onDisconnected }) {
|
|
|
124
130
|
audioRef.current.play().catch(() => {
|
|
125
131
|
});
|
|
126
132
|
}
|
|
133
|
+
signalConnected();
|
|
127
134
|
}
|
|
128
135
|
};
|
|
129
136
|
room.on(import_livekit_client.RoomEvent.ParticipantConnected, handleParticipantConnected);
|
|
@@ -141,6 +148,7 @@ function AudioHandler({ onAgentConnected, onDisconnected }) {
|
|
|
141
148
|
audioRef.current.srcObject = new MediaStream(initialTracks);
|
|
142
149
|
audioRef.current.play().catch(() => {
|
|
143
150
|
});
|
|
151
|
+
signalConnected();
|
|
144
152
|
}
|
|
145
153
|
return () => {
|
|
146
154
|
room.off(import_livekit_client.RoomEvent.ParticipantConnected, handleParticipantConnected);
|
|
@@ -160,15 +168,15 @@ var WidgetAPIError = class extends Error {
|
|
|
160
168
|
this.name = "WidgetAPIError";
|
|
161
169
|
}
|
|
162
170
|
};
|
|
163
|
-
async function createWidgetSession(
|
|
171
|
+
async function createWidgetSession(publishableKey, apiBase) {
|
|
164
172
|
const base = apiBase || DEFAULT_API_BASE;
|
|
165
173
|
const response = await fetch(`${base}/widget/session`, {
|
|
166
174
|
method: "POST",
|
|
167
175
|
headers: {
|
|
168
176
|
"Content-Type": "application/json",
|
|
169
|
-
"X-API-Key":
|
|
177
|
+
"X-API-Key": publishableKey
|
|
170
178
|
},
|
|
171
|
-
body: JSON.stringify({
|
|
179
|
+
body: JSON.stringify({})
|
|
172
180
|
});
|
|
173
181
|
if (!response.ok) {
|
|
174
182
|
const data = await response.json().catch(() => ({
|
|
@@ -277,7 +285,7 @@ function useThunderPhone(opts) {
|
|
|
277
285
|
}
|
|
278
286
|
);
|
|
279
287
|
try {
|
|
280
|
-
const sess = await createWidgetSession(opts.
|
|
288
|
+
const sess = await createWidgetSession(opts.publishableKey, opts.apiBase);
|
|
281
289
|
setSession(sess);
|
|
282
290
|
} catch (err) {
|
|
283
291
|
setState("error");
|
|
@@ -289,7 +297,7 @@ function useThunderPhone(opts) {
|
|
|
289
297
|
opts.onError?.({ error: "unknown", message: "Unable to connect." });
|
|
290
298
|
}
|
|
291
299
|
}
|
|
292
|
-
}, [opts.
|
|
300
|
+
}, [opts.publishableKey, opts.apiBase, state, opts.onError]);
|
|
293
301
|
const disconnect = (0, import_react3.useCallback)(() => {
|
|
294
302
|
handleDisconnect();
|
|
295
303
|
}, [handleDisconnect]);
|
|
@@ -323,8 +331,7 @@ function useThunderPhone(opts) {
|
|
|
323
331
|
// src/ThunderPhoneWidget.tsx
|
|
324
332
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
325
333
|
function ThunderPhoneWidget({
|
|
326
|
-
|
|
327
|
-
agentId,
|
|
334
|
+
publishableKey,
|
|
328
335
|
apiBase,
|
|
329
336
|
onConnect,
|
|
330
337
|
onDisconnect,
|
|
@@ -332,7 +339,7 @@ function ThunderPhoneWidget({
|
|
|
332
339
|
className,
|
|
333
340
|
ringtone
|
|
334
341
|
}) {
|
|
335
|
-
const phone = useThunderPhone({
|
|
342
|
+
const phone = useThunderPhone({ publishableKey, apiBase, onConnect, onDisconnect, onError, ringtone });
|
|
336
343
|
const handleClick = () => {
|
|
337
344
|
if (phone.state === "connected") {
|
|
338
345
|
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, 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"]}
|
|
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 publishableKey: string\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.publishableKey, 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.publishableKey, 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 let didSignalConnected = false\n const signalConnected = () => {\n if (didSignalConnected) return\n didSignalConnected = true\n onAgentConnected()\n }\n\n if (room.remoteParticipants.size > 0) {\n signalConnected()\n }\n\n const handleParticipantConnected = () => signalConnected()\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 signalConnected()\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 signalConnected()\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 publishableKey: string,\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': publishableKey,\n },\n body: JSON.stringify({}),\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 publishableKey,\n apiBase,\n onConnect,\n onDisconnect,\n onError,\n className,\n ringtone,\n}: ThunderPhoneWidgetProps) {\n const phone = useThunderPhone({ publishableKey, 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
|
@@ -82,10 +82,16 @@ function AudioHandler({ onAgentConnected, onDisconnected }) {
|
|
|
82
82
|
const room = useRoomContext();
|
|
83
83
|
const audioRef = useRef(null);
|
|
84
84
|
useEffect2(() => {
|
|
85
|
-
|
|
85
|
+
let didSignalConnected = false;
|
|
86
|
+
const signalConnected = () => {
|
|
87
|
+
if (didSignalConnected) return;
|
|
88
|
+
didSignalConnected = true;
|
|
86
89
|
onAgentConnected();
|
|
90
|
+
};
|
|
91
|
+
if (room.remoteParticipants.size > 0) {
|
|
92
|
+
signalConnected();
|
|
87
93
|
}
|
|
88
|
-
const handleParticipantConnected = () =>
|
|
94
|
+
const handleParticipantConnected = () => signalConnected();
|
|
89
95
|
const handleDisconnect = () => onDisconnected();
|
|
90
96
|
const attachTrack = (track, _pub, _participant) => {
|
|
91
97
|
if (track.kind === Track.Kind.Audio && audioRef.current) {
|
|
@@ -97,6 +103,7 @@ function AudioHandler({ onAgentConnected, onDisconnected }) {
|
|
|
97
103
|
audioRef.current.play().catch(() => {
|
|
98
104
|
});
|
|
99
105
|
}
|
|
106
|
+
signalConnected();
|
|
100
107
|
}
|
|
101
108
|
};
|
|
102
109
|
room.on(RoomEvent.ParticipantConnected, handleParticipantConnected);
|
|
@@ -114,6 +121,7 @@ function AudioHandler({ onAgentConnected, onDisconnected }) {
|
|
|
114
121
|
audioRef.current.srcObject = new MediaStream(initialTracks);
|
|
115
122
|
audioRef.current.play().catch(() => {
|
|
116
123
|
});
|
|
124
|
+
signalConnected();
|
|
117
125
|
}
|
|
118
126
|
return () => {
|
|
119
127
|
room.off(RoomEvent.ParticipantConnected, handleParticipantConnected);
|
|
@@ -133,15 +141,15 @@ var WidgetAPIError = class extends Error {
|
|
|
133
141
|
this.name = "WidgetAPIError";
|
|
134
142
|
}
|
|
135
143
|
};
|
|
136
|
-
async function createWidgetSession(
|
|
144
|
+
async function createWidgetSession(publishableKey, apiBase) {
|
|
137
145
|
const base = apiBase || DEFAULT_API_BASE;
|
|
138
146
|
const response = await fetch(`${base}/widget/session`, {
|
|
139
147
|
method: "POST",
|
|
140
148
|
headers: {
|
|
141
149
|
"Content-Type": "application/json",
|
|
142
|
-
"X-API-Key":
|
|
150
|
+
"X-API-Key": publishableKey
|
|
143
151
|
},
|
|
144
|
-
body: JSON.stringify({
|
|
152
|
+
body: JSON.stringify({})
|
|
145
153
|
});
|
|
146
154
|
if (!response.ok) {
|
|
147
155
|
const data = await response.json().catch(() => ({
|
|
@@ -250,7 +258,7 @@ function useThunderPhone(opts) {
|
|
|
250
258
|
}
|
|
251
259
|
);
|
|
252
260
|
try {
|
|
253
|
-
const sess = await createWidgetSession(opts.
|
|
261
|
+
const sess = await createWidgetSession(opts.publishableKey, opts.apiBase);
|
|
254
262
|
setSession(sess);
|
|
255
263
|
} catch (err) {
|
|
256
264
|
setState("error");
|
|
@@ -262,7 +270,7 @@ function useThunderPhone(opts) {
|
|
|
262
270
|
opts.onError?.({ error: "unknown", message: "Unable to connect." });
|
|
263
271
|
}
|
|
264
272
|
}
|
|
265
|
-
}, [opts.
|
|
273
|
+
}, [opts.publishableKey, opts.apiBase, state, opts.onError]);
|
|
266
274
|
const disconnect = useCallback(() => {
|
|
267
275
|
handleDisconnect();
|
|
268
276
|
}, [handleDisconnect]);
|
|
@@ -296,8 +304,7 @@ function useThunderPhone(opts) {
|
|
|
296
304
|
// src/ThunderPhoneWidget.tsx
|
|
297
305
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
298
306
|
function ThunderPhoneWidget({
|
|
299
|
-
|
|
300
|
-
agentId,
|
|
307
|
+
publishableKey,
|
|
301
308
|
apiBase,
|
|
302
309
|
onConnect,
|
|
303
310
|
onDisconnect,
|
|
@@ -305,7 +312,7 @@ function ThunderPhoneWidget({
|
|
|
305
312
|
className,
|
|
306
313
|
ringtone
|
|
307
314
|
}) {
|
|
308
|
-
const phone = useThunderPhone({
|
|
315
|
+
const phone = useThunderPhone({ publishableKey, apiBase, onConnect, onDisconnect, onError, ringtone });
|
|
309
316
|
const handleClick = () => {
|
|
310
317
|
if (phone.state === "connected") {
|
|
311
318
|
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, 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"]}
|
|
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 publishableKey: string\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.publishableKey, 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.publishableKey, 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 let didSignalConnected = false\n const signalConnected = () => {\n if (didSignalConnected) return\n didSignalConnected = true\n onAgentConnected()\n }\n\n if (room.remoteParticipants.size > 0) {\n signalConnected()\n }\n\n const handleParticipantConnected = () => signalConnected()\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 signalConnected()\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 signalConnected()\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 publishableKey: string,\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': publishableKey,\n },\n body: JSON.stringify({}),\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 publishableKey,\n apiBase,\n onConnect,\n onDisconnect,\n onError,\n className,\n ringtone,\n}: ThunderPhoneWidgetProps) {\n const phone = useThunderPhone({ publishableKey, 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"]}
|