@thunderphone/widget 0.2.2 → 0.3.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 +37 -48
- package/dist/index.d.mts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +155 -139
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +148 -133
- package/dist/index.mjs.map +1 -1
- package/dist/mount.global.js +156 -142
- package/dist/mount.global.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,40 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Embed a ThunderPhone voice AI agent on any website. Users can talk to your agent directly from your site using their browser microphone.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
### Option 1: Script Tag (CDN)
|
|
8
|
-
|
|
9
|
-
No build tools required. Add two tags to your HTML:
|
|
10
|
-
|
|
11
|
-
```html
|
|
12
|
-
<link rel="stylesheet" href="https://cdn.thunderphone.com/widget/latest/style.css" />
|
|
13
|
-
<script src="https://cdn.thunderphone.com/widget/latest/widget.js"></script>
|
|
14
|
-
|
|
15
|
-
<div id="thunderphone"></div>
|
|
16
|
-
|
|
17
|
-
<script>
|
|
18
|
-
ThunderPhone.mount({
|
|
19
|
-
element: '#thunderphone',
|
|
20
|
-
apiKey: 'pk_live_your_publishable_key',
|
|
21
|
-
agentId: 123,
|
|
22
|
-
})
|
|
23
|
-
</script>
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Pin to a specific version for production:
|
|
27
|
-
|
|
28
|
-
```html
|
|
29
|
-
<link rel="stylesheet" href="https://cdn.thunderphone.com/widget/v0.1.0/style.css" />
|
|
30
|
-
<script src="https://cdn.thunderphone.com/widget/v0.1.0/widget.js"></script>
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### Option 2: npm (React)
|
|
5
|
+
## Installation
|
|
34
6
|
|
|
35
7
|
```bash
|
|
36
8
|
npm install @thunderphone/widget
|
|
37
9
|
```
|
|
38
10
|
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
39
13
|
```tsx
|
|
40
14
|
import { ThunderPhoneWidget } from '@thunderphone/widget'
|
|
41
15
|
import '@thunderphone/widget/style.css'
|
|
@@ -57,7 +31,7 @@ function App() {
|
|
|
57
31
|
3. Enable the **Embeddable Widget** toggle on the agent you want to expose
|
|
58
32
|
4. Use the agent's ID and your publishable key in the widget code above
|
|
59
33
|
|
|
60
|
-
## Props
|
|
34
|
+
## Props
|
|
61
35
|
|
|
62
36
|
| Prop | Type | Required | Description |
|
|
63
37
|
|------|------|----------|-------------|
|
|
@@ -69,8 +43,6 @@ function App() {
|
|
|
69
43
|
| `onError` | `(error) => void` | No | Called on errors. Error has `error` (code) and `message` fields |
|
|
70
44
|
| `className` | `string` | No | Additional CSS class for the widget container |
|
|
71
45
|
|
|
72
|
-
For the script tag version, pass these as properties on the options object to `ThunderPhone.mount()`, plus an `element` property (CSS selector or DOM element) for where to render.
|
|
73
|
-
|
|
74
46
|
## Styling
|
|
75
47
|
|
|
76
48
|
The widget uses plain CSS with `tp-` prefixed classes, so it won't conflict with your styles. You can override any of these classes:
|
|
@@ -97,29 +69,46 @@ Example — custom colors:
|
|
|
97
69
|
}
|
|
98
70
|
```
|
|
99
71
|
|
|
100
|
-
##
|
|
72
|
+
## Domain Restrictions
|
|
101
73
|
|
|
102
|
-
|
|
74
|
+
Your publishable key can be restricted to specific domains in the Developers settings page. Requests from unlisted domains will be rejected. `localhost` is always allowed for development.
|
|
103
75
|
|
|
104
|
-
|
|
105
|
-
const widget = ThunderPhone.mount({
|
|
106
|
-
element: '#thunderphone',
|
|
107
|
-
apiKey: 'pk_live_...',
|
|
108
|
-
agentId: 123,
|
|
109
|
-
onConnect: () => console.log('Connected'),
|
|
110
|
-
onDisconnect: () => console.log('Disconnected'),
|
|
111
|
-
onError: (err) => console.error(err.message),
|
|
112
|
-
})
|
|
76
|
+
Wildcard subdomains are supported: `*.example.com` matches `app.example.com`, `docs.example.com`, etc.
|
|
113
77
|
|
|
114
|
-
|
|
115
|
-
|
|
78
|
+
## CDN / Script Tag
|
|
79
|
+
|
|
80
|
+
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.
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<link rel="stylesheet" href="https://cdn.thunderphone.com/widget/v0.2.1/style.css" />
|
|
84
|
+
<script src="https://cdn.thunderphone.com/widget/v0.2.1/widget.js"></script>
|
|
85
|
+
|
|
86
|
+
<div id="thunderphone"></div>
|
|
87
|
+
|
|
88
|
+
<script>
|
|
89
|
+
ThunderPhone.mount({
|
|
90
|
+
element: '#thunderphone',
|
|
91
|
+
apiKey: 'pk_live_your_publishable_key',
|
|
92
|
+
agentId: 123,
|
|
93
|
+
})
|
|
94
|
+
</script>
|
|
116
95
|
```
|
|
117
96
|
|
|
118
|
-
|
|
97
|
+
Use `latest` instead of a version number for the most recent release (cached for 5 minutes):
|
|
119
98
|
|
|
120
|
-
|
|
99
|
+
```
|
|
100
|
+
https://cdn.thunderphone.com/widget/latest/widget.js
|
|
101
|
+
https://cdn.thunderphone.com/widget/latest/style.css
|
|
102
|
+
```
|
|
121
103
|
|
|
122
|
-
|
|
104
|
+
`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:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
const widget = ThunderPhone.mount({ element: '#thunderphone', apiKey: '...', agentId: 123 })
|
|
108
|
+
|
|
109
|
+
// Later, to remove the widget:
|
|
110
|
+
widget.unmount()
|
|
111
|
+
```
|
|
123
112
|
|
|
124
113
|
## License
|
|
125
114
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
2
3
|
|
|
3
4
|
interface WidgetError {
|
|
4
5
|
error: string;
|
|
@@ -17,4 +18,28 @@ interface ThunderPhoneWidgetProps {
|
|
|
17
18
|
|
|
18
19
|
declare function ThunderPhoneWidget({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, className, }: ThunderPhoneWidgetProps): react_jsx_runtime.JSX.Element;
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
interface UseThunderPhoneOptions {
|
|
22
|
+
apiKey: string;
|
|
23
|
+
agentId: number;
|
|
24
|
+
apiBase?: string;
|
|
25
|
+
onConnect?: () => void;
|
|
26
|
+
onDisconnect?: () => void;
|
|
27
|
+
onError?: (error: {
|
|
28
|
+
error: string;
|
|
29
|
+
message: string;
|
|
30
|
+
}) => void;
|
|
31
|
+
}
|
|
32
|
+
interface UseThunderPhoneReturn {
|
|
33
|
+
state: WidgetState;
|
|
34
|
+
connect: () => void;
|
|
35
|
+
disconnect: () => void;
|
|
36
|
+
toggleMute: () => void;
|
|
37
|
+
isMuted: boolean;
|
|
38
|
+
error: string | undefined;
|
|
39
|
+
agentName: string | undefined;
|
|
40
|
+
/** Render this somewhere in your tree — it's invisible but handles audio. */
|
|
41
|
+
audio: ReactNode;
|
|
42
|
+
}
|
|
43
|
+
declare function useThunderPhone(opts: UseThunderPhoneOptions): UseThunderPhoneReturn;
|
|
44
|
+
|
|
45
|
+
export { ThunderPhoneWidget, type ThunderPhoneWidgetProps, type UseThunderPhoneOptions, type UseThunderPhoneReturn, type WidgetError, type WidgetState, useThunderPhone };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
2
3
|
|
|
3
4
|
interface WidgetError {
|
|
4
5
|
error: string;
|
|
@@ -17,4 +18,28 @@ interface ThunderPhoneWidgetProps {
|
|
|
17
18
|
|
|
18
19
|
declare function ThunderPhoneWidget({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError, className, }: ThunderPhoneWidgetProps): react_jsx_runtime.JSX.Element;
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
interface UseThunderPhoneOptions {
|
|
22
|
+
apiKey: string;
|
|
23
|
+
agentId: number;
|
|
24
|
+
apiBase?: string;
|
|
25
|
+
onConnect?: () => void;
|
|
26
|
+
onDisconnect?: () => void;
|
|
27
|
+
onError?: (error: {
|
|
28
|
+
error: string;
|
|
29
|
+
message: string;
|
|
30
|
+
}) => void;
|
|
31
|
+
}
|
|
32
|
+
interface UseThunderPhoneReturn {
|
|
33
|
+
state: WidgetState;
|
|
34
|
+
connect: () => void;
|
|
35
|
+
disconnect: () => void;
|
|
36
|
+
toggleMute: () => void;
|
|
37
|
+
isMuted: boolean;
|
|
38
|
+
error: string | undefined;
|
|
39
|
+
agentName: string | undefined;
|
|
40
|
+
/** Render this somewhere in your tree — it's invisible but handles audio. */
|
|
41
|
+
audio: ReactNode;
|
|
42
|
+
}
|
|
43
|
+
declare function useThunderPhone(opts: UseThunderPhoneOptions): UseThunderPhoneReturn;
|
|
44
|
+
|
|
45
|
+
export { ThunderPhoneWidget, type ThunderPhoneWidgetProps, type UseThunderPhoneOptions, type UseThunderPhoneReturn, type WidgetError, type WidgetState, useThunderPhone };
|
package/dist/index.js
CHANGED
|
@@ -20,101 +20,54 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
ThunderPhoneWidget: () => ThunderPhoneWidget
|
|
23
|
+
ThunderPhoneWidget: () => ThunderPhoneWidget,
|
|
24
|
+
useThunderPhone: () => useThunderPhone
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
26
27
|
|
|
27
|
-
// src/ThunderPhoneWidget.tsx
|
|
28
|
-
var import_react3 = require("react");
|
|
29
|
-
var import_components_react2 = require("@livekit/components-react");
|
|
30
|
-
|
|
31
|
-
// src/AudioHandler.tsx
|
|
32
|
-
var import_react = require("react");
|
|
33
|
-
var import_components_react = require("@livekit/components-react");
|
|
34
|
-
var import_livekit_client = require("livekit-client");
|
|
35
|
-
var import_jsx_runtime = require("react/jsx-runtime");
|
|
36
|
-
function AudioHandler({ onAgentConnected, onDisconnected }) {
|
|
37
|
-
const room = (0, import_components_react.useRoomContext)();
|
|
38
|
-
const audioRef = (0, import_react.useRef)(null);
|
|
39
|
-
(0, import_react.useEffect)(() => {
|
|
40
|
-
if (room.remoteParticipants.size > 0) {
|
|
41
|
-
onAgentConnected();
|
|
42
|
-
}
|
|
43
|
-
const handleParticipantConnected = () => onAgentConnected();
|
|
44
|
-
const handleDisconnect = () => onDisconnected();
|
|
45
|
-
const attachTrack = (track, _pub, _participant) => {
|
|
46
|
-
if (track.kind === import_livekit_client.Track.Kind.Audio && audioRef.current) {
|
|
47
|
-
const stream = new MediaStream([track.mediaStreamTrack]);
|
|
48
|
-
audioRef.current.srcObject = stream;
|
|
49
|
-
audioRef.current.play().catch(() => {
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
room.on(import_livekit_client.RoomEvent.ParticipantConnected, handleParticipantConnected);
|
|
54
|
-
room.on(import_livekit_client.RoomEvent.Disconnected, handleDisconnect);
|
|
55
|
-
room.on(import_livekit_client.RoomEvent.TrackSubscribed, attachTrack);
|
|
56
|
-
for (const participant of room.remoteParticipants.values()) {
|
|
57
|
-
for (const pub of participant.trackPublications.values()) {
|
|
58
|
-
if (pub.track && pub.isSubscribed && pub.track.kind === import_livekit_client.Track.Kind.Audio && audioRef.current) {
|
|
59
|
-
const stream = new MediaStream([pub.track.mediaStreamTrack]);
|
|
60
|
-
audioRef.current.srcObject = stream;
|
|
61
|
-
audioRef.current.play().catch(() => {
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return () => {
|
|
67
|
-
room.off(import_livekit_client.RoomEvent.ParticipantConnected, handleParticipantConnected);
|
|
68
|
-
room.off(import_livekit_client.RoomEvent.Disconnected, handleDisconnect);
|
|
69
|
-
room.off(import_livekit_client.RoomEvent.TrackSubscribed, attachTrack);
|
|
70
|
-
};
|
|
71
|
-
}, [room, onAgentConnected, onDisconnected]);
|
|
72
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("audio", { ref: audioRef, autoPlay: true });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
28
|
// src/WidgetButton.tsx
|
|
76
|
-
var
|
|
29
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
77
30
|
function WidgetButton({ state, muted, onClick, onMuteToggle }) {
|
|
78
31
|
if (state === "connected") {
|
|
79
|
-
return /* @__PURE__ */ (0,
|
|
80
|
-
/* @__PURE__ */ (0,
|
|
81
|
-
/* @__PURE__ */ (0,
|
|
82
|
-
/* @__PURE__ */ (0,
|
|
83
|
-
/* @__PURE__ */ (0,
|
|
84
|
-
/* @__PURE__ */ (0,
|
|
85
|
-
/* @__PURE__ */ (0,
|
|
86
|
-
] }) : /* @__PURE__ */ (0,
|
|
87
|
-
/* @__PURE__ */ (0,
|
|
88
|
-
/* @__PURE__ */ (0,
|
|
89
|
-
/* @__PURE__ */ (0,
|
|
90
|
-
/* @__PURE__ */ (0,
|
|
32
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tp-button-group", children: [
|
|
33
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "tp-button tp-button--mute", onClick: onMuteToggle, type: "button", children: muted ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { className: "tp-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
34
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "1", y1: "1", x2: "23", y2: "23" }),
|
|
35
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6" }),
|
|
36
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M17 16.95A7 7 0 0 1 5 12v-2m14 0v2c0 .76-.13 1.49-.36 2.18" }),
|
|
37
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "19", x2: "12", y2: "23" }),
|
|
38
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "8", y1: "23", x2: "16", y2: "23" })
|
|
39
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { className: "tp-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
40
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }),
|
|
41
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
|
|
42
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "19", x2: "12", y2: "23" }),
|
|
43
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "8", y1: "23", x2: "16", y2: "23" })
|
|
91
44
|
] }) }),
|
|
92
|
-
/* @__PURE__ */ (0,
|
|
45
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "tp-button tp-button--end", onClick, type: "button", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "tp-icon", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) }) })
|
|
93
46
|
] });
|
|
94
47
|
}
|
|
95
|
-
return /* @__PURE__ */ (0,
|
|
48
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
96
49
|
"button",
|
|
97
50
|
{
|
|
98
51
|
className: `tp-button tp-button--start ${state === "connecting" ? "tp-button--loading" : ""}`,
|
|
99
52
|
onClick,
|
|
100
53
|
disabled: state === "connecting",
|
|
101
54
|
type: "button",
|
|
102
|
-
children: state === "connecting" ? /* @__PURE__ */ (0,
|
|
103
|
-
/* @__PURE__ */ (0,
|
|
104
|
-
/* @__PURE__ */ (0,
|
|
105
|
-
/* @__PURE__ */ (0,
|
|
106
|
-
/* @__PURE__ */ (0,
|
|
55
|
+
children: state === "connecting" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "tp-icon tp-spin", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { className: "tp-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
56
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }),
|
|
57
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
|
|
58
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "19", x2: "12", y2: "23" }),
|
|
59
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "8", y1: "23", x2: "16", y2: "23" })
|
|
107
60
|
] })
|
|
108
61
|
}
|
|
109
62
|
);
|
|
110
63
|
}
|
|
111
64
|
|
|
112
65
|
// src/WidgetStatus.tsx
|
|
113
|
-
var
|
|
114
|
-
var
|
|
66
|
+
var import_react = require("react");
|
|
67
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
115
68
|
function WidgetStatus({ state, agentName, errorMessage }) {
|
|
116
|
-
const [elapsed, setElapsed] = (0,
|
|
117
|
-
(0,
|
|
69
|
+
const [elapsed, setElapsed] = (0, import_react.useState)(0);
|
|
70
|
+
(0, import_react.useEffect)(() => {
|
|
118
71
|
if (state !== "connected") {
|
|
119
72
|
setElapsed(0);
|
|
120
73
|
return;
|
|
@@ -134,15 +87,63 @@ function WidgetStatus({ state, agentName, errorMessage }) {
|
|
|
134
87
|
disconnected: "Disconnected",
|
|
135
88
|
error: "Unable to connect"
|
|
136
89
|
};
|
|
137
|
-
return /* @__PURE__ */ (0,
|
|
138
|
-
agentName && /* @__PURE__ */ (0,
|
|
139
|
-
/* @__PURE__ */ (0,
|
|
140
|
-
state === "connected" && /* @__PURE__ */ (0,
|
|
90
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "tp-status", children: [
|
|
91
|
+
agentName && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "tp-status__name", children: agentName }),
|
|
92
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `tp-status__text tp-status--${state}`, children: [
|
|
93
|
+
state === "connected" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "tp-status__dot" }),
|
|
141
94
|
errorMessage && state === "error" ? errorMessage : statusText[state]
|
|
142
95
|
] })
|
|
143
96
|
] });
|
|
144
97
|
}
|
|
145
98
|
|
|
99
|
+
// src/useThunderPhone.ts
|
|
100
|
+
var import_react3 = require("react");
|
|
101
|
+
var import_components_react2 = require("@livekit/components-react");
|
|
102
|
+
|
|
103
|
+
// src/AudioHandler.tsx
|
|
104
|
+
var import_react2 = require("react");
|
|
105
|
+
var import_components_react = require("@livekit/components-react");
|
|
106
|
+
var import_livekit_client = require("livekit-client");
|
|
107
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
108
|
+
function AudioHandler({ onAgentConnected, onDisconnected }) {
|
|
109
|
+
const room = (0, import_components_react.useRoomContext)();
|
|
110
|
+
const audioRef = (0, import_react2.useRef)(null);
|
|
111
|
+
(0, import_react2.useEffect)(() => {
|
|
112
|
+
if (room.remoteParticipants.size > 0) {
|
|
113
|
+
onAgentConnected();
|
|
114
|
+
}
|
|
115
|
+
const handleParticipantConnected = () => onAgentConnected();
|
|
116
|
+
const handleDisconnect = () => onDisconnected();
|
|
117
|
+
const attachTrack = (track, _pub, _participant) => {
|
|
118
|
+
if (track.kind === import_livekit_client.Track.Kind.Audio && audioRef.current) {
|
|
119
|
+
const stream = new MediaStream([track.mediaStreamTrack]);
|
|
120
|
+
audioRef.current.srcObject = stream;
|
|
121
|
+
audioRef.current.play().catch(() => {
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
room.on(import_livekit_client.RoomEvent.ParticipantConnected, handleParticipantConnected);
|
|
126
|
+
room.on(import_livekit_client.RoomEvent.Disconnected, handleDisconnect);
|
|
127
|
+
room.on(import_livekit_client.RoomEvent.TrackSubscribed, attachTrack);
|
|
128
|
+
for (const participant of room.remoteParticipants.values()) {
|
|
129
|
+
for (const pub of participant.trackPublications.values()) {
|
|
130
|
+
if (pub.track && pub.isSubscribed && pub.track.kind === import_livekit_client.Track.Kind.Audio && audioRef.current) {
|
|
131
|
+
const stream = new MediaStream([pub.track.mediaStreamTrack]);
|
|
132
|
+
audioRef.current.srcObject = stream;
|
|
133
|
+
audioRef.current.play().catch(() => {
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return () => {
|
|
139
|
+
room.off(import_livekit_client.RoomEvent.ParticipantConnected, handleParticipantConnected);
|
|
140
|
+
room.off(import_livekit_client.RoomEvent.Disconnected, handleDisconnect);
|
|
141
|
+
room.off(import_livekit_client.RoomEvent.TrackSubscribed, attachTrack);
|
|
142
|
+
};
|
|
143
|
+
}, [room, onAgentConnected, onDisconnected]);
|
|
144
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("audio", { ref: audioRef, autoPlay: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
146
147
|
// src/api.ts
|
|
147
148
|
var DEFAULT_API_BASE = "https://api.thunderphone.com/v1";
|
|
148
149
|
var WidgetAPIError = class extends Error {
|
|
@@ -172,99 +173,114 @@ async function createWidgetSession(apiKey, agentId, apiBase) {
|
|
|
172
173
|
return response.json();
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
// src/
|
|
176
|
-
|
|
177
|
-
function ThunderPhoneWidget({
|
|
178
|
-
apiKey,
|
|
179
|
-
agentId,
|
|
180
|
-
apiBase,
|
|
181
|
-
onConnect,
|
|
182
|
-
onDisconnect,
|
|
183
|
-
onError,
|
|
184
|
-
className
|
|
185
|
-
}) {
|
|
176
|
+
// src/useThunderPhone.ts
|
|
177
|
+
function useThunderPhone(opts) {
|
|
186
178
|
const [state, setState] = (0, import_react3.useState)("idle");
|
|
187
179
|
const [session, setSession] = (0, import_react3.useState)(null);
|
|
188
180
|
const [muted, setMuted] = (0, import_react3.useState)(false);
|
|
189
|
-
const [
|
|
190
|
-
const
|
|
181
|
+
const [error, setError] = (0, import_react3.useState)();
|
|
182
|
+
const handleDisconnect = (0, import_react3.useCallback)(() => {
|
|
183
|
+
setState("disconnected");
|
|
184
|
+
setSession(null);
|
|
185
|
+
setMuted(false);
|
|
186
|
+
opts.onDisconnect?.();
|
|
187
|
+
setTimeout(() => setState("idle"), 1500);
|
|
188
|
+
}, [opts.onDisconnect]);
|
|
189
|
+
const handleAgentConnected = (0, import_react3.useCallback)(() => {
|
|
190
|
+
setState("connected");
|
|
191
|
+
opts.onConnect?.();
|
|
192
|
+
}, [opts.onConnect]);
|
|
193
|
+
const connect = (0, import_react3.useCallback)(async () => {
|
|
191
194
|
if (state === "connecting" || state === "connected") return;
|
|
192
195
|
setState("connecting");
|
|
193
|
-
|
|
196
|
+
setError(void 0);
|
|
194
197
|
try {
|
|
195
|
-
const sess = await createWidgetSession(apiKey, agentId, apiBase);
|
|
198
|
+
const sess = await createWidgetSession(opts.apiKey, opts.agentId, opts.apiBase);
|
|
196
199
|
setSession(sess);
|
|
197
200
|
} catch (err) {
|
|
198
201
|
setState("error");
|
|
199
202
|
if (err instanceof WidgetAPIError) {
|
|
200
|
-
|
|
201
|
-
onError?.({ error: err.code, message: err.message });
|
|
203
|
+
setError(err.message);
|
|
204
|
+
opts.onError?.({ error: err.code, message: err.message });
|
|
202
205
|
} else {
|
|
203
|
-
|
|
204
|
-
onError?.({ error: "unknown", message: "Unable to connect." });
|
|
206
|
+
setError("Unable to connect.");
|
|
207
|
+
opts.onError?.({ error: "unknown", message: "Unable to connect." });
|
|
205
208
|
}
|
|
206
209
|
}
|
|
207
|
-
}, [apiKey, agentId, apiBase, state, onError]);
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
210
|
+
}, [opts.apiKey, opts.agentId, opts.apiBase, state, opts.onError]);
|
|
211
|
+
const disconnect = (0, import_react3.useCallback)(() => {
|
|
212
|
+
handleDisconnect();
|
|
213
|
+
}, [handleDisconnect]);
|
|
214
|
+
const toggleMute = (0, import_react3.useCallback)(() => setMuted((m) => !m), []);
|
|
215
|
+
const audio = session ? (0, import_react3.createElement)(
|
|
216
|
+
import_components_react2.LiveKitRoom,
|
|
217
|
+
{
|
|
218
|
+
token: session.token,
|
|
219
|
+
serverUrl: session.server_url,
|
|
220
|
+
audio: !muted,
|
|
221
|
+
video: false,
|
|
222
|
+
connect: true
|
|
223
|
+
},
|
|
224
|
+
(0, import_react3.createElement)(AudioHandler, {
|
|
225
|
+
onAgentConnected: handleAgentConnected,
|
|
226
|
+
onDisconnected: handleDisconnect
|
|
227
|
+
})
|
|
228
|
+
) : null;
|
|
229
|
+
return {
|
|
230
|
+
state,
|
|
231
|
+
connect,
|
|
232
|
+
disconnect,
|
|
233
|
+
toggleMute,
|
|
234
|
+
isMuted: muted,
|
|
235
|
+
error,
|
|
236
|
+
agentName: session?.agent_name,
|
|
237
|
+
audio
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/ThunderPhoneWidget.tsx
|
|
242
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
243
|
+
function ThunderPhoneWidget({
|
|
244
|
+
apiKey,
|
|
245
|
+
agentId,
|
|
246
|
+
apiBase,
|
|
247
|
+
onConnect,
|
|
248
|
+
onDisconnect,
|
|
249
|
+
onError,
|
|
250
|
+
className
|
|
251
|
+
}) {
|
|
252
|
+
const phone = useThunderPhone({ apiKey, agentId, apiBase, onConnect, onDisconnect, onError });
|
|
219
253
|
const handleClick = () => {
|
|
220
|
-
if (state === "connected") {
|
|
221
|
-
|
|
222
|
-
} else if (state === "idle" || state === "error" || state === "disconnected") {
|
|
223
|
-
|
|
254
|
+
if (phone.state === "connected") {
|
|
255
|
+
phone.disconnect();
|
|
256
|
+
} else if (phone.state === "idle" || phone.state === "error" || phone.state === "disconnected") {
|
|
257
|
+
phone.connect();
|
|
224
258
|
}
|
|
225
259
|
};
|
|
226
|
-
const handleMuteToggle = (0, import_react3.useCallback)(() => {
|
|
227
|
-
setMuted((m) => !m);
|
|
228
|
-
}, []);
|
|
229
260
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `tp-widget ${className || ""}`, children: [
|
|
230
261
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
231
262
|
WidgetStatus,
|
|
232
263
|
{
|
|
233
|
-
state,
|
|
234
|
-
agentName:
|
|
235
|
-
errorMessage
|
|
264
|
+
state: phone.state,
|
|
265
|
+
agentName: phone.agentName || null,
|
|
266
|
+
errorMessage: phone.error
|
|
236
267
|
}
|
|
237
268
|
),
|
|
238
269
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
239
270
|
WidgetButton,
|
|
240
271
|
{
|
|
241
|
-
state,
|
|
242
|
-
muted,
|
|
272
|
+
state: phone.state,
|
|
273
|
+
muted: phone.isMuted,
|
|
243
274
|
onClick: handleClick,
|
|
244
|
-
onMuteToggle:
|
|
275
|
+
onMuteToggle: phone.toggleMute
|
|
245
276
|
}
|
|
246
277
|
),
|
|
247
|
-
|
|
248
|
-
import_components_react2.LiveKitRoom,
|
|
249
|
-
{
|
|
250
|
-
token: session.token,
|
|
251
|
-
serverUrl: session.server_url,
|
|
252
|
-
audio: !muted,
|
|
253
|
-
video: false,
|
|
254
|
-
connect: true,
|
|
255
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
256
|
-
AudioHandler,
|
|
257
|
-
{
|
|
258
|
-
onAgentConnected: handleAgentConnected,
|
|
259
|
-
onDisconnected: handleDisconnect
|
|
260
|
-
}
|
|
261
|
-
)
|
|
262
|
-
}
|
|
263
|
-
)
|
|
278
|
+
phone.audio
|
|
264
279
|
] });
|
|
265
280
|
}
|
|
266
281
|
// Annotate the CommonJS export names for ESM import in node:
|
|
267
282
|
0 && (module.exports = {
|
|
268
|
-
ThunderPhoneWidget
|
|
283
|
+
ThunderPhoneWidget,
|
|
284
|
+
useThunderPhone
|
|
269
285
|
});
|
|
270
286
|
//# sourceMappingURL=index.js.map
|