@thunderphone/widget 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/dist/index.d.mts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +63 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -3
- package/dist/index.mjs.map +1 -1
- package/dist/mount.global.js +67 -6
- package/dist/mount.global.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ function App() {
|
|
|
42
42
|
| `onDisconnect` | `() => void` | No | Called when the session ends |
|
|
43
43
|
| `onError` | `(error) => void` | No | Called on errors. Error has `error` (code) and `message` fields |
|
|
44
44
|
| `className` | `string` | No | Additional CSS class for the widget container |
|
|
45
|
+
| `ringtone` | `boolean \| string` | No | Play a ringtone while connecting. `true` for default, or a URL for custom audio. Disabled by default |
|
|
45
46
|
|
|
46
47
|
## Headless Hook
|
|
47
48
|
|
|
@@ -91,6 +92,7 @@ function CustomCallButton() {
|
|
|
91
92
|
| `onConnect` | `() => void` | No | Called when the voice session connects |
|
|
92
93
|
| `onDisconnect` | `() => void` | No | Called when the session ends |
|
|
93
94
|
| `onError` | `(error) => void` | No | Called on errors |
|
|
95
|
+
| `ringtone` | `boolean \| string` | No | Play a ringtone while connecting. `true` for default, or a URL for custom audio |
|
|
94
96
|
|
|
95
97
|
### Hook Return Value
|
|
96
98
|
|
|
@@ -105,6 +107,42 @@ function CustomCallButton() {
|
|
|
105
107
|
| `agentName` | `string \| undefined` | Name of the connected agent |
|
|
106
108
|
| `audio` | `ReactNode` | Invisible element — must be rendered in the tree |
|
|
107
109
|
|
|
110
|
+
## Ringtone
|
|
111
|
+
|
|
112
|
+
Play a ringing sound while the widget connects, to simulate a phone call:
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
<ThunderPhoneWidget
|
|
116
|
+
apiKey="pk_live_your_publishable_key"
|
|
117
|
+
agentId={123}
|
|
118
|
+
ringtone={true}
|
|
119
|
+
/>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use a custom audio file by passing a URL:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<ThunderPhoneWidget
|
|
126
|
+
apiKey="pk_live_your_publishable_key"
|
|
127
|
+
agentId={123}
|
|
128
|
+
ringtone="https://example.com/my-ringtone.mp3"
|
|
129
|
+
/>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The ringtone loops during the `connecting` state and fades out when the agent connects. It is opt-in and disabled by default.
|
|
133
|
+
|
|
134
|
+
The headless hook accepts the same option:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
const phone = useThunderPhone({ apiKey, agentId, ringtone: true })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
And the script-tag mount API:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
ThunderPhone.mount({ element: '#thunderphone', apiKey: '...', agentId: 123, ringtone: true })
|
|
144
|
+
```
|
|
145
|
+
|
|
108
146
|
## Styling
|
|
109
147
|
|
|
110
148
|
The widget uses plain CSS with `tp-` prefixed classes, so it won't conflict with your styles. You can override any of these classes:
|
package/dist/index.d.mts
CHANGED
|
@@ -14,9 +14,16 @@ interface ThunderPhoneWidgetProps {
|
|
|
14
14
|
onDisconnect?: () => void;
|
|
15
15
|
onError?: (error: WidgetError) => void;
|
|
16
16
|
className?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Play a ringtone while connecting. Opt-in — disabled by default.
|
|
19
|
+
* - `true` plays the default ringtone from the ThunderPhone CDN.
|
|
20
|
+
* - A string URL plays a custom audio file.
|
|
21
|
+
* - `false` or omitted disables the ringtone.
|
|
22
|
+
*/
|
|
23
|
+
ringtone?: boolean | string;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
declare function ThunderPhoneWidget({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, className, }: ThunderPhoneWidgetProps): react_jsx_runtime.JSX.Element;
|
|
26
|
+
declare function ThunderPhoneWidget({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, className, ringtone, }: ThunderPhoneWidgetProps): react_jsx_runtime.JSX.Element;
|
|
20
27
|
|
|
21
28
|
interface UseThunderPhoneOptions {
|
|
22
29
|
apiKey: string;
|
|
@@ -28,6 +35,13 @@ interface UseThunderPhoneOptions {
|
|
|
28
35
|
error: string;
|
|
29
36
|
message: string;
|
|
30
37
|
}) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Play a ringtone while connecting. Opt-in — disabled by default.
|
|
40
|
+
* - `true` plays the default ringtone from the ThunderPhone CDN.
|
|
41
|
+
* - A string URL plays a custom audio file.
|
|
42
|
+
* - `false` or omitted disables the ringtone.
|
|
43
|
+
*/
|
|
44
|
+
ringtone?: boolean | string;
|
|
31
45
|
}
|
|
32
46
|
interface UseThunderPhoneReturn {
|
|
33
47
|
state: WidgetState;
|
package/dist/index.d.ts
CHANGED
|
@@ -14,9 +14,16 @@ interface ThunderPhoneWidgetProps {
|
|
|
14
14
|
onDisconnect?: () => void;
|
|
15
15
|
onError?: (error: WidgetError) => void;
|
|
16
16
|
className?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Play a ringtone while connecting. Opt-in — disabled by default.
|
|
19
|
+
* - `true` plays the default ringtone from the ThunderPhone CDN.
|
|
20
|
+
* - A string URL plays a custom audio file.
|
|
21
|
+
* - `false` or omitted disables the ringtone.
|
|
22
|
+
*/
|
|
23
|
+
ringtone?: boolean | string;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
declare function ThunderPhoneWidget({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, className, }: ThunderPhoneWidgetProps): react_jsx_runtime.JSX.Element;
|
|
26
|
+
declare function ThunderPhoneWidget({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, className, ringtone, }: ThunderPhoneWidgetProps): react_jsx_runtime.JSX.Element;
|
|
20
27
|
|
|
21
28
|
interface UseThunderPhoneOptions {
|
|
22
29
|
apiKey: string;
|
|
@@ -28,6 +35,13 @@ interface UseThunderPhoneOptions {
|
|
|
28
35
|
error: string;
|
|
29
36
|
message: string;
|
|
30
37
|
}) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Play a ringtone while connecting. Opt-in — disabled by default.
|
|
40
|
+
* - `true` plays the default ringtone from the ThunderPhone CDN.
|
|
41
|
+
* - A string URL plays a custom audio file.
|
|
42
|
+
* - `false` or omitted disables the ringtone.
|
|
43
|
+
*/
|
|
44
|
+
ringtone?: boolean | string;
|
|
31
45
|
}
|
|
32
46
|
interface UseThunderPhoneReturn {
|
|
33
47
|
state: WidgetState;
|
package/dist/index.js
CHANGED
|
@@ -181,11 +181,64 @@ async function createWidgetSession(apiKey, agentId, apiBase) {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
// src/useThunderPhone.ts
|
|
184
|
+
var DEFAULT_RINGTONE_URL = "https://cdn.thunderphone.com/widget/assets/ringtone-default.mp3";
|
|
185
|
+
function resolveRingtoneUrl(ringtone) {
|
|
186
|
+
if (ringtone === true || ringtone === "default") return DEFAULT_RINGTONE_URL;
|
|
187
|
+
if (typeof ringtone === "string" && ringtone.length > 0) return ringtone;
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
184
190
|
function useThunderPhone(opts) {
|
|
185
191
|
const [state, setState] = (0, import_react3.useState)("idle");
|
|
186
192
|
const [session, setSession] = (0, import_react3.useState)(null);
|
|
187
193
|
const [muted, setMuted] = (0, import_react3.useState)(false);
|
|
188
194
|
const [error, setError] = (0, import_react3.useState)();
|
|
195
|
+
const ringtoneUrl = resolveRingtoneUrl(opts.ringtone);
|
|
196
|
+
const ringtoneRef = (0, import_react3.useRef)(null);
|
|
197
|
+
(0, import_react3.useEffect)(() => {
|
|
198
|
+
if (!ringtoneUrl) {
|
|
199
|
+
ringtoneRef.current = null;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const audio2 = new Audio(ringtoneUrl);
|
|
203
|
+
audio2.loop = true;
|
|
204
|
+
audio2.preload = "auto";
|
|
205
|
+
audio2.volume = 1;
|
|
206
|
+
ringtoneRef.current = audio2;
|
|
207
|
+
return () => {
|
|
208
|
+
audio2.pause();
|
|
209
|
+
audio2.src = "";
|
|
210
|
+
ringtoneRef.current = null;
|
|
211
|
+
};
|
|
212
|
+
}, [ringtoneUrl]);
|
|
213
|
+
(0, import_react3.useEffect)(() => {
|
|
214
|
+
const audio2 = ringtoneRef.current;
|
|
215
|
+
if (!audio2) return;
|
|
216
|
+
let fadeInterval;
|
|
217
|
+
if (state === "connecting") {
|
|
218
|
+
audio2.currentTime = 0;
|
|
219
|
+
audio2.volume = 1;
|
|
220
|
+
audio2.play().catch(() => {
|
|
221
|
+
});
|
|
222
|
+
} else {
|
|
223
|
+
if (!audio2.paused) {
|
|
224
|
+
fadeInterval = setInterval(() => {
|
|
225
|
+
const next = audio2.volume - 0.1;
|
|
226
|
+
if (next <= 0) {
|
|
227
|
+
clearInterval(fadeInterval);
|
|
228
|
+
fadeInterval = void 0;
|
|
229
|
+
audio2.pause();
|
|
230
|
+
audio2.currentTime = 0;
|
|
231
|
+
audio2.volume = 1;
|
|
232
|
+
} else {
|
|
233
|
+
audio2.volume = next;
|
|
234
|
+
}
|
|
235
|
+
}, 20);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return () => {
|
|
239
|
+
if (fadeInterval) clearInterval(fadeInterval);
|
|
240
|
+
};
|
|
241
|
+
}, [state]);
|
|
189
242
|
const handleDisconnect = (0, import_react3.useCallback)(() => {
|
|
190
243
|
setState("disconnected");
|
|
191
244
|
setSession(null);
|
|
@@ -201,6 +254,13 @@ function useThunderPhone(opts) {
|
|
|
201
254
|
if (state === "connecting" || state === "connected") return;
|
|
202
255
|
setState("connecting");
|
|
203
256
|
setError(void 0);
|
|
257
|
+
navigator.mediaDevices.getUserMedia({ audio: true }).then(
|
|
258
|
+
(stream) => {
|
|
259
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
260
|
+
},
|
|
261
|
+
() => {
|
|
262
|
+
}
|
|
263
|
+
);
|
|
204
264
|
try {
|
|
205
265
|
const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase);
|
|
206
266
|
setSession(sess);
|
|
@@ -254,9 +314,10 @@ function ThunderPhoneWidget({
|
|
|
254
314
|
onConnect,
|
|
255
315
|
onDisconnect,
|
|
256
316
|
onError,
|
|
257
|
-
className
|
|
317
|
+
className,
|
|
318
|
+
ringtone
|
|
258
319
|
}) {
|
|
259
|
-
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError });
|
|
320
|
+
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, ringtone });
|
|
260
321
|
const handleClick = () => {
|
|
261
322
|
if (phone.state === "connected") {
|
|
262
323
|
phone.disconnect();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/WidgetButton.tsx","../src/WidgetStatus.tsx","../src/useThunderPhone.ts","../src/AudioHandler.tsx","../src/api.ts","../src/ThunderPhoneWidget.tsx"],"sourcesContent":["import './style.css'\n\nexport { ThunderPhoneWidget } from './ThunderPhoneWidget'\nexport { useThunderPhone } from './useThunderPhone'\nexport type { UseThunderPhoneOptions, UseThunderPhoneReturn } from './useThunderPhone'\nexport type { ThunderPhoneWidgetProps, WidgetError, WidgetState } from './types'\n","import type { WidgetState } from './types'\n\ninterface WidgetButtonProps {\n state: WidgetState\n muted: boolean\n onClick: () => void\n onMuteToggle: () => void\n}\n\nexport function WidgetButton({ state, muted, onClick, onMuteToggle }: WidgetButtonProps) {\n if (state === 'connected') {\n return (\n <div className=\"tp-button-group\">\n <button className=\"tp-button tp-button--mute\" onClick={onMuteToggle} type=\"button\">\n {muted ? (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\" />\n <path d=\"M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6\" />\n <path d=\"M17 16.95A7 7 0 0 1 5 12v-2m14 0v2c0 .76-.13 1.49-.36 2.18\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n <button className=\"tp-button tp-button--end\" onClick={onClick} type=\"button\">\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" />\n </svg>\n </button>\n </div>\n )\n }\n\n return (\n <button\n className={`tp-button tp-button--start ${state === 'connecting' ? 'tp-button--loading' : ''}`}\n onClick={onClick}\n disabled={state === 'connecting'}\n type=\"button\"\n >\n {state === 'connecting' ? (\n <svg className=\"tp-icon tp-spin\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n )\n}\n","import { useEffect, useState } from 'react'\nimport type { WidgetState } from './types'\n\ninterface WidgetStatusProps {\n state: WidgetState\n agentName: string | null\n errorMessage?: string\n}\n\nexport function WidgetStatus({ state, agentName, errorMessage }: WidgetStatusProps) {\n const [elapsed, setElapsed] = useState(0)\n\n useEffect(() => {\n if (state !== 'connected') {\n setElapsed(0)\n return\n }\n const interval = setInterval(() => setElapsed(s => s + 1), 1000)\n return () => clearInterval(interval)\n }, [state])\n\n const formatTime = (seconds: number) => {\n const m = Math.floor(seconds / 60)\n const s = seconds % 60\n return `${m}:${s.toString().padStart(2, '0')}`\n }\n\n const statusText: Record<WidgetState, string> = {\n idle: 'Ready',\n connecting: 'Connecting...',\n connected: formatTime(elapsed),\n disconnected: 'Disconnected',\n error: 'Unable to connect',\n }\n\n return (\n <div className=\"tp-status\">\n {agentName && <div className=\"tp-status__name\">{agentName}</div>}\n <div className={`tp-status__text tp-status--${state}`}>\n {state === 'connected' && <span className=\"tp-status__dot\" />}\n {errorMessage && state === 'error' ? errorMessage : statusText[state]}\n </div>\n </div>\n )\n}\n","import { useCallback, useState, type ReactNode, createElement } from 'react'\nimport { LiveKitRoom } from '@livekit/components-react'\nimport { AudioHandler } from './AudioHandler'\nimport { createWidgetSession, WidgetAPIError } from './api'\nimport type { WidgetState, WidgetSessionResponse } from './types'\n\nexport interface UseThunderPhoneOptions {\n apiKey: string\n agentId: number\n apiBase?: string\n onConnect?: () => void\n onDisconnect?: () => void\n onError?: (error: { error: string; message: string }) => void\n}\n\nexport interface UseThunderPhoneReturn {\n state: WidgetState\n connect: () => void\n disconnect: () => void\n toggleMute: () => void\n isMuted: boolean\n error: string | undefined\n agentName: string | undefined\n /** Render this somewhere in your tree — it's invisible but handles audio. */\n audio: ReactNode\n}\n\nexport function useThunderPhone(opts: UseThunderPhoneOptions): UseThunderPhoneReturn {\n const [state, setState] = useState<WidgetState>('idle')\n const [session, setSession] = useState<WidgetSessionResponse | null>(null)\n const [muted, setMuted] = useState(false)\n const [error, setError] = useState<string | undefined>()\n\n const handleDisconnect = useCallback(() => {\n setState('disconnected')\n setSession(null)\n setMuted(false)\n opts.onDisconnect?.()\n setTimeout(() => setState('idle'), 1500)\n }, [opts.onDisconnect])\n\n const handleAgentConnected = useCallback(() => {\n setState('connected')\n opts.onConnect?.()\n }, [opts.onConnect])\n\n const connect = useCallback(async () => {\n if (state === 'connecting' || state === 'connected') return\n setState('connecting')\n setError(undefined)\n try {\n const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase)\n setSession(sess)\n } catch (err) {\n setState('error')\n if (err instanceof WidgetAPIError) {\n setError(err.message)\n opts.onError?.({ error: err.code, message: err.message })\n } else {\n setError('Unable to connect.')\n opts.onError?.({ error: 'unknown', message: 'Unable to connect.' })\n }\n }\n }, [opts.apiKey, opts.agentId, opts.apiBase, state, opts.onError])\n\n const disconnect = useCallback(() => {\n handleDisconnect()\n }, [handleDisconnect])\n\n const toggleMute = useCallback(() => setMuted(m => !m), [])\n\n const audio: ReactNode = session\n ? createElement(\n LiveKitRoom,\n {\n token: session.token,\n serverUrl: session.server_url,\n audio: !muted,\n video: false,\n connect: true,\n },\n createElement(AudioHandler, {\n onAgentConnected: handleAgentConnected,\n onDisconnected: handleDisconnect,\n }),\n )\n : null\n\n return {\n state,\n connect,\n disconnect,\n toggleMute,\n isMuted: muted,\n error,\n agentName: session?.agent_name,\n audio,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport { useRoomContext } from '@livekit/components-react'\nimport { RoomEvent, Track, type RemoteTrackPublication, type RemoteParticipant } from 'livekit-client'\n\ninterface AudioHandlerProps {\n onAgentConnected: () => void\n onDisconnected: () => void\n}\n\nexport function AudioHandler({ onAgentConnected, onDisconnected }: AudioHandlerProps) {\n const room = useRoomContext()\n const audioRef = useRef<HTMLAudioElement>(null)\n\n useEffect(() => {\n if (room.remoteParticipants.size > 0) {\n onAgentConnected()\n }\n\n const handleParticipantConnected = () => onAgentConnected()\n const handleDisconnect = () => onDisconnected()\n\n // The bot publishes multiple audio tracks (tts, bg noise, hold music).\n // Add each track to a single MediaStream so the browser mixes them.\n const attachTrack = (\n track: { kind: Track.Kind; mediaStreamTrack: MediaStreamTrack },\n _pub: RemoteTrackPublication,\n _participant: RemoteParticipant,\n ) => {\n if (track.kind === Track.Kind.Audio && audioRef.current) {\n const existing = audioRef.current.srcObject as MediaStream | null\n if (existing) {\n existing.addTrack(track.mediaStreamTrack)\n } else {\n audioRef.current.srcObject = new MediaStream([track.mediaStreamTrack])\n audioRef.current.play().catch(() => {})\n }\n }\n }\n\n room.on(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.on(RoomEvent.Disconnected, handleDisconnect)\n room.on(RoomEvent.TrackSubscribed, attachTrack)\n\n const initialTracks: MediaStreamTrack[] = []\n for (const participant of room.remoteParticipants.values()) {\n for (const pub of participant.trackPublications.values()) {\n if (pub.track && pub.isSubscribed && pub.track.kind === Track.Kind.Audio) {\n initialTracks.push(pub.track.mediaStreamTrack)\n }\n }\n }\n if (initialTracks.length > 0 && audioRef.current) {\n audioRef.current.srcObject = new MediaStream(initialTracks)\n audioRef.current.play().catch(() => {})\n }\n\n return () => {\n room.off(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.off(RoomEvent.Disconnected, handleDisconnect)\n room.off(RoomEvent.TrackSubscribed, attachTrack)\n }\n }, [room, onAgentConnected, onDisconnected])\n\n return <audio ref={audioRef} autoPlay />\n}\n","import type { WidgetSessionResponse, WidgetError } from './types'\n\nconst DEFAULT_API_BASE = 'https://api.thunderphone.com/v1'\n\nexport class WidgetAPIError extends Error {\n constructor(public code: string, message: string) {\n super(message)\n this.name = 'WidgetAPIError'\n }\n}\n\nexport async function createWidgetSession(\n apiKey: string,\n agentId: number,\n apiBase?: string,\n): Promise<WidgetSessionResponse> {\n const base = apiBase || DEFAULT_API_BASE\n const response = await fetch(`${base}/widget/session`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': apiKey,\n },\n body: JSON.stringify({ agent_id: agentId }),\n })\n\n if (!response.ok) {\n const data: WidgetError = await response.json().catch(() => ({\n error: 'unknown',\n message: 'Unable to connect.',\n }))\n throw new WidgetAPIError(data.error, data.message)\n }\n\n return response.json()\n}\n","import { WidgetButton } from './WidgetButton'\nimport { WidgetStatus } from './WidgetStatus'\nimport { useThunderPhone } from './useThunderPhone'\nimport type { ThunderPhoneWidgetProps } from './types'\n\nexport function ThunderPhoneWidget({\n apiKey,\n agentId,\n apiBase,\n onConnect,\n onDisconnect,\n onError,\n className,\n}: ThunderPhoneWidgetProps) {\n const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError })\n\n const handleClick = () => {\n if (phone.state === 'connected') {\n phone.disconnect()\n } else if (phone.state === 'idle' || phone.state === 'error' || phone.state === 'disconnected') {\n phone.connect()\n }\n }\n\n return (\n <div className={`tp-widget ${className || ''}`}>\n <WidgetStatus\n state={phone.state}\n agentName={phone.agentName || null}\n errorMessage={phone.error}\n />\n <WidgetButton\n state={phone.state}\n muted={phone.isMuted}\n onClick={handleClick}\n onMuteToggle={phone.toggleMute}\n />\n {phone.audio}\n </div>\n )\n}\n"],"mappings":"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","names":["import_jsx_runtime","import_react","import_components_react","import_react","import_jsx_runtime","import_jsx_runtime"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/WidgetButton.tsx","../src/WidgetStatus.tsx","../src/useThunderPhone.ts","../src/AudioHandler.tsx","../src/api.ts","../src/ThunderPhoneWidget.tsx"],"sourcesContent":["import './style.css'\n\nexport { ThunderPhoneWidget } from './ThunderPhoneWidget'\nexport { useThunderPhone } from './useThunderPhone'\nexport type { UseThunderPhoneOptions, UseThunderPhoneReturn } from './useThunderPhone'\nexport type { ThunderPhoneWidgetProps, WidgetError, WidgetState } from './types'\n","import type { WidgetState } from './types'\n\ninterface WidgetButtonProps {\n state: WidgetState\n muted: boolean\n onClick: () => void\n onMuteToggle: () => void\n}\n\nexport function WidgetButton({ state, muted, onClick, onMuteToggle }: WidgetButtonProps) {\n if (state === 'connected') {\n return (\n <div className=\"tp-button-group\">\n <button className=\"tp-button tp-button--mute\" onClick={onMuteToggle} type=\"button\">\n {muted ? (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\" />\n <path d=\"M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6\" />\n <path d=\"M17 16.95A7 7 0 0 1 5 12v-2m14 0v2c0 .76-.13 1.49-.36 2.18\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n <button className=\"tp-button tp-button--end\" onClick={onClick} type=\"button\">\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" />\n </svg>\n </button>\n </div>\n )\n }\n\n return (\n <button\n className={`tp-button tp-button--start ${state === 'connecting' ? 'tp-button--loading' : ''}`}\n onClick={onClick}\n disabled={state === 'connecting'}\n type=\"button\"\n >\n {state === 'connecting' ? (\n <svg className=\"tp-icon tp-spin\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n )\n}\n","import { useEffect, useState } from 'react'\nimport type { WidgetState } from './types'\n\ninterface WidgetStatusProps {\n state: WidgetState\n agentName: string | null\n errorMessage?: string\n}\n\nexport function WidgetStatus({ state, agentName, errorMessage }: WidgetStatusProps) {\n const [elapsed, setElapsed] = useState(0)\n\n useEffect(() => {\n if (state !== 'connected') {\n setElapsed(0)\n return\n }\n const interval = setInterval(() => setElapsed(s => s + 1), 1000)\n return () => clearInterval(interval)\n }, [state])\n\n const formatTime = (seconds: number) => {\n const m = Math.floor(seconds / 60)\n const s = seconds % 60\n return `${m}:${s.toString().padStart(2, '0')}`\n }\n\n const statusText: Record<WidgetState, string> = {\n idle: 'Ready',\n connecting: 'Connecting...',\n connected: formatTime(elapsed),\n disconnected: 'Disconnected',\n error: 'Unable to connect',\n }\n\n return (\n <div className=\"tp-status\">\n {agentName && <div className=\"tp-status__name\">{agentName}</div>}\n <div className={`tp-status__text tp-status--${state}`}>\n {state === 'connected' && <span className=\"tp-status__dot\" />}\n {errorMessage && state === 'error' ? errorMessage : statusText[state]}\n </div>\n </div>\n )\n}\n","import { useCallback, useEffect, useRef, useState, type ReactNode, createElement } from 'react'\nimport { LiveKitRoom } from '@livekit/components-react'\nimport { AudioHandler } from './AudioHandler'\nimport { createWidgetSession, WidgetAPIError } from './api'\nimport type { WidgetState, WidgetSessionResponse } from './types'\n\nconst DEFAULT_RINGTONE_URL = 'https://cdn.thunderphone.com/widget/assets/ringtone-default.mp3'\n\nexport interface UseThunderPhoneOptions {\n apiKey: string\n agentId: number\n apiBase?: string\n onConnect?: () => void\n onDisconnect?: () => void\n onError?: (error: { error: string; message: string }) => void\n /**\n * Play a ringtone while connecting. Opt-in — disabled by default.\n * - `true` plays the default ringtone from the ThunderPhone CDN.\n * - A string URL plays a custom audio file.\n * - `false` or omitted disables the ringtone.\n */\n ringtone?: boolean | string\n}\n\nexport interface UseThunderPhoneReturn {\n state: WidgetState\n connect: () => void\n disconnect: () => void\n toggleMute: () => void\n isMuted: boolean\n error: string | undefined\n agentName: string | undefined\n /** Render this somewhere in your tree — it's invisible but handles audio. */\n audio: ReactNode\n}\n\n/** Resolve the ringtone option to a URL or null. */\nfunction resolveRingtoneUrl(ringtone: boolean | string | undefined): string | null {\n if (ringtone === true || ringtone === 'default') return DEFAULT_RINGTONE_URL\n if (typeof ringtone === 'string' && ringtone.length > 0) return ringtone\n return null\n}\n\nexport function useThunderPhone(opts: UseThunderPhoneOptions): UseThunderPhoneReturn {\n const [state, setState] = useState<WidgetState>('idle')\n const [session, setSession] = useState<WidgetSessionResponse | null>(null)\n const [muted, setMuted] = useState(false)\n const [error, setError] = useState<string | undefined>()\n\n // --- Ringtone management ---\n const ringtoneUrl = resolveRingtoneUrl(opts.ringtone)\n const ringtoneRef = useRef<HTMLAudioElement | null>(null)\n\n // Preload the ringtone audio element once when configured.\n useEffect(() => {\n if (!ringtoneUrl) {\n ringtoneRef.current = null\n return\n }\n const audio = new Audio(ringtoneUrl)\n audio.loop = true\n audio.preload = 'auto'\n audio.volume = 1.0\n ringtoneRef.current = audio\n return () => {\n audio.pause()\n audio.src = ''\n ringtoneRef.current = null\n }\n }, [ringtoneUrl])\n\n // Start/stop ringtone based on state.\n useEffect(() => {\n const audio = ringtoneRef.current\n if (!audio) return\n\n let fadeInterval: ReturnType<typeof setInterval> | undefined\n\n if (state === 'connecting') {\n // Reset to start and play (catch handles browsers that block autoplay\n // before a user gesture — unlikely here since connect() is click-driven).\n audio.currentTime = 0\n audio.volume = 1.0\n audio.play().catch(() => {})\n } else {\n // Fade out briefly (~200ms) for a smooth stop.\n if (!audio.paused) {\n fadeInterval = setInterval(() => {\n const next = audio.volume - 0.1\n if (next <= 0) {\n clearInterval(fadeInterval)\n fadeInterval = undefined\n audio.pause()\n audio.currentTime = 0\n audio.volume = 1.0\n } else {\n audio.volume = next\n }\n }, 20) // 10 steps * 20ms = 200ms fade\n }\n }\n\n // Clean up any in-progress fade if state changes again before it completes.\n return () => {\n if (fadeInterval) clearInterval(fadeInterval)\n }\n }, [state])\n\n const handleDisconnect = useCallback(() => {\n setState('disconnected')\n setSession(null)\n setMuted(false)\n opts.onDisconnect?.()\n setTimeout(() => setState('idle'), 1500)\n }, [opts.onDisconnect])\n\n const handleAgentConnected = useCallback(() => {\n setState('connected')\n opts.onConnect?.()\n }, [opts.onConnect])\n\n const connect = useCallback(async () => {\n if (state === 'connecting' || state === 'connected') return\n setState('connecting')\n setError(undefined)\n // Warm up mic permission in the background so the browser prompt (if\n // needed) overlaps with the API call. This is fire-and-forget: if the\n // session request fails we simply discard the stream without the user\n // noticing an unnecessary permission prompt — getUserMedia only shows\n // the prompt once per origin, so subsequent calls are instant.\n navigator.mediaDevices.getUserMedia({ audio: true }).then(\n (stream) => { stream.getTracks().forEach((t) => t.stop()) },\n () => {},\n )\n\n try {\n const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase)\n setSession(sess)\n } catch (err) {\n setState('error')\n if (err instanceof WidgetAPIError) {\n setError(err.message)\n opts.onError?.({ error: err.code, message: err.message })\n } else {\n setError('Unable to connect.')\n opts.onError?.({ error: 'unknown', message: 'Unable to connect.' })\n }\n }\n }, [opts.apiKey, opts.agentId, opts.apiBase, state, opts.onError])\n\n const disconnect = useCallback(() => {\n handleDisconnect()\n }, [handleDisconnect])\n\n const toggleMute = useCallback(() => setMuted(m => !m), [])\n\n const audio: ReactNode = session\n ? createElement(\n LiveKitRoom,\n {\n token: session.token,\n serverUrl: session.server_url,\n audio: !muted,\n video: false,\n connect: true,\n },\n createElement(AudioHandler, {\n onAgentConnected: handleAgentConnected,\n onDisconnected: handleDisconnect,\n }),\n )\n : null\n\n return {\n state,\n connect,\n disconnect,\n toggleMute,\n isMuted: muted,\n error,\n agentName: session?.agent_name,\n audio,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport { useRoomContext } from '@livekit/components-react'\nimport { RoomEvent, Track, type RemoteTrackPublication, type RemoteParticipant } from 'livekit-client'\n\ninterface AudioHandlerProps {\n onAgentConnected: () => void\n onDisconnected: () => void\n}\n\nexport function AudioHandler({ onAgentConnected, onDisconnected }: AudioHandlerProps) {\n const room = useRoomContext()\n const audioRef = useRef<HTMLAudioElement>(null)\n\n useEffect(() => {\n if (room.remoteParticipants.size > 0) {\n onAgentConnected()\n }\n\n const handleParticipantConnected = () => onAgentConnected()\n const handleDisconnect = () => onDisconnected()\n\n // The bot publishes multiple audio tracks (tts, bg noise, hold music).\n // Add each track to a single MediaStream so the browser mixes them.\n const attachTrack = (\n track: { kind: Track.Kind; mediaStreamTrack: MediaStreamTrack },\n _pub: RemoteTrackPublication,\n _participant: RemoteParticipant,\n ) => {\n if (track.kind === Track.Kind.Audio && audioRef.current) {\n const existing = audioRef.current.srcObject as MediaStream | null\n if (existing) {\n existing.addTrack(track.mediaStreamTrack)\n } else {\n audioRef.current.srcObject = new MediaStream([track.mediaStreamTrack])\n audioRef.current.play().catch(() => {})\n }\n }\n }\n\n room.on(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.on(RoomEvent.Disconnected, handleDisconnect)\n room.on(RoomEvent.TrackSubscribed, attachTrack)\n\n const initialTracks: MediaStreamTrack[] = []\n for (const participant of room.remoteParticipants.values()) {\n for (const pub of participant.trackPublications.values()) {\n if (pub.track && pub.isSubscribed && pub.track.kind === Track.Kind.Audio) {\n initialTracks.push(pub.track.mediaStreamTrack)\n }\n }\n }\n if (initialTracks.length > 0 && audioRef.current) {\n audioRef.current.srcObject = new MediaStream(initialTracks)\n audioRef.current.play().catch(() => {})\n }\n\n return () => {\n room.off(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.off(RoomEvent.Disconnected, handleDisconnect)\n room.off(RoomEvent.TrackSubscribed, attachTrack)\n }\n }, [room, onAgentConnected, onDisconnected])\n\n return <audio ref={audioRef} autoPlay />\n}\n","import type { WidgetSessionResponse, WidgetError } from './types'\n\nconst DEFAULT_API_BASE = 'https://api.thunderphone.com/v1'\n\nexport class WidgetAPIError extends Error {\n constructor(public code: string, message: string) {\n super(message)\n this.name = 'WidgetAPIError'\n }\n}\n\nexport async function createWidgetSession(\n apiKey: string,\n agentId: number,\n apiBase?: string,\n): Promise<WidgetSessionResponse> {\n const base = apiBase || DEFAULT_API_BASE\n const response = await fetch(`${base}/widget/session`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': apiKey,\n },\n body: JSON.stringify({ agent_id: agentId }),\n })\n\n if (!response.ok) {\n const data: WidgetError = await response.json().catch(() => ({\n error: 'unknown',\n message: 'Unable to connect.',\n }))\n throw new WidgetAPIError(data.error, data.message)\n }\n\n return response.json()\n}\n","import { WidgetButton } from './WidgetButton'\nimport { WidgetStatus } from './WidgetStatus'\nimport { useThunderPhone } from './useThunderPhone'\nimport type { ThunderPhoneWidgetProps } from './types'\n\nexport function ThunderPhoneWidget({\n apiKey,\n agentId,\n apiBase,\n onConnect,\n onDisconnect,\n onError,\n className,\n ringtone,\n}: ThunderPhoneWidgetProps) {\n const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, ringtone })\n\n const handleClick = () => {\n if (phone.state === 'connected') {\n phone.disconnect()\n } else if (phone.state === 'idle' || phone.state === 'error' || phone.state === 'disconnected') {\n phone.connect()\n }\n }\n\n return (\n <div className={`tp-widget ${className || ''}`}>\n <WidgetStatus\n state={phone.state}\n agentName={phone.agentName || null}\n errorMessage={phone.error}\n />\n <WidgetButton\n state={phone.state}\n muted={phone.isMuted}\n onClick={handleClick}\n onMuteToggle={phone.toggleMute}\n />\n {phone.audio}\n </div>\n )\n}\n"],"mappings":"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","names":["import_jsx_runtime","import_react","import_components_react","import_react","import_jsx_runtime","audio","import_jsx_runtime"]}
|
package/dist/index.mjs
CHANGED
|
@@ -70,7 +70,7 @@ function WidgetStatus({ state, agentName, errorMessage }) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// src/useThunderPhone.ts
|
|
73
|
-
import { useCallback, useState as useState2, createElement } from "react";
|
|
73
|
+
import { useCallback, useEffect as useEffect3, useRef as useRef2, useState as useState2, createElement } from "react";
|
|
74
74
|
import { LiveKitRoom } from "@livekit/components-react";
|
|
75
75
|
|
|
76
76
|
// src/AudioHandler.tsx
|
|
@@ -154,11 +154,64 @@ async function createWidgetSession(apiKey, agentId, apiBase) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// src/useThunderPhone.ts
|
|
157
|
+
var DEFAULT_RINGTONE_URL = "https://cdn.thunderphone.com/widget/assets/ringtone-default.mp3";
|
|
158
|
+
function resolveRingtoneUrl(ringtone) {
|
|
159
|
+
if (ringtone === true || ringtone === "default") return DEFAULT_RINGTONE_URL;
|
|
160
|
+
if (typeof ringtone === "string" && ringtone.length > 0) return ringtone;
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
157
163
|
function useThunderPhone(opts) {
|
|
158
164
|
const [state, setState] = useState2("idle");
|
|
159
165
|
const [session, setSession] = useState2(null);
|
|
160
166
|
const [muted, setMuted] = useState2(false);
|
|
161
167
|
const [error, setError] = useState2();
|
|
168
|
+
const ringtoneUrl = resolveRingtoneUrl(opts.ringtone);
|
|
169
|
+
const ringtoneRef = useRef2(null);
|
|
170
|
+
useEffect3(() => {
|
|
171
|
+
if (!ringtoneUrl) {
|
|
172
|
+
ringtoneRef.current = null;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const audio2 = new Audio(ringtoneUrl);
|
|
176
|
+
audio2.loop = true;
|
|
177
|
+
audio2.preload = "auto";
|
|
178
|
+
audio2.volume = 1;
|
|
179
|
+
ringtoneRef.current = audio2;
|
|
180
|
+
return () => {
|
|
181
|
+
audio2.pause();
|
|
182
|
+
audio2.src = "";
|
|
183
|
+
ringtoneRef.current = null;
|
|
184
|
+
};
|
|
185
|
+
}, [ringtoneUrl]);
|
|
186
|
+
useEffect3(() => {
|
|
187
|
+
const audio2 = ringtoneRef.current;
|
|
188
|
+
if (!audio2) return;
|
|
189
|
+
let fadeInterval;
|
|
190
|
+
if (state === "connecting") {
|
|
191
|
+
audio2.currentTime = 0;
|
|
192
|
+
audio2.volume = 1;
|
|
193
|
+
audio2.play().catch(() => {
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
if (!audio2.paused) {
|
|
197
|
+
fadeInterval = setInterval(() => {
|
|
198
|
+
const next = audio2.volume - 0.1;
|
|
199
|
+
if (next <= 0) {
|
|
200
|
+
clearInterval(fadeInterval);
|
|
201
|
+
fadeInterval = void 0;
|
|
202
|
+
audio2.pause();
|
|
203
|
+
audio2.currentTime = 0;
|
|
204
|
+
audio2.volume = 1;
|
|
205
|
+
} else {
|
|
206
|
+
audio2.volume = next;
|
|
207
|
+
}
|
|
208
|
+
}, 20);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return () => {
|
|
212
|
+
if (fadeInterval) clearInterval(fadeInterval);
|
|
213
|
+
};
|
|
214
|
+
}, [state]);
|
|
162
215
|
const handleDisconnect = useCallback(() => {
|
|
163
216
|
setState("disconnected");
|
|
164
217
|
setSession(null);
|
|
@@ -174,6 +227,13 @@ function useThunderPhone(opts) {
|
|
|
174
227
|
if (state === "connecting" || state === "connected") return;
|
|
175
228
|
setState("connecting");
|
|
176
229
|
setError(void 0);
|
|
230
|
+
navigator.mediaDevices.getUserMedia({ audio: true }).then(
|
|
231
|
+
(stream) => {
|
|
232
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
233
|
+
},
|
|
234
|
+
() => {
|
|
235
|
+
}
|
|
236
|
+
);
|
|
177
237
|
try {
|
|
178
238
|
const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase);
|
|
179
239
|
setSession(sess);
|
|
@@ -227,9 +287,10 @@ function ThunderPhoneWidget({
|
|
|
227
287
|
onConnect,
|
|
228
288
|
onDisconnect,
|
|
229
289
|
onError,
|
|
230
|
-
className
|
|
290
|
+
className,
|
|
291
|
+
ringtone
|
|
231
292
|
}) {
|
|
232
|
-
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError });
|
|
293
|
+
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, ringtone });
|
|
233
294
|
const handleClick = () => {
|
|
234
295
|
if (phone.state === "connected") {
|
|
235
296
|
phone.disconnect();
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/WidgetButton.tsx","../src/WidgetStatus.tsx","../src/useThunderPhone.ts","../src/AudioHandler.tsx","../src/api.ts","../src/ThunderPhoneWidget.tsx"],"sourcesContent":["import type { WidgetState } from './types'\n\ninterface WidgetButtonProps {\n state: WidgetState\n muted: boolean\n onClick: () => void\n onMuteToggle: () => void\n}\n\nexport function WidgetButton({ state, muted, onClick, onMuteToggle }: WidgetButtonProps) {\n if (state === 'connected') {\n return (\n <div className=\"tp-button-group\">\n <button className=\"tp-button tp-button--mute\" onClick={onMuteToggle} type=\"button\">\n {muted ? (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\" />\n <path d=\"M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6\" />\n <path d=\"M17 16.95A7 7 0 0 1 5 12v-2m14 0v2c0 .76-.13 1.49-.36 2.18\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n <button className=\"tp-button tp-button--end\" onClick={onClick} type=\"button\">\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" />\n </svg>\n </button>\n </div>\n )\n }\n\n return (\n <button\n className={`tp-button tp-button--start ${state === 'connecting' ? 'tp-button--loading' : ''}`}\n onClick={onClick}\n disabled={state === 'connecting'}\n type=\"button\"\n >\n {state === 'connecting' ? (\n <svg className=\"tp-icon tp-spin\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n )\n}\n","import { useEffect, useState } from 'react'\nimport type { WidgetState } from './types'\n\ninterface WidgetStatusProps {\n state: WidgetState\n agentName: string | null\n errorMessage?: string\n}\n\nexport function WidgetStatus({ state, agentName, errorMessage }: WidgetStatusProps) {\n const [elapsed, setElapsed] = useState(0)\n\n useEffect(() => {\n if (state !== 'connected') {\n setElapsed(0)\n return\n }\n const interval = setInterval(() => setElapsed(s => s + 1), 1000)\n return () => clearInterval(interval)\n }, [state])\n\n const formatTime = (seconds: number) => {\n const m = Math.floor(seconds / 60)\n const s = seconds % 60\n return `${m}:${s.toString().padStart(2, '0')}`\n }\n\n const statusText: Record<WidgetState, string> = {\n idle: 'Ready',\n connecting: 'Connecting...',\n connected: formatTime(elapsed),\n disconnected: 'Disconnected',\n error: 'Unable to connect',\n }\n\n return (\n <div className=\"tp-status\">\n {agentName && <div className=\"tp-status__name\">{agentName}</div>}\n <div className={`tp-status__text tp-status--${state}`}>\n {state === 'connected' && <span className=\"tp-status__dot\" />}\n {errorMessage && state === 'error' ? errorMessage : statusText[state]}\n </div>\n </div>\n )\n}\n","import { useCallback, useState, type ReactNode, createElement } from 'react'\nimport { LiveKitRoom } from '@livekit/components-react'\nimport { AudioHandler } from './AudioHandler'\nimport { createWidgetSession, WidgetAPIError } from './api'\nimport type { WidgetState, WidgetSessionResponse } from './types'\n\nexport interface UseThunderPhoneOptions {\n apiKey: string\n agentId: number\n apiBase?: string\n onConnect?: () => void\n onDisconnect?: () => void\n onError?: (error: { error: string; message: string }) => void\n}\n\nexport interface UseThunderPhoneReturn {\n state: WidgetState\n connect: () => void\n disconnect: () => void\n toggleMute: () => void\n isMuted: boolean\n error: string | undefined\n agentName: string | undefined\n /** Render this somewhere in your tree — it's invisible but handles audio. */\n audio: ReactNode\n}\n\nexport function useThunderPhone(opts: UseThunderPhoneOptions): UseThunderPhoneReturn {\n const [state, setState] = useState<WidgetState>('idle')\n const [session, setSession] = useState<WidgetSessionResponse | null>(null)\n const [muted, setMuted] = useState(false)\n const [error, setError] = useState<string | undefined>()\n\n const handleDisconnect = useCallback(() => {\n setState('disconnected')\n setSession(null)\n setMuted(false)\n opts.onDisconnect?.()\n setTimeout(() => setState('idle'), 1500)\n }, [opts.onDisconnect])\n\n const handleAgentConnected = useCallback(() => {\n setState('connected')\n opts.onConnect?.()\n }, [opts.onConnect])\n\n const connect = useCallback(async () => {\n if (state === 'connecting' || state === 'connected') return\n setState('connecting')\n setError(undefined)\n try {\n const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase)\n setSession(sess)\n } catch (err) {\n setState('error')\n if (err instanceof WidgetAPIError) {\n setError(err.message)\n opts.onError?.({ error: err.code, message: err.message })\n } else {\n setError('Unable to connect.')\n opts.onError?.({ error: 'unknown', message: 'Unable to connect.' })\n }\n }\n }, [opts.apiKey, opts.agentId, opts.apiBase, state, opts.onError])\n\n const disconnect = useCallback(() => {\n handleDisconnect()\n }, [handleDisconnect])\n\n const toggleMute = useCallback(() => setMuted(m => !m), [])\n\n const audio: ReactNode = session\n ? createElement(\n LiveKitRoom,\n {\n token: session.token,\n serverUrl: session.server_url,\n audio: !muted,\n video: false,\n connect: true,\n },\n createElement(AudioHandler, {\n onAgentConnected: handleAgentConnected,\n onDisconnected: handleDisconnect,\n }),\n )\n : null\n\n return {\n state,\n connect,\n disconnect,\n toggleMute,\n isMuted: muted,\n error,\n agentName: session?.agent_name,\n audio,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport { useRoomContext } from '@livekit/components-react'\nimport { RoomEvent, Track, type RemoteTrackPublication, type RemoteParticipant } from 'livekit-client'\n\ninterface AudioHandlerProps {\n onAgentConnected: () => void\n onDisconnected: () => void\n}\n\nexport function AudioHandler({ onAgentConnected, onDisconnected }: AudioHandlerProps) {\n const room = useRoomContext()\n const audioRef = useRef<HTMLAudioElement>(null)\n\n useEffect(() => {\n if (room.remoteParticipants.size > 0) {\n onAgentConnected()\n }\n\n const handleParticipantConnected = () => onAgentConnected()\n const handleDisconnect = () => onDisconnected()\n\n // The bot publishes multiple audio tracks (tts, bg noise, hold music).\n // Add each track to a single MediaStream so the browser mixes them.\n const attachTrack = (\n track: { kind: Track.Kind; mediaStreamTrack: MediaStreamTrack },\n _pub: RemoteTrackPublication,\n _participant: RemoteParticipant,\n ) => {\n if (track.kind === Track.Kind.Audio && audioRef.current) {\n const existing = audioRef.current.srcObject as MediaStream | null\n if (existing) {\n existing.addTrack(track.mediaStreamTrack)\n } else {\n audioRef.current.srcObject = new MediaStream([track.mediaStreamTrack])\n audioRef.current.play().catch(() => {})\n }\n }\n }\n\n room.on(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.on(RoomEvent.Disconnected, handleDisconnect)\n room.on(RoomEvent.TrackSubscribed, attachTrack)\n\n const initialTracks: MediaStreamTrack[] = []\n for (const participant of room.remoteParticipants.values()) {\n for (const pub of participant.trackPublications.values()) {\n if (pub.track && pub.isSubscribed && pub.track.kind === Track.Kind.Audio) {\n initialTracks.push(pub.track.mediaStreamTrack)\n }\n }\n }\n if (initialTracks.length > 0 && audioRef.current) {\n audioRef.current.srcObject = new MediaStream(initialTracks)\n audioRef.current.play().catch(() => {})\n }\n\n return () => {\n room.off(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.off(RoomEvent.Disconnected, handleDisconnect)\n room.off(RoomEvent.TrackSubscribed, attachTrack)\n }\n }, [room, onAgentConnected, onDisconnected])\n\n return <audio ref={audioRef} autoPlay />\n}\n","import type { WidgetSessionResponse, WidgetError } from './types'\n\nconst DEFAULT_API_BASE = 'https://api.thunderphone.com/v1'\n\nexport class WidgetAPIError extends Error {\n constructor(public code: string, message: string) {\n super(message)\n this.name = 'WidgetAPIError'\n }\n}\n\nexport async function createWidgetSession(\n apiKey: string,\n agentId: number,\n apiBase?: string,\n): Promise<WidgetSessionResponse> {\n const base = apiBase || DEFAULT_API_BASE\n const response = await fetch(`${base}/widget/session`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': apiKey,\n },\n body: JSON.stringify({ agent_id: agentId }),\n })\n\n if (!response.ok) {\n const data: WidgetError = await response.json().catch(() => ({\n error: 'unknown',\n message: 'Unable to connect.',\n }))\n throw new WidgetAPIError(data.error, data.message)\n }\n\n return response.json()\n}\n","import { WidgetButton } from './WidgetButton'\nimport { WidgetStatus } from './WidgetStatus'\nimport { useThunderPhone } from './useThunderPhone'\nimport type { ThunderPhoneWidgetProps } from './types'\n\nexport function ThunderPhoneWidget({\n apiKey,\n agentId,\n apiBase,\n onConnect,\n onDisconnect,\n onError,\n className,\n}: ThunderPhoneWidgetProps) {\n const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError })\n\n const handleClick = () => {\n if (phone.state === 'connected') {\n phone.disconnect()\n } else if (phone.state === 'idle' || phone.state === 'error' || phone.state === 'disconnected') {\n phone.connect()\n }\n }\n\n return (\n <div className={`tp-widget ${className || ''}`}>\n <WidgetStatus\n state={phone.state}\n agentName={phone.agentName || null}\n errorMessage={phone.error}\n />\n <WidgetButton\n state={phone.state}\n muted={phone.isMuted}\n onClick={handleClick}\n onMuteToggle={phone.toggleMute}\n />\n {phone.audio}\n </div>\n )\n}\n"],"mappings":"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","names":["jsx","jsxs","useState","useEffect","jsx","useState","jsx","jsxs"]}
|
|
1
|
+
{"version":3,"sources":["../src/WidgetButton.tsx","../src/WidgetStatus.tsx","../src/useThunderPhone.ts","../src/AudioHandler.tsx","../src/api.ts","../src/ThunderPhoneWidget.tsx"],"sourcesContent":["import type { WidgetState } from './types'\n\ninterface WidgetButtonProps {\n state: WidgetState\n muted: boolean\n onClick: () => void\n onMuteToggle: () => void\n}\n\nexport function WidgetButton({ state, muted, onClick, onMuteToggle }: WidgetButtonProps) {\n if (state === 'connected') {\n return (\n <div className=\"tp-button-group\">\n <button className=\"tp-button tp-button--mute\" onClick={onMuteToggle} type=\"button\">\n {muted ? (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\" />\n <path d=\"M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6\" />\n <path d=\"M17 16.95A7 7 0 0 1 5 12v-2m14 0v2c0 .76-.13 1.49-.36 2.18\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n <button className=\"tp-button tp-button--end\" onClick={onClick} type=\"button\">\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" />\n </svg>\n </button>\n </div>\n )\n }\n\n return (\n <button\n className={`tp-button tp-button--start ${state === 'connecting' ? 'tp-button--loading' : ''}`}\n onClick={onClick}\n disabled={state === 'connecting'}\n type=\"button\"\n >\n {state === 'connecting' ? (\n <svg className=\"tp-icon tp-spin\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n ) : (\n <svg className=\"tp-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\" />\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\" />\n </svg>\n )}\n </button>\n )\n}\n","import { useEffect, useState } from 'react'\nimport type { WidgetState } from './types'\n\ninterface WidgetStatusProps {\n state: WidgetState\n agentName: string | null\n errorMessage?: string\n}\n\nexport function WidgetStatus({ state, agentName, errorMessage }: WidgetStatusProps) {\n const [elapsed, setElapsed] = useState(0)\n\n useEffect(() => {\n if (state !== 'connected') {\n setElapsed(0)\n return\n }\n const interval = setInterval(() => setElapsed(s => s + 1), 1000)\n return () => clearInterval(interval)\n }, [state])\n\n const formatTime = (seconds: number) => {\n const m = Math.floor(seconds / 60)\n const s = seconds % 60\n return `${m}:${s.toString().padStart(2, '0')}`\n }\n\n const statusText: Record<WidgetState, string> = {\n idle: 'Ready',\n connecting: 'Connecting...',\n connected: formatTime(elapsed),\n disconnected: 'Disconnected',\n error: 'Unable to connect',\n }\n\n return (\n <div className=\"tp-status\">\n {agentName && <div className=\"tp-status__name\">{agentName}</div>}\n <div className={`tp-status__text tp-status--${state}`}>\n {state === 'connected' && <span className=\"tp-status__dot\" />}\n {errorMessage && state === 'error' ? errorMessage : statusText[state]}\n </div>\n </div>\n )\n}\n","import { useCallback, useEffect, useRef, useState, type ReactNode, createElement } from 'react'\nimport { LiveKitRoom } from '@livekit/components-react'\nimport { AudioHandler } from './AudioHandler'\nimport { createWidgetSession, WidgetAPIError } from './api'\nimport type { WidgetState, WidgetSessionResponse } from './types'\n\nconst DEFAULT_RINGTONE_URL = 'https://cdn.thunderphone.com/widget/assets/ringtone-default.mp3'\n\nexport interface UseThunderPhoneOptions {\n apiKey: string\n agentId: number\n apiBase?: string\n onConnect?: () => void\n onDisconnect?: () => void\n onError?: (error: { error: string; message: string }) => void\n /**\n * Play a ringtone while connecting. Opt-in — disabled by default.\n * - `true` plays the default ringtone from the ThunderPhone CDN.\n * - A string URL plays a custom audio file.\n * - `false` or omitted disables the ringtone.\n */\n ringtone?: boolean | string\n}\n\nexport interface UseThunderPhoneReturn {\n state: WidgetState\n connect: () => void\n disconnect: () => void\n toggleMute: () => void\n isMuted: boolean\n error: string | undefined\n agentName: string | undefined\n /** Render this somewhere in your tree — it's invisible but handles audio. */\n audio: ReactNode\n}\n\n/** Resolve the ringtone option to a URL or null. */\nfunction resolveRingtoneUrl(ringtone: boolean | string | undefined): string | null {\n if (ringtone === true || ringtone === 'default') return DEFAULT_RINGTONE_URL\n if (typeof ringtone === 'string' && ringtone.length > 0) return ringtone\n return null\n}\n\nexport function useThunderPhone(opts: UseThunderPhoneOptions): UseThunderPhoneReturn {\n const [state, setState] = useState<WidgetState>('idle')\n const [session, setSession] = useState<WidgetSessionResponse | null>(null)\n const [muted, setMuted] = useState(false)\n const [error, setError] = useState<string | undefined>()\n\n // --- Ringtone management ---\n const ringtoneUrl = resolveRingtoneUrl(opts.ringtone)\n const ringtoneRef = useRef<HTMLAudioElement | null>(null)\n\n // Preload the ringtone audio element once when configured.\n useEffect(() => {\n if (!ringtoneUrl) {\n ringtoneRef.current = null\n return\n }\n const audio = new Audio(ringtoneUrl)\n audio.loop = true\n audio.preload = 'auto'\n audio.volume = 1.0\n ringtoneRef.current = audio\n return () => {\n audio.pause()\n audio.src = ''\n ringtoneRef.current = null\n }\n }, [ringtoneUrl])\n\n // Start/stop ringtone based on state.\n useEffect(() => {\n const audio = ringtoneRef.current\n if (!audio) return\n\n let fadeInterval: ReturnType<typeof setInterval> | undefined\n\n if (state === 'connecting') {\n // Reset to start and play (catch handles browsers that block autoplay\n // before a user gesture — unlikely here since connect() is click-driven).\n audio.currentTime = 0\n audio.volume = 1.0\n audio.play().catch(() => {})\n } else {\n // Fade out briefly (~200ms) for a smooth stop.\n if (!audio.paused) {\n fadeInterval = setInterval(() => {\n const next = audio.volume - 0.1\n if (next <= 0) {\n clearInterval(fadeInterval)\n fadeInterval = undefined\n audio.pause()\n audio.currentTime = 0\n audio.volume = 1.0\n } else {\n audio.volume = next\n }\n }, 20) // 10 steps * 20ms = 200ms fade\n }\n }\n\n // Clean up any in-progress fade if state changes again before it completes.\n return () => {\n if (fadeInterval) clearInterval(fadeInterval)\n }\n }, [state])\n\n const handleDisconnect = useCallback(() => {\n setState('disconnected')\n setSession(null)\n setMuted(false)\n opts.onDisconnect?.()\n setTimeout(() => setState('idle'), 1500)\n }, [opts.onDisconnect])\n\n const handleAgentConnected = useCallback(() => {\n setState('connected')\n opts.onConnect?.()\n }, [opts.onConnect])\n\n const connect = useCallback(async () => {\n if (state === 'connecting' || state === 'connected') return\n setState('connecting')\n setError(undefined)\n // Warm up mic permission in the background so the browser prompt (if\n // needed) overlaps with the API call. This is fire-and-forget: if the\n // session request fails we simply discard the stream without the user\n // noticing an unnecessary permission prompt — getUserMedia only shows\n // the prompt once per origin, so subsequent calls are instant.\n navigator.mediaDevices.getUserMedia({ audio: true }).then(\n (stream) => { stream.getTracks().forEach((t) => t.stop()) },\n () => {},\n )\n\n try {\n const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase)\n setSession(sess)\n } catch (err) {\n setState('error')\n if (err instanceof WidgetAPIError) {\n setError(err.message)\n opts.onError?.({ error: err.code, message: err.message })\n } else {\n setError('Unable to connect.')\n opts.onError?.({ error: 'unknown', message: 'Unable to connect.' })\n }\n }\n }, [opts.apiKey, opts.agentId, opts.apiBase, state, opts.onError])\n\n const disconnect = useCallback(() => {\n handleDisconnect()\n }, [handleDisconnect])\n\n const toggleMute = useCallback(() => setMuted(m => !m), [])\n\n const audio: ReactNode = session\n ? createElement(\n LiveKitRoom,\n {\n token: session.token,\n serverUrl: session.server_url,\n audio: !muted,\n video: false,\n connect: true,\n },\n createElement(AudioHandler, {\n onAgentConnected: handleAgentConnected,\n onDisconnected: handleDisconnect,\n }),\n )\n : null\n\n return {\n state,\n connect,\n disconnect,\n toggleMute,\n isMuted: muted,\n error,\n agentName: session?.agent_name,\n audio,\n }\n}\n","import { useEffect, useRef } from 'react'\nimport { useRoomContext } from '@livekit/components-react'\nimport { RoomEvent, Track, type RemoteTrackPublication, type RemoteParticipant } from 'livekit-client'\n\ninterface AudioHandlerProps {\n onAgentConnected: () => void\n onDisconnected: () => void\n}\n\nexport function AudioHandler({ onAgentConnected, onDisconnected }: AudioHandlerProps) {\n const room = useRoomContext()\n const audioRef = useRef<HTMLAudioElement>(null)\n\n useEffect(() => {\n if (room.remoteParticipants.size > 0) {\n onAgentConnected()\n }\n\n const handleParticipantConnected = () => onAgentConnected()\n const handleDisconnect = () => onDisconnected()\n\n // The bot publishes multiple audio tracks (tts, bg noise, hold music).\n // Add each track to a single MediaStream so the browser mixes them.\n const attachTrack = (\n track: { kind: Track.Kind; mediaStreamTrack: MediaStreamTrack },\n _pub: RemoteTrackPublication,\n _participant: RemoteParticipant,\n ) => {\n if (track.kind === Track.Kind.Audio && audioRef.current) {\n const existing = audioRef.current.srcObject as MediaStream | null\n if (existing) {\n existing.addTrack(track.mediaStreamTrack)\n } else {\n audioRef.current.srcObject = new MediaStream([track.mediaStreamTrack])\n audioRef.current.play().catch(() => {})\n }\n }\n }\n\n room.on(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.on(RoomEvent.Disconnected, handleDisconnect)\n room.on(RoomEvent.TrackSubscribed, attachTrack)\n\n const initialTracks: MediaStreamTrack[] = []\n for (const participant of room.remoteParticipants.values()) {\n for (const pub of participant.trackPublications.values()) {\n if (pub.track && pub.isSubscribed && pub.track.kind === Track.Kind.Audio) {\n initialTracks.push(pub.track.mediaStreamTrack)\n }\n }\n }\n if (initialTracks.length > 0 && audioRef.current) {\n audioRef.current.srcObject = new MediaStream(initialTracks)\n audioRef.current.play().catch(() => {})\n }\n\n return () => {\n room.off(RoomEvent.ParticipantConnected, handleParticipantConnected)\n room.off(RoomEvent.Disconnected, handleDisconnect)\n room.off(RoomEvent.TrackSubscribed, attachTrack)\n }\n }, [room, onAgentConnected, onDisconnected])\n\n return <audio ref={audioRef} autoPlay />\n}\n","import type { WidgetSessionResponse, WidgetError } from './types'\n\nconst DEFAULT_API_BASE = 'https://api.thunderphone.com/v1'\n\nexport class WidgetAPIError extends Error {\n constructor(public code: string, message: string) {\n super(message)\n this.name = 'WidgetAPIError'\n }\n}\n\nexport async function createWidgetSession(\n apiKey: string,\n agentId: number,\n apiBase?: string,\n): Promise<WidgetSessionResponse> {\n const base = apiBase || DEFAULT_API_BASE\n const response = await fetch(`${base}/widget/session`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': apiKey,\n },\n body: JSON.stringify({ agent_id: agentId }),\n })\n\n if (!response.ok) {\n const data: WidgetError = await response.json().catch(() => ({\n error: 'unknown',\n message: 'Unable to connect.',\n }))\n throw new WidgetAPIError(data.error, data.message)\n }\n\n return response.json()\n}\n","import { WidgetButton } from './WidgetButton'\nimport { WidgetStatus } from './WidgetStatus'\nimport { useThunderPhone } from './useThunderPhone'\nimport type { ThunderPhoneWidgetProps } from './types'\n\nexport function ThunderPhoneWidget({\n apiKey,\n agentId,\n apiBase,\n onConnect,\n onDisconnect,\n onError,\n className,\n ringtone,\n}: ThunderPhoneWidgetProps) {\n const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, ringtone })\n\n const handleClick = () => {\n if (phone.state === 'connected') {\n phone.disconnect()\n } else if (phone.state === 'idle' || phone.state === 'error' || phone.state === 'disconnected') {\n phone.connect()\n }\n }\n\n return (\n <div className={`tp-widget ${className || ''}`}>\n <WidgetStatus\n state={phone.state}\n agentName={phone.agentName || null}\n errorMessage={phone.error}\n />\n <WidgetButton\n state={phone.state}\n muted={phone.isMuted}\n onClick={handleClick}\n onMuteToggle={phone.toggleMute}\n />\n {phone.audio}\n </div>\n )\n}\n"],"mappings":"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","names":["jsx","jsxs","useEffect","useRef","useState","useEffect","jsx","useState","useRef","useEffect","audio","jsx","jsxs"]}
|
package/dist/mount.global.js
CHANGED
|
@@ -1371,11 +1371,11 @@ var ThunderPhone = (() => {
|
|
|
1371
1371
|
var dispatcher = resolveDispatcher();
|
|
1372
1372
|
return dispatcher.useReducer(reducer, initialArg, init);
|
|
1373
1373
|
}
|
|
1374
|
-
function
|
|
1374
|
+
function useRef4(initialValue) {
|
|
1375
1375
|
var dispatcher = resolveDispatcher();
|
|
1376
1376
|
return dispatcher.useRef(initialValue);
|
|
1377
1377
|
}
|
|
1378
|
-
function
|
|
1378
|
+
function useEffect5(create, deps) {
|
|
1379
1379
|
var dispatcher = resolveDispatcher();
|
|
1380
1380
|
return dispatcher.useEffect(create, deps);
|
|
1381
1381
|
}
|
|
@@ -2158,14 +2158,14 @@ var ThunderPhone = (() => {
|
|
|
2158
2158
|
exports.useContext = useContext2;
|
|
2159
2159
|
exports.useDebugValue = useDebugValue;
|
|
2160
2160
|
exports.useDeferredValue = useDeferredValue;
|
|
2161
|
-
exports.useEffect =
|
|
2161
|
+
exports.useEffect = useEffect5;
|
|
2162
2162
|
exports.useId = useId;
|
|
2163
2163
|
exports.useImperativeHandle = useImperativeHandle;
|
|
2164
2164
|
exports.useInsertionEffect = useInsertionEffect;
|
|
2165
2165
|
exports.useLayoutEffect = useLayoutEffect;
|
|
2166
2166
|
exports.useMemo = useMemo2;
|
|
2167
2167
|
exports.useReducer = useReducer2;
|
|
2168
|
-
exports.useRef =
|
|
2168
|
+
exports.useRef = useRef4;
|
|
2169
2169
|
exports.useState = useState4;
|
|
2170
2170
|
exports.useSyncExternalStore = useSyncExternalStore;
|
|
2171
2171
|
exports.useTransition = useTransition;
|
|
@@ -57513,11 +57513,64 @@ var ThunderPhone = (() => {
|
|
|
57513
57513
|
}
|
|
57514
57514
|
|
|
57515
57515
|
// src/useThunderPhone.ts
|
|
57516
|
+
var DEFAULT_RINGTONE_URL = "https://cdn.thunderphone.com/widget/assets/ringtone-default.mp3";
|
|
57517
|
+
function resolveRingtoneUrl(ringtone) {
|
|
57518
|
+
if (ringtone === true || ringtone === "default") return DEFAULT_RINGTONE_URL;
|
|
57519
|
+
if (typeof ringtone === "string" && ringtone.length > 0) return ringtone;
|
|
57520
|
+
return null;
|
|
57521
|
+
}
|
|
57516
57522
|
function useThunderPhone(opts) {
|
|
57517
57523
|
const [state, setState] = (0, import_react3.useState)("idle");
|
|
57518
57524
|
const [session, setSession] = (0, import_react3.useState)(null);
|
|
57519
57525
|
const [muted, setMuted] = (0, import_react3.useState)(false);
|
|
57520
57526
|
const [error, setError] = (0, import_react3.useState)();
|
|
57527
|
+
const ringtoneUrl = resolveRingtoneUrl(opts.ringtone);
|
|
57528
|
+
const ringtoneRef = (0, import_react3.useRef)(null);
|
|
57529
|
+
(0, import_react3.useEffect)(() => {
|
|
57530
|
+
if (!ringtoneUrl) {
|
|
57531
|
+
ringtoneRef.current = null;
|
|
57532
|
+
return;
|
|
57533
|
+
}
|
|
57534
|
+
const audio2 = new Audio(ringtoneUrl);
|
|
57535
|
+
audio2.loop = true;
|
|
57536
|
+
audio2.preload = "auto";
|
|
57537
|
+
audio2.volume = 1;
|
|
57538
|
+
ringtoneRef.current = audio2;
|
|
57539
|
+
return () => {
|
|
57540
|
+
audio2.pause();
|
|
57541
|
+
audio2.src = "";
|
|
57542
|
+
ringtoneRef.current = null;
|
|
57543
|
+
};
|
|
57544
|
+
}, [ringtoneUrl]);
|
|
57545
|
+
(0, import_react3.useEffect)(() => {
|
|
57546
|
+
const audio2 = ringtoneRef.current;
|
|
57547
|
+
if (!audio2) return;
|
|
57548
|
+
let fadeInterval;
|
|
57549
|
+
if (state === "connecting") {
|
|
57550
|
+
audio2.currentTime = 0;
|
|
57551
|
+
audio2.volume = 1;
|
|
57552
|
+
audio2.play().catch(() => {
|
|
57553
|
+
});
|
|
57554
|
+
} else {
|
|
57555
|
+
if (!audio2.paused) {
|
|
57556
|
+
fadeInterval = setInterval(() => {
|
|
57557
|
+
const next = audio2.volume - 0.1;
|
|
57558
|
+
if (next <= 0) {
|
|
57559
|
+
clearInterval(fadeInterval);
|
|
57560
|
+
fadeInterval = void 0;
|
|
57561
|
+
audio2.pause();
|
|
57562
|
+
audio2.currentTime = 0;
|
|
57563
|
+
audio2.volume = 1;
|
|
57564
|
+
} else {
|
|
57565
|
+
audio2.volume = next;
|
|
57566
|
+
}
|
|
57567
|
+
}, 20);
|
|
57568
|
+
}
|
|
57569
|
+
}
|
|
57570
|
+
return () => {
|
|
57571
|
+
if (fadeInterval) clearInterval(fadeInterval);
|
|
57572
|
+
};
|
|
57573
|
+
}, [state]);
|
|
57521
57574
|
const handleDisconnect = (0, import_react3.useCallback)(() => {
|
|
57522
57575
|
setState("disconnected");
|
|
57523
57576
|
setSession(null);
|
|
@@ -57533,6 +57586,13 @@ var ThunderPhone = (() => {
|
|
|
57533
57586
|
if (state === "connecting" || state === "connected") return;
|
|
57534
57587
|
setState("connecting");
|
|
57535
57588
|
setError(void 0);
|
|
57589
|
+
navigator.mediaDevices.getUserMedia({ audio: true }).then(
|
|
57590
|
+
(stream) => {
|
|
57591
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
57592
|
+
},
|
|
57593
|
+
() => {
|
|
57594
|
+
}
|
|
57595
|
+
);
|
|
57536
57596
|
try {
|
|
57537
57597
|
const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase);
|
|
57538
57598
|
setSession(sess);
|
|
@@ -57586,9 +57646,10 @@ var ThunderPhone = (() => {
|
|
|
57586
57646
|
onConnect,
|
|
57587
57647
|
onDisconnect,
|
|
57588
57648
|
onError,
|
|
57589
|
-
className
|
|
57649
|
+
className,
|
|
57650
|
+
ringtone
|
|
57590
57651
|
}) {
|
|
57591
|
-
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError });
|
|
57652
|
+
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, ringtone });
|
|
57592
57653
|
const handleClick = () => {
|
|
57593
57654
|
if (phone.state === "connected") {
|
|
57594
57655
|
phone.disconnect();
|