@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 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
- ## Quick Start
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 / Options
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
- ## Script Tag API
72
+ ## Domain Restrictions
101
73
 
102
- `ThunderPhone.mount()` returns a handle you can use to clean up:
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
- ```js
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
- // Later, to remove the widget:
115
- widget.unmount()
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
- ## Domain Restrictions
97
+ Use `latest` instead of a version number for the most recent release (cached for 5 minutes):
119
98
 
120
- 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.
99
+ ```
100
+ https://cdn.thunderphone.com/widget/latest/widget.js
101
+ https://cdn.thunderphone.com/widget/latest/style.css
102
+ ```
121
103
 
122
- Wildcard subdomains are supported: `*.example.com` matches `app.example.com`, `docs.example.com`, etc.
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
- export { ThunderPhoneWidget, type ThunderPhoneWidgetProps, type WidgetError, type WidgetState };
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
- export { ThunderPhoneWidget, type ThunderPhoneWidgetProps, type WidgetError, type WidgetState };
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 import_jsx_runtime2 = require("react/jsx-runtime");
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, import_jsx_runtime2.jsxs)("div", { className: "tp-button-group", children: [
80
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "tp-button tp-button--mute", onClick: onMuteToggle, type: "button", children: muted ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { className: "tp-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
81
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "1", y1: "1", x2: "23", y2: "23" }),
82
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6" }),
83
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M17 16.95A7 7 0 0 1 5 12v-2m14 0v2c0 .76-.13 1.49-.36 2.18" }),
84
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "12", y1: "19", x2: "12", y2: "23" }),
85
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "8", y1: "23", x2: "16", y2: "23" })
86
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { className: "tp-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
87
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }),
88
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
89
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "12", y1: "19", x2: "12", y2: "23" }),
90
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "8", y1: "23", x2: "16", y2: "23" })
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, import_jsx_runtime2.jsx)("button", { className: "tp-button tp-button--end", onClick, type: "button", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "tp-icon", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) }) })
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, import_jsx_runtime2.jsx)(
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, import_jsx_runtime2.jsx)("svg", { className: "tp-icon tp-spin", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { className: "tp-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
103
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }),
104
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
105
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "12", y1: "19", x2: "12", y2: "23" }),
106
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "8", y1: "23", x2: "16", y2: "23" })
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 import_react2 = require("react");
114
- var import_jsx_runtime3 = require("react/jsx-runtime");
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, import_react2.useState)(0);
117
- (0, import_react2.useEffect)(() => {
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, import_jsx_runtime3.jsxs)("div", { className: "tp-status", children: [
138
- agentName && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "tp-status__name", children: agentName }),
139
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `tp-status__text tp-status--${state}`, children: [
140
- state === "connected" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "tp-status__dot" }),
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/ThunderPhoneWidget.tsx
176
- var import_jsx_runtime4 = require("react/jsx-runtime");
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 [errorMessage, setErrorMessage] = (0, import_react3.useState)();
190
- const handleConnect = (0, import_react3.useCallback)(async () => {
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
- setErrorMessage(void 0);
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
- setErrorMessage(err.message);
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
- setErrorMessage("Unable to connect.");
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 handleDisconnect = (0, import_react3.useCallback)(() => {
209
- setState("disconnected");
210
- setSession(null);
211
- setMuted(false);
212
- onDisconnect?.();
213
- setTimeout(() => setState("idle"), 1500);
214
- }, [onDisconnect]);
215
- const handleAgentConnected = (0, import_react3.useCallback)(() => {
216
- setState("connected");
217
- onConnect?.();
218
- }, [onConnect]);
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
- handleDisconnect();
222
- } else if (state === "idle" || state === "error" || state === "disconnected") {
223
- handleConnect();
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: session?.agent_name || null,
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: handleMuteToggle
275
+ onMuteToggle: phone.toggleMute
245
276
  }
246
277
  ),
247
- session && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
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