@thunderphone/widget 0.4.2 → 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 +7 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7 -8
- package/dist/index.mjs.map +1 -1
- package/dist/mount.global.js +7 -8
- 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
|
@@ -168,15 +168,15 @@ var WidgetAPIError = class extends Error {
|
|
|
168
168
|
this.name = "WidgetAPIError";
|
|
169
169
|
}
|
|
170
170
|
};
|
|
171
|
-
async function createWidgetSession(
|
|
171
|
+
async function createWidgetSession(publishableKey, apiBase) {
|
|
172
172
|
const base = apiBase || DEFAULT_API_BASE;
|
|
173
173
|
const response = await fetch(`${base}/widget/session`, {
|
|
174
174
|
method: "POST",
|
|
175
175
|
headers: {
|
|
176
176
|
"Content-Type": "application/json",
|
|
177
|
-
"X-API-Key":
|
|
177
|
+
"X-API-Key": publishableKey
|
|
178
178
|
},
|
|
179
|
-
body: JSON.stringify({
|
|
179
|
+
body: JSON.stringify({})
|
|
180
180
|
});
|
|
181
181
|
if (!response.ok) {
|
|
182
182
|
const data = await response.json().catch(() => ({
|
|
@@ -285,7 +285,7 @@ function useThunderPhone(opts) {
|
|
|
285
285
|
}
|
|
286
286
|
);
|
|
287
287
|
try {
|
|
288
|
-
const sess = await createWidgetSession(opts.
|
|
288
|
+
const sess = await createWidgetSession(opts.publishableKey, opts.apiBase);
|
|
289
289
|
setSession(sess);
|
|
290
290
|
} catch (err) {
|
|
291
291
|
setState("error");
|
|
@@ -297,7 +297,7 @@ function useThunderPhone(opts) {
|
|
|
297
297
|
opts.onError?.({ error: "unknown", message: "Unable to connect." });
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
-
}, [opts.
|
|
300
|
+
}, [opts.publishableKey, opts.apiBase, state, opts.onError]);
|
|
301
301
|
const disconnect = (0, import_react3.useCallback)(() => {
|
|
302
302
|
handleDisconnect();
|
|
303
303
|
}, [handleDisconnect]);
|
|
@@ -331,8 +331,7 @@ function useThunderPhone(opts) {
|
|
|
331
331
|
// src/ThunderPhoneWidget.tsx
|
|
332
332
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
333
333
|
function ThunderPhoneWidget({
|
|
334
|
-
|
|
335
|
-
agentId,
|
|
334
|
+
publishableKey,
|
|
336
335
|
apiBase,
|
|
337
336
|
onConnect,
|
|
338
337
|
onDisconnect,
|
|
@@ -340,7 +339,7 @@ function ThunderPhoneWidget({
|
|
|
340
339
|
className,
|
|
341
340
|
ringtone
|
|
342
341
|
}) {
|
|
343
|
-
const phone = useThunderPhone({
|
|
342
|
+
const phone = useThunderPhone({ publishableKey, apiBase, onConnect, onDisconnect, onError, ringtone });
|
|
344
343
|
const handleClick = () => {
|
|
345
344
|
if (phone.state === "connected") {
|
|
346
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 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 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
|
@@ -141,15 +141,15 @@ var WidgetAPIError = class extends Error {
|
|
|
141
141
|
this.name = "WidgetAPIError";
|
|
142
142
|
}
|
|
143
143
|
};
|
|
144
|
-
async function createWidgetSession(
|
|
144
|
+
async function createWidgetSession(publishableKey, apiBase) {
|
|
145
145
|
const base = apiBase || DEFAULT_API_BASE;
|
|
146
146
|
const response = await fetch(`${base}/widget/session`, {
|
|
147
147
|
method: "POST",
|
|
148
148
|
headers: {
|
|
149
149
|
"Content-Type": "application/json",
|
|
150
|
-
"X-API-Key":
|
|
150
|
+
"X-API-Key": publishableKey
|
|
151
151
|
},
|
|
152
|
-
body: JSON.stringify({
|
|
152
|
+
body: JSON.stringify({})
|
|
153
153
|
});
|
|
154
154
|
if (!response.ok) {
|
|
155
155
|
const data = await response.json().catch(() => ({
|
|
@@ -258,7 +258,7 @@ function useThunderPhone(opts) {
|
|
|
258
258
|
}
|
|
259
259
|
);
|
|
260
260
|
try {
|
|
261
|
-
const sess = await createWidgetSession(opts.
|
|
261
|
+
const sess = await createWidgetSession(opts.publishableKey, opts.apiBase);
|
|
262
262
|
setSession(sess);
|
|
263
263
|
} catch (err) {
|
|
264
264
|
setState("error");
|
|
@@ -270,7 +270,7 @@ function useThunderPhone(opts) {
|
|
|
270
270
|
opts.onError?.({ error: "unknown", message: "Unable to connect." });
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
|
-
}, [opts.
|
|
273
|
+
}, [opts.publishableKey, opts.apiBase, state, opts.onError]);
|
|
274
274
|
const disconnect = useCallback(() => {
|
|
275
275
|
handleDisconnect();
|
|
276
276
|
}, [handleDisconnect]);
|
|
@@ -304,8 +304,7 @@ function useThunderPhone(opts) {
|
|
|
304
304
|
// src/ThunderPhoneWidget.tsx
|
|
305
305
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
306
306
|
function ThunderPhoneWidget({
|
|
307
|
-
|
|
308
|
-
agentId,
|
|
307
|
+
publishableKey,
|
|
309
308
|
apiBase,
|
|
310
309
|
onConnect,
|
|
311
310
|
onDisconnect,
|
|
@@ -313,7 +312,7 @@ function ThunderPhoneWidget({
|
|
|
313
312
|
className,
|
|
314
313
|
ringtone
|
|
315
314
|
}) {
|
|
316
|
-
const phone = useThunderPhone({
|
|
315
|
+
const phone = useThunderPhone({ publishableKey, apiBase, onConnect, onDisconnect, onError, ringtone });
|
|
317
316
|
const handleClick = () => {
|
|
318
317
|
if (phone.state === "connected") {
|
|
319
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 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 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"]}
|
package/dist/mount.global.js
CHANGED
|
@@ -57500,15 +57500,15 @@ var ThunderPhone = (() => {
|
|
|
57500
57500
|
this.name = "WidgetAPIError";
|
|
57501
57501
|
}
|
|
57502
57502
|
};
|
|
57503
|
-
async function createWidgetSession(
|
|
57503
|
+
async function createWidgetSession(publishableKey, apiBase) {
|
|
57504
57504
|
const base = apiBase || DEFAULT_API_BASE;
|
|
57505
57505
|
const response = await fetch(`${base}/widget/session`, {
|
|
57506
57506
|
method: "POST",
|
|
57507
57507
|
headers: {
|
|
57508
57508
|
"Content-Type": "application/json",
|
|
57509
|
-
"X-API-Key":
|
|
57509
|
+
"X-API-Key": publishableKey
|
|
57510
57510
|
},
|
|
57511
|
-
body: JSON.stringify({
|
|
57511
|
+
body: JSON.stringify({})
|
|
57512
57512
|
});
|
|
57513
57513
|
if (!response.ok) {
|
|
57514
57514
|
const data = await response.json().catch(() => ({
|
|
@@ -57617,7 +57617,7 @@ var ThunderPhone = (() => {
|
|
|
57617
57617
|
}
|
|
57618
57618
|
);
|
|
57619
57619
|
try {
|
|
57620
|
-
const sess = await createWidgetSession(opts.
|
|
57620
|
+
const sess = await createWidgetSession(opts.publishableKey, opts.apiBase);
|
|
57621
57621
|
setSession(sess);
|
|
57622
57622
|
} catch (err) {
|
|
57623
57623
|
setState("error");
|
|
@@ -57629,7 +57629,7 @@ var ThunderPhone = (() => {
|
|
|
57629
57629
|
opts.onError?.({ error: "unknown", message: "Unable to connect." });
|
|
57630
57630
|
}
|
|
57631
57631
|
}
|
|
57632
|
-
}, [opts.
|
|
57632
|
+
}, [opts.publishableKey, opts.apiBase, state, opts.onError]);
|
|
57633
57633
|
const disconnect = (0, import_react3.useCallback)(() => {
|
|
57634
57634
|
handleDisconnect();
|
|
57635
57635
|
}, [handleDisconnect]);
|
|
@@ -57663,8 +57663,7 @@ var ThunderPhone = (() => {
|
|
|
57663
57663
|
// src/ThunderPhoneWidget.tsx
|
|
57664
57664
|
var import_jsx_runtime4 = __toESM(require_jsx_runtime());
|
|
57665
57665
|
function ThunderPhoneWidget({
|
|
57666
|
-
|
|
57667
|
-
agentId,
|
|
57666
|
+
publishableKey,
|
|
57668
57667
|
apiBase,
|
|
57669
57668
|
onConnect,
|
|
57670
57669
|
onDisconnect,
|
|
@@ -57672,7 +57671,7 @@ var ThunderPhone = (() => {
|
|
|
57672
57671
|
className,
|
|
57673
57672
|
ringtone
|
|
57674
57673
|
}) {
|
|
57675
|
-
const phone = useThunderPhone({
|
|
57674
|
+
const phone = useThunderPhone({ publishableKey, apiBase, onConnect, onDisconnect, onError, ringtone });
|
|
57676
57675
|
const handleClick = () => {
|
|
57677
57676
|
if (phone.state === "connected") {
|
|
57678
57677
|
phone.disconnect();
|