@exotel-npm-dev/exotel-ai-assist 1.1.0 → 1.1.2
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 +50 -20
- package/dist/components/ExotelAIAssistApp.d.ts +3 -1
- package/dist/controller/index.d.ts +2 -0
- package/dist/controller/index.js +61 -18
- package/dist/hooks/useExotelAIAssist.d.ts +7 -0
- package/dist/index.js +75 -21
- package/dist/react/index.js +75 -21
- package/dist/transport/index.d.ts +5 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/utils/index.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,12 +113,14 @@ function AgentDashboard() {
|
|
|
113
113
|
import { useExotelAIAssist } from "@exotel-npm-dev/exotel-ai-assist/react";
|
|
114
114
|
|
|
115
115
|
function MyCustomUI({ call_sid }: { call_sid: string }) {
|
|
116
|
-
const { status, suggestions, transcripts, lastError } = useExotelAIAssist({
|
|
116
|
+
const { isReady, status, suggestions, transcripts, lastError } = useExotelAIAssist({
|
|
117
117
|
authToken: "your-auth-token",
|
|
118
118
|
call_sid,
|
|
119
119
|
accountId: "your-account-id",
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
if (!isReady) return <p>Connecting…</p>;
|
|
123
|
+
|
|
122
124
|
return (
|
|
123
125
|
<div>
|
|
124
126
|
<p>Status: {status}</p>
|
|
@@ -169,6 +171,16 @@ const ctrl = new ExotelAIAssistController({
|
|
|
169
171
|
authToken: "your-auth-token",
|
|
170
172
|
call_sid: "CALL-SID-001",
|
|
171
173
|
accountId: "your-account-id",
|
|
174
|
+
env: "production", // defaults to PRODUCTION if omitted
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
ctrl.on("onReady", (ready) => {
|
|
178
|
+
if (ready) {
|
|
179
|
+
console.log("Connection established and acknowledged by server");
|
|
180
|
+
// Safe to start consuming data / show the UI
|
|
181
|
+
} else {
|
|
182
|
+
console.log("Connection lost");
|
|
183
|
+
}
|
|
172
184
|
});
|
|
173
185
|
|
|
174
186
|
ctrl.on("suggestion", (s) => console.log("Suggestion:", s));
|
|
@@ -210,15 +222,16 @@ Extends `EventEmitter`.
|
|
|
210
222
|
|
|
211
223
|
#### Constructor options (`ExotelAIAssistParams`)
|
|
212
224
|
|
|
213
|
-
| Field | Type
|
|
214
|
-
| ---------------------- |
|
|
215
|
-
| `authToken` | `string`
|
|
216
|
-
| `call_sid` | `string`
|
|
217
|
-
| `accountId` | `string`
|
|
218
|
-
| `
|
|
219
|
-
| `
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
225
|
+
| Field | Type | Required | Default | Description |
|
|
226
|
+
| ---------------------- | ------------- | -------- | -------------------------- | ------------------------------------------------------------------ |
|
|
227
|
+
| `authToken` | `string` | ✓ | — | Bearer token |
|
|
228
|
+
| `call_sid` | `string` | ✓ | — | Active call SID |
|
|
229
|
+
| `accountId` | `string` | ✓ | — | Exotel account identifier |
|
|
230
|
+
| `env` | `"production" or "uat" or "development"` | — | `production` | Target environment (`production`, `uat`, or `development`) |
|
|
231
|
+
| `wssBaseUrl` | `string` | — | Derived from `env` | Full override for the WebSocket URL (takes precedence over `env`) |
|
|
232
|
+
| `reconnectInterval` | `number` | — | `3000` | Base reconnect delay in ms |
|
|
233
|
+
| `maxReconnectAttempts` | `number` | — | `5` | Max retries before error |
|
|
234
|
+
| `[customParam]` | `string` | — | — | Extra query params forwarded to the server (max 3) |
|
|
222
235
|
|
|
223
236
|
#### Methods
|
|
224
237
|
|
|
@@ -232,16 +245,33 @@ Extends `EventEmitter`.
|
|
|
232
245
|
|
|
233
246
|
#### Events
|
|
234
247
|
|
|
235
|
-
| Event | Payload | Description
|
|
236
|
-
| -------------- | ------------------ |
|
|
237
|
-
| `
|
|
238
|
-
| `
|
|
239
|
-
| `
|
|
240
|
-
| `
|
|
241
|
-
| `
|
|
242
|
-
| `
|
|
243
|
-
| `
|
|
244
|
-
| `
|
|
248
|
+
| Event | Payload | Description |
|
|
249
|
+
| -------------- | ------------------ | --------------------------------------------------------------------------------------------------------------- |
|
|
250
|
+
| `onReady` | `boolean` | `true` when connected and server sends `ack`; `false` on disconnect. Multi-tab safe — works for follower tabs too |
|
|
251
|
+
| `suggestion` | `Suggestion` | New AI suggestion (capped at last 50) |
|
|
252
|
+
| `transcript` | `TranscriptLine[]` | Live transcript update |
|
|
253
|
+
| `sentiment` | `Sentiment` | Sentiment label update |
|
|
254
|
+
| `onCallStart` | — | Connection opened |
|
|
255
|
+
| `onCallEnd` | — | Connection closed |
|
|
256
|
+
| `statusChange` | `ConnectionStatus` | Status transition |
|
|
257
|
+
| `error` | `Error` | Any error (auth, parse, max-reconnect) |
|
|
258
|
+
| `raw` | `unknown` | Every raw server message |
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
### `useExotelAIAssist(params)` — Hook return values
|
|
263
|
+
|
|
264
|
+
| Field | Type | Description |
|
|
265
|
+
| ------------- | ------------------ | ---------------------------------------------------------------------- |
|
|
266
|
+
| `isReady` | `boolean` | `true` after connected + server ack. Multi-tab safe. `false` on disconnect |
|
|
267
|
+
| `status` | `ConnectionStatus` | Current connection status |
|
|
268
|
+
| `suggestions` | `Suggestion[]` | AI suggestions, oldest first, capped at 50 |
|
|
269
|
+
| `transcripts` | `TranscriptLine[]` | Live transcript lines, ordered by start time |
|
|
270
|
+
| `sentiment` | `Sentiment \| null`| Latest sentiment reading |
|
|
271
|
+
| `lastError` | `Error \| null` | Most recent error |
|
|
272
|
+
| `connect()` | `() => void` | Manually open the connection |
|
|
273
|
+
| `disconnect()` | `() => void` | Manually close the connection |
|
|
274
|
+
| `setParams()` | `(patch) => void` | Merge params; reconnects if connection params change |
|
|
245
275
|
|
|
246
276
|
---
|
|
247
277
|
|
|
@@ -2,6 +2,8 @@ import { ExotelAIAssistParams } from "../types";
|
|
|
2
2
|
import "../styles/index.css";
|
|
3
3
|
export interface ExotelAIAssistProps extends ExotelAIAssistParams {
|
|
4
4
|
className?: string;
|
|
5
|
+
/** Called with `true` when the server acknowledges the connection, `false` on disconnect. */
|
|
6
|
+
onReady?: (ready: boolean) => void;
|
|
5
7
|
}
|
|
6
|
-
export declare function ExotelAIAssist({ className, ...params }: ExotelAIAssistProps): JSX.Element;
|
|
8
|
+
export declare function ExotelAIAssist({ className, onReady, ...params }: ExotelAIAssistProps): JSX.Element;
|
|
7
9
|
export { ExotelAIAssist as ExotelAIAssistApp };
|
|
@@ -6,6 +6,7 @@ export declare class ExotelAIAssistController extends EventEmitter<ControllerEve
|
|
|
6
6
|
private status;
|
|
7
7
|
private destroyed;
|
|
8
8
|
private connectionEstablished;
|
|
9
|
+
private readyFired;
|
|
9
10
|
private reconnectAttempt;
|
|
10
11
|
private reconnectTimer;
|
|
11
12
|
private static readonly MAX_RECONNECT_ATTEMPTS;
|
|
@@ -20,6 +21,7 @@ export declare class ExotelAIAssistController extends EventEmitter<ControllerEve
|
|
|
20
21
|
private _handleTransportMessage;
|
|
21
22
|
private _scheduleReconnect;
|
|
22
23
|
private _clearReconnectTimer;
|
|
24
|
+
private _fireReady;
|
|
23
25
|
private _handleServerPayload;
|
|
24
26
|
private _setStatus;
|
|
25
27
|
}
|
package/dist/controller/index.js
CHANGED
|
@@ -343,6 +343,7 @@ class BroadcastChannelTransport {
|
|
|
343
343
|
this.pendingUrl = null;
|
|
344
344
|
this.pendingAuthToken = null;
|
|
345
345
|
this.lockReleaser = null;
|
|
346
|
+
this.ackReceived = false;
|
|
346
347
|
this._destroyed = false;
|
|
347
348
|
const channelName = `exotel-ai-assist:${sessionHash}`;
|
|
348
349
|
const lockName = `exotel-ai-assist-leader:${sessionHash}`;
|
|
@@ -351,12 +352,15 @@ class BroadcastChannelTransport {
|
|
|
351
352
|
var _a;
|
|
352
353
|
const data = ev.data;
|
|
353
354
|
if (this.isLeader) {
|
|
354
|
-
// A new follower tab has opened. If our socket is already connected,
|
|
355
|
-
// broadcast CONNECTED so the follower can sync its status immediately.
|
|
356
355
|
if (data.type === "JOIN" && this.socketConnected) {
|
|
357
356
|
this.channel.postMessage({
|
|
358
357
|
type: "CONNECTED"
|
|
359
358
|
});
|
|
359
|
+
if (this.ackReceived) {
|
|
360
|
+
this.channel.postMessage({
|
|
361
|
+
type: "ACKNOWLEDGED"
|
|
362
|
+
});
|
|
363
|
+
}
|
|
360
364
|
}
|
|
361
365
|
return;
|
|
362
366
|
}
|
|
@@ -411,6 +415,7 @@ class BroadcastChannelTransport {
|
|
|
411
415
|
var _a;
|
|
412
416
|
if (this._destroyed) return;
|
|
413
417
|
this.socketConnected = true;
|
|
418
|
+
this.ackReceived = false;
|
|
414
419
|
this.socket.send(JSON.stringify({
|
|
415
420
|
type: "auth",
|
|
416
421
|
access_token: authToken
|
|
@@ -445,6 +450,7 @@ class BroadcastChannelTransport {
|
|
|
445
450
|
var _a;
|
|
446
451
|
if (this._destroyed) return;
|
|
447
452
|
this.socketConnected = false;
|
|
453
|
+
this.ackReceived = false;
|
|
448
454
|
const msg = {
|
|
449
455
|
type: "DISCONNECTED",
|
|
450
456
|
code: ev.code
|
|
@@ -477,6 +483,9 @@ class BroadcastChannelTransport {
|
|
|
477
483
|
onMessage(handler) {
|
|
478
484
|
this.handler = handler;
|
|
479
485
|
}
|
|
486
|
+
markAcknowledged() {
|
|
487
|
+
this.ackReceived = true;
|
|
488
|
+
}
|
|
480
489
|
destroy() {
|
|
481
490
|
var _a;
|
|
482
491
|
this._destroyed = true;
|
|
@@ -1497,6 +1506,18 @@ function requireObject_hash() {
|
|
|
1497
1506
|
var object_hashExports = requireObject_hash();
|
|
1498
1507
|
var objectHash = /*@__PURE__*/getDefaultExportFromCjs(object_hashExports);
|
|
1499
1508
|
|
|
1509
|
+
var Environment;
|
|
1510
|
+
(function (Environment) {
|
|
1511
|
+
Environment["PRODUCTION"] = "production";
|
|
1512
|
+
Environment["UAT"] = "uat";
|
|
1513
|
+
Environment["DEVELOPMENT"] = "development";
|
|
1514
|
+
})(Environment || (Environment = {}));
|
|
1515
|
+
const WssBaseUrl = {
|
|
1516
|
+
[Environment.PRODUCTION]: "wss://ai-assist.in.exotel.com",
|
|
1517
|
+
[Environment.UAT]: "wss://oneassist-uat.in.exotel.com",
|
|
1518
|
+
[Environment.DEVELOPMENT]: "ws://localhost:8081"
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1500
1521
|
class Utils {
|
|
1501
1522
|
/**
|
|
1502
1523
|
* Returns a deterministic SHA-1 hex digest for any plain object.
|
|
@@ -1528,10 +1549,16 @@ class Utils {
|
|
|
1528
1549
|
* @param accountId - The account ID to get the base WebSocket URL for.
|
|
1529
1550
|
* @returns The base WebSocket URL.
|
|
1530
1551
|
*/
|
|
1531
|
-
static getWssBaseUrl(accountId) {
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1552
|
+
static getWssBaseUrl(accountId, env) {
|
|
1553
|
+
if (env === Environment.DEVELOPMENT) {
|
|
1554
|
+
return `${WssBaseUrl[Environment.DEVELOPMENT]}/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
|
|
1555
|
+
} else if (env === Environment.UAT) {
|
|
1556
|
+
return `${WssBaseUrl[Environment.UAT]}/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
|
|
1557
|
+
} else if (env === Environment.PRODUCTION) {
|
|
1558
|
+
return `${WssBaseUrl[Environment.PRODUCTION]}/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
|
|
1559
|
+
} else {
|
|
1560
|
+
return `${WssBaseUrl[Environment.PRODUCTION]}/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
|
|
1561
|
+
}
|
|
1535
1562
|
}
|
|
1536
1563
|
/**
|
|
1537
1564
|
* Builds a WebSocket URL from the given parameters.
|
|
@@ -1539,17 +1566,18 @@ class Utils {
|
|
|
1539
1566
|
* @returns The WebSocket URL.
|
|
1540
1567
|
*/
|
|
1541
1568
|
static buildWsUrl(params) {
|
|
1569
|
+
var _a;
|
|
1542
1570
|
const {
|
|
1543
1571
|
wssBaseUrl
|
|
1544
1572
|
} = params;
|
|
1545
|
-
const resolved = wssBaseUrl !== null && wssBaseUrl !== void 0 ? wssBaseUrl : Utils.getWssBaseUrl(params.accountId);
|
|
1573
|
+
const resolved = wssBaseUrl !== null && wssBaseUrl !== void 0 ? wssBaseUrl : Utils.getWssBaseUrl(params.accountId, (_a = params === null || params === void 0 ? void 0 : params.env) !== null && _a !== void 0 ? _a : Environment.PRODUCTION);
|
|
1546
1574
|
const base = resolved.endsWith("/") ? resolved.slice(0, -1) : resolved;
|
|
1547
1575
|
const query = Utils.constructQueryParams(params);
|
|
1548
1576
|
return `${base}?${query}`;
|
|
1549
1577
|
}
|
|
1550
1578
|
static constructQueryParams(params) {
|
|
1551
1579
|
// Keys consumed by the library — never forwarded to the server.
|
|
1552
|
-
const INTERNAL_KEYS = new Set(["authToken", "accountId", "wssBaseUrl", "reconnectInterval", "maxReconnectAttempts"]);
|
|
1580
|
+
const INTERNAL_KEYS = new Set(["authToken", "accountId", "wssBaseUrl", "reconnectInterval", "maxReconnectAttempts", "env"]);
|
|
1553
1581
|
// Mandatory server params remapped to the names the API expects.
|
|
1554
1582
|
// accountId is embedded in the URL path — never sent as a query param.
|
|
1555
1583
|
const pairs = [];
|
|
@@ -1595,6 +1623,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1595
1623
|
// connectionEstablished === false → connection never opened → retry
|
|
1596
1624
|
// connectionEstablished === true → server ended a live connection → destroy
|
|
1597
1625
|
this.connectionEstablished = false;
|
|
1626
|
+
this.readyFired = false;
|
|
1598
1627
|
this.reconnectAttempt = 0;
|
|
1599
1628
|
this.reconnectTimer = null;
|
|
1600
1629
|
this.params = Object.assign({}, params);
|
|
@@ -1603,6 +1632,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1603
1632
|
if (this.destroyed) return;
|
|
1604
1633
|
this._clearReconnectTimer();
|
|
1605
1634
|
this.connectionEstablished = false;
|
|
1635
|
+
this.readyFired = false;
|
|
1606
1636
|
this.reconnectAttempt = 0;
|
|
1607
1637
|
this._setStatus("connecting");
|
|
1608
1638
|
this._ensureTransport();
|
|
@@ -1612,6 +1642,10 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1612
1642
|
var _a;
|
|
1613
1643
|
this._clearReconnectTimer();
|
|
1614
1644
|
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
1645
|
+
if (this.readyFired) {
|
|
1646
|
+
this.readyFired = false;
|
|
1647
|
+
this.emit("onReady", false);
|
|
1648
|
+
}
|
|
1615
1649
|
this._setStatus("disconnected");
|
|
1616
1650
|
}
|
|
1617
1651
|
setParams(patch) {
|
|
@@ -1622,6 +1656,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1622
1656
|
if (hashChanged) {
|
|
1623
1657
|
this._clearReconnectTimer();
|
|
1624
1658
|
this.connectionEstablished = false;
|
|
1659
|
+
this.readyFired = false;
|
|
1625
1660
|
this.reconnectAttempt = 0;
|
|
1626
1661
|
// Tear down the current transport and start fresh with the new params.
|
|
1627
1662
|
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
@@ -1662,22 +1697,23 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1662
1697
|
this._setStatus("connected");
|
|
1663
1698
|
this.emit("onCallStart");
|
|
1664
1699
|
break;
|
|
1700
|
+
case "ACKNOWLEDGED":
|
|
1701
|
+
this._fireReady();
|
|
1702
|
+
break;
|
|
1665
1703
|
case "DISCONNECTED":
|
|
1666
1704
|
if (this.connectionEstablished) {
|
|
1667
|
-
|
|
1668
|
-
|
|
1705
|
+
if (this.readyFired) {
|
|
1706
|
+
this.readyFired = false;
|
|
1707
|
+
this.emit("onReady", false);
|
|
1708
|
+
}
|
|
1669
1709
|
this._setStatus("disconnected");
|
|
1670
1710
|
this.emit("onCallEnd");
|
|
1671
1711
|
this.destroy();
|
|
1672
1712
|
} else {
|
|
1673
|
-
// WebSocket never opened — network/DNS/refused error.
|
|
1674
|
-
// Retry up to MAX_RECONNECT_ATTEMPTS then give up.
|
|
1675
1713
|
this._scheduleReconnect();
|
|
1676
1714
|
}
|
|
1677
1715
|
break;
|
|
1678
1716
|
case "ERROR":
|
|
1679
|
-
// onerror fires before onclose; DISCONNECTED will arrive right after
|
|
1680
|
-
// and drive the retry / destroy decision. Just surface the error here.
|
|
1681
1717
|
this.emit("error", new Error((_a = msg.message) !== null && _a !== void 0 ? _a : "Unknown transport error"));
|
|
1682
1718
|
break;
|
|
1683
1719
|
case "MESSAGE":
|
|
@@ -1711,18 +1747,21 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1711
1747
|
this.reconnectTimer = null;
|
|
1712
1748
|
}
|
|
1713
1749
|
}
|
|
1750
|
+
_fireReady() {
|
|
1751
|
+
if (this.readyFired) return;
|
|
1752
|
+
this.readyFired = true;
|
|
1753
|
+
this.emit("onReady", true);
|
|
1754
|
+
}
|
|
1714
1755
|
_handleServerPayload(raw) {
|
|
1715
|
-
var _a;
|
|
1756
|
+
var _a, _b;
|
|
1716
1757
|
let parsed;
|
|
1717
1758
|
try {
|
|
1718
1759
|
parsed = JSON.parse(raw);
|
|
1719
|
-
} catch (
|
|
1760
|
+
} catch (_c) {
|
|
1720
1761
|
this.emit("error", new Error(`Failed to parse server message: ${raw}`));
|
|
1721
1762
|
return;
|
|
1722
1763
|
}
|
|
1723
1764
|
this.emit("raw", parsed);
|
|
1724
|
-
// Auth error messages arrive before the server closes the connection.
|
|
1725
|
-
// Treat both as fatal — the server will follow up with a 4001/4002 close.
|
|
1726
1765
|
const msgType = parsed.type;
|
|
1727
1766
|
if (msgType === "auth_failed" || msgType === "auth_timeout") {
|
|
1728
1767
|
const detail = (_a = parsed.error) !== null && _a !== void 0 ? _a : msgType;
|
|
@@ -1732,6 +1771,10 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1732
1771
|
this.emit("error", err);
|
|
1733
1772
|
return;
|
|
1734
1773
|
}
|
|
1774
|
+
if (msgType === "ack" && !this.readyFired) {
|
|
1775
|
+
(_b = this.transport) === null || _b === void 0 ? void 0 : _b.markAcknowledged();
|
|
1776
|
+
this._fireReady();
|
|
1777
|
+
}
|
|
1735
1778
|
if (parsed.config) {
|
|
1736
1779
|
this.emit("botConfig", parsed.config);
|
|
1737
1780
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { ExotelAIAssistParams, Suggestion, TranscriptLine, Sentiment, BotConfig, ConnectionStatus } from "../types";
|
|
2
2
|
export interface UseExotelAIAssistReturn {
|
|
3
3
|
status: ConnectionStatus;
|
|
4
|
+
/**
|
|
5
|
+
* `true` once the WebSocket connection is established and the server has
|
|
6
|
+
* acknowledged it. In multi-tab scenarios where this tab joins an
|
|
7
|
+
* already-acknowledged session, it becomes `true` immediately.
|
|
8
|
+
* Resets to `false` on disconnect.
|
|
9
|
+
*/
|
|
10
|
+
isReady: boolean;
|
|
4
11
|
/** AI suggestions, oldest first, capped at 50. */
|
|
5
12
|
suggestions: Suggestion[];
|
|
6
13
|
/** Live transcript lines, ordered by start time. */
|
package/dist/index.js
CHANGED
|
@@ -19761,6 +19761,7 @@ class BroadcastChannelTransport {
|
|
|
19761
19761
|
this.pendingUrl = null;
|
|
19762
19762
|
this.pendingAuthToken = null;
|
|
19763
19763
|
this.lockReleaser = null;
|
|
19764
|
+
this.ackReceived = false;
|
|
19764
19765
|
this._destroyed = false;
|
|
19765
19766
|
const channelName = `exotel-ai-assist:${sessionHash}`;
|
|
19766
19767
|
const lockName = `exotel-ai-assist-leader:${sessionHash}`;
|
|
@@ -19769,12 +19770,15 @@ class BroadcastChannelTransport {
|
|
|
19769
19770
|
var _a;
|
|
19770
19771
|
const data = ev.data;
|
|
19771
19772
|
if (this.isLeader) {
|
|
19772
|
-
// A new follower tab has opened. If our socket is already connected,
|
|
19773
|
-
// broadcast CONNECTED so the follower can sync its status immediately.
|
|
19774
19773
|
if (data.type === "JOIN" && this.socketConnected) {
|
|
19775
19774
|
this.channel.postMessage({
|
|
19776
19775
|
type: "CONNECTED"
|
|
19777
19776
|
});
|
|
19777
|
+
if (this.ackReceived) {
|
|
19778
|
+
this.channel.postMessage({
|
|
19779
|
+
type: "ACKNOWLEDGED"
|
|
19780
|
+
});
|
|
19781
|
+
}
|
|
19778
19782
|
}
|
|
19779
19783
|
return;
|
|
19780
19784
|
}
|
|
@@ -19829,6 +19833,7 @@ class BroadcastChannelTransport {
|
|
|
19829
19833
|
var _a;
|
|
19830
19834
|
if (this._destroyed) return;
|
|
19831
19835
|
this.socketConnected = true;
|
|
19836
|
+
this.ackReceived = false;
|
|
19832
19837
|
this.socket.send(JSON.stringify({
|
|
19833
19838
|
type: "auth",
|
|
19834
19839
|
access_token: authToken
|
|
@@ -19863,6 +19868,7 @@ class BroadcastChannelTransport {
|
|
|
19863
19868
|
var _a;
|
|
19864
19869
|
if (this._destroyed) return;
|
|
19865
19870
|
this.socketConnected = false;
|
|
19871
|
+
this.ackReceived = false;
|
|
19866
19872
|
const msg = {
|
|
19867
19873
|
type: "DISCONNECTED",
|
|
19868
19874
|
code: ev.code
|
|
@@ -19895,6 +19901,9 @@ class BroadcastChannelTransport {
|
|
|
19895
19901
|
onMessage(handler) {
|
|
19896
19902
|
this.handler = handler;
|
|
19897
19903
|
}
|
|
19904
|
+
markAcknowledged() {
|
|
19905
|
+
this.ackReceived = true;
|
|
19906
|
+
}
|
|
19898
19907
|
destroy() {
|
|
19899
19908
|
var _a;
|
|
19900
19909
|
this._destroyed = true;
|
|
@@ -20915,6 +20924,18 @@ function requireObject_hash() {
|
|
|
20915
20924
|
var object_hashExports = requireObject_hash();
|
|
20916
20925
|
var objectHash = /*@__PURE__*/getDefaultExportFromCjs(object_hashExports);
|
|
20917
20926
|
|
|
20927
|
+
var Environment;
|
|
20928
|
+
(function (Environment) {
|
|
20929
|
+
Environment["PRODUCTION"] = "production";
|
|
20930
|
+
Environment["UAT"] = "uat";
|
|
20931
|
+
Environment["DEVELOPMENT"] = "development";
|
|
20932
|
+
})(Environment || (Environment = {}));
|
|
20933
|
+
const WssBaseUrl = {
|
|
20934
|
+
[Environment.PRODUCTION]: "wss://ai-assist.in.exotel.com",
|
|
20935
|
+
[Environment.UAT]: "wss://oneassist-uat.in.exotel.com",
|
|
20936
|
+
[Environment.DEVELOPMENT]: "ws://localhost:8081"
|
|
20937
|
+
};
|
|
20938
|
+
|
|
20918
20939
|
class Utils {
|
|
20919
20940
|
/**
|
|
20920
20941
|
* Returns a deterministic SHA-1 hex digest for any plain object.
|
|
@@ -20946,10 +20967,16 @@ class Utils {
|
|
|
20946
20967
|
* @param accountId - The account ID to get the base WebSocket URL for.
|
|
20947
20968
|
* @returns The base WebSocket URL.
|
|
20948
20969
|
*/
|
|
20949
|
-
static getWssBaseUrl(accountId) {
|
|
20950
|
-
|
|
20951
|
-
|
|
20952
|
-
|
|
20970
|
+
static getWssBaseUrl(accountId, env) {
|
|
20971
|
+
if (env === Environment.DEVELOPMENT) {
|
|
20972
|
+
return `${WssBaseUrl[Environment.DEVELOPMENT]}/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
|
|
20973
|
+
} else if (env === Environment.UAT) {
|
|
20974
|
+
return `${WssBaseUrl[Environment.UAT]}/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
|
|
20975
|
+
} else if (env === Environment.PRODUCTION) {
|
|
20976
|
+
return `${WssBaseUrl[Environment.PRODUCTION]}/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
|
|
20977
|
+
} else {
|
|
20978
|
+
return `${WssBaseUrl[Environment.PRODUCTION]}/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
|
|
20979
|
+
}
|
|
20953
20980
|
}
|
|
20954
20981
|
/**
|
|
20955
20982
|
* Builds a WebSocket URL from the given parameters.
|
|
@@ -20957,17 +20984,18 @@ class Utils {
|
|
|
20957
20984
|
* @returns The WebSocket URL.
|
|
20958
20985
|
*/
|
|
20959
20986
|
static buildWsUrl(params) {
|
|
20987
|
+
var _a;
|
|
20960
20988
|
const {
|
|
20961
20989
|
wssBaseUrl
|
|
20962
20990
|
} = params;
|
|
20963
|
-
const resolved = wssBaseUrl !== null && wssBaseUrl !== void 0 ? wssBaseUrl : Utils.getWssBaseUrl(params.accountId);
|
|
20991
|
+
const resolved = wssBaseUrl !== null && wssBaseUrl !== void 0 ? wssBaseUrl : Utils.getWssBaseUrl(params.accountId, (_a = params === null || params === void 0 ? void 0 : params.env) !== null && _a !== void 0 ? _a : Environment.PRODUCTION);
|
|
20964
20992
|
const base = resolved.endsWith("/") ? resolved.slice(0, -1) : resolved;
|
|
20965
20993
|
const query = Utils.constructQueryParams(params);
|
|
20966
20994
|
return `${base}?${query}`;
|
|
20967
20995
|
}
|
|
20968
20996
|
static constructQueryParams(params) {
|
|
20969
20997
|
// Keys consumed by the library — never forwarded to the server.
|
|
20970
|
-
const INTERNAL_KEYS = new Set(["authToken", "accountId", "wssBaseUrl", "reconnectInterval", "maxReconnectAttempts"]);
|
|
20998
|
+
const INTERNAL_KEYS = new Set(["authToken", "accountId", "wssBaseUrl", "reconnectInterval", "maxReconnectAttempts", "env"]);
|
|
20971
20999
|
// Mandatory server params remapped to the names the API expects.
|
|
20972
21000
|
// accountId is embedded in the URL path — never sent as a query param.
|
|
20973
21001
|
const pairs = [];
|
|
@@ -21013,6 +21041,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21013
21041
|
// connectionEstablished === false → connection never opened → retry
|
|
21014
21042
|
// connectionEstablished === true → server ended a live connection → destroy
|
|
21015
21043
|
this.connectionEstablished = false;
|
|
21044
|
+
this.readyFired = false;
|
|
21016
21045
|
this.reconnectAttempt = 0;
|
|
21017
21046
|
this.reconnectTimer = null;
|
|
21018
21047
|
this.params = Object.assign({}, params);
|
|
@@ -21021,6 +21050,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21021
21050
|
if (this.destroyed) return;
|
|
21022
21051
|
this._clearReconnectTimer();
|
|
21023
21052
|
this.connectionEstablished = false;
|
|
21053
|
+
this.readyFired = false;
|
|
21024
21054
|
this.reconnectAttempt = 0;
|
|
21025
21055
|
this._setStatus("connecting");
|
|
21026
21056
|
this._ensureTransport();
|
|
@@ -21030,6 +21060,10 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21030
21060
|
var _a;
|
|
21031
21061
|
this._clearReconnectTimer();
|
|
21032
21062
|
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
21063
|
+
if (this.readyFired) {
|
|
21064
|
+
this.readyFired = false;
|
|
21065
|
+
this.emit("onReady", false);
|
|
21066
|
+
}
|
|
21033
21067
|
this._setStatus("disconnected");
|
|
21034
21068
|
}
|
|
21035
21069
|
setParams(patch) {
|
|
@@ -21040,6 +21074,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21040
21074
|
if (hashChanged) {
|
|
21041
21075
|
this._clearReconnectTimer();
|
|
21042
21076
|
this.connectionEstablished = false;
|
|
21077
|
+
this.readyFired = false;
|
|
21043
21078
|
this.reconnectAttempt = 0;
|
|
21044
21079
|
// Tear down the current transport and start fresh with the new params.
|
|
21045
21080
|
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
@@ -21080,22 +21115,23 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21080
21115
|
this._setStatus("connected");
|
|
21081
21116
|
this.emit("onCallStart");
|
|
21082
21117
|
break;
|
|
21118
|
+
case "ACKNOWLEDGED":
|
|
21119
|
+
this._fireReady();
|
|
21120
|
+
break;
|
|
21083
21121
|
case "DISCONNECTED":
|
|
21084
21122
|
if (this.connectionEstablished) {
|
|
21085
|
-
|
|
21086
|
-
|
|
21123
|
+
if (this.readyFired) {
|
|
21124
|
+
this.readyFired = false;
|
|
21125
|
+
this.emit("onReady", false);
|
|
21126
|
+
}
|
|
21087
21127
|
this._setStatus("disconnected");
|
|
21088
21128
|
this.emit("onCallEnd");
|
|
21089
21129
|
this.destroy();
|
|
21090
21130
|
} else {
|
|
21091
|
-
// WebSocket never opened — network/DNS/refused error.
|
|
21092
|
-
// Retry up to MAX_RECONNECT_ATTEMPTS then give up.
|
|
21093
21131
|
this._scheduleReconnect();
|
|
21094
21132
|
}
|
|
21095
21133
|
break;
|
|
21096
21134
|
case "ERROR":
|
|
21097
|
-
// onerror fires before onclose; DISCONNECTED will arrive right after
|
|
21098
|
-
// and drive the retry / destroy decision. Just surface the error here.
|
|
21099
21135
|
this.emit("error", new Error((_a = msg.message) !== null && _a !== void 0 ? _a : "Unknown transport error"));
|
|
21100
21136
|
break;
|
|
21101
21137
|
case "MESSAGE":
|
|
@@ -21129,18 +21165,21 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21129
21165
|
this.reconnectTimer = null;
|
|
21130
21166
|
}
|
|
21131
21167
|
}
|
|
21168
|
+
_fireReady() {
|
|
21169
|
+
if (this.readyFired) return;
|
|
21170
|
+
this.readyFired = true;
|
|
21171
|
+
this.emit("onReady", true);
|
|
21172
|
+
}
|
|
21132
21173
|
_handleServerPayload(raw) {
|
|
21133
|
-
var _a;
|
|
21174
|
+
var _a, _b;
|
|
21134
21175
|
let parsed;
|
|
21135
21176
|
try {
|
|
21136
21177
|
parsed = JSON.parse(raw);
|
|
21137
|
-
} catch (
|
|
21178
|
+
} catch (_c) {
|
|
21138
21179
|
this.emit("error", new Error(`Failed to parse server message: ${raw}`));
|
|
21139
21180
|
return;
|
|
21140
21181
|
}
|
|
21141
21182
|
this.emit("raw", parsed);
|
|
21142
|
-
// Auth error messages arrive before the server closes the connection.
|
|
21143
|
-
// Treat both as fatal — the server will follow up with a 4001/4002 close.
|
|
21144
21183
|
const msgType = parsed.type;
|
|
21145
21184
|
if (msgType === "auth_failed" || msgType === "auth_timeout") {
|
|
21146
21185
|
const detail = (_a = parsed.error) !== null && _a !== void 0 ? _a : msgType;
|
|
@@ -21150,6 +21189,10 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21150
21189
|
this.emit("error", err);
|
|
21151
21190
|
return;
|
|
21152
21191
|
}
|
|
21192
|
+
if (msgType === "ack" && !this.readyFired) {
|
|
21193
|
+
(_b = this.transport) === null || _b === void 0 ? void 0 : _b.markAcknowledged();
|
|
21194
|
+
this._fireReady();
|
|
21195
|
+
}
|
|
21153
21196
|
if (parsed.config) {
|
|
21154
21197
|
this.emit("botConfig", parsed.config);
|
|
21155
21198
|
}
|
|
@@ -21203,6 +21246,7 @@ ExotelAIAssistController.RECONNECT_BASE_MS = 3000;
|
|
|
21203
21246
|
function useExotelAIAssist(params) {
|
|
21204
21247
|
const controllerRef = reactExports.useRef(null);
|
|
21205
21248
|
const [status, setStatus] = reactExports.useState("idle");
|
|
21249
|
+
const [isReady, setIsReady] = reactExports.useState(false);
|
|
21206
21250
|
const [suggestions, setSuggestions] = reactExports.useState([]);
|
|
21207
21251
|
const [transcripts, setTranscripts] = reactExports.useState([]);
|
|
21208
21252
|
const [sentiment, setSentiment] = reactExports.useState(null);
|
|
@@ -21210,8 +21254,8 @@ function useExotelAIAssist(params) {
|
|
|
21210
21254
|
const [lastError, setLastError] = reactExports.useState(null);
|
|
21211
21255
|
const paramHash = Utils.hash(params);
|
|
21212
21256
|
reactExports.useEffect(() => {
|
|
21213
|
-
// Clear all stale state from the previous session before connecting.
|
|
21214
21257
|
setStatus("idle");
|
|
21258
|
+
setIsReady(false);
|
|
21215
21259
|
setSuggestions([]);
|
|
21216
21260
|
setTranscripts([]);
|
|
21217
21261
|
setSentiment(null);
|
|
@@ -21220,6 +21264,7 @@ function useExotelAIAssist(params) {
|
|
|
21220
21264
|
const ctrl = new ExotelAIAssistController(params);
|
|
21221
21265
|
controllerRef.current = ctrl;
|
|
21222
21266
|
ctrl.on("statusChange", setStatus);
|
|
21267
|
+
ctrl.on("onReady", setIsReady);
|
|
21223
21268
|
ctrl.on("botConfig", setBotConfig);
|
|
21224
21269
|
ctrl.on("suggestion", s => setSuggestions(prev => [...prev, s].slice(-50)));
|
|
21225
21270
|
ctrl.on("sentiment", setSentiment);
|
|
@@ -21250,6 +21295,7 @@ function useExotelAIAssist(params) {
|
|
|
21250
21295
|
}, []);
|
|
21251
21296
|
return {
|
|
21252
21297
|
status,
|
|
21298
|
+
isReady,
|
|
21253
21299
|
suggestions,
|
|
21254
21300
|
transcripts,
|
|
21255
21301
|
sentiment,
|
|
@@ -21800,16 +21846,24 @@ function TranscriptTab({
|
|
|
21800
21846
|
|
|
21801
21847
|
function ExotelAIAssist(_a) {
|
|
21802
21848
|
var {
|
|
21803
|
-
className
|
|
21849
|
+
className,
|
|
21850
|
+
onReady
|
|
21804
21851
|
} = _a,
|
|
21805
|
-
params = __rest(_a, ["className"]);
|
|
21852
|
+
params = __rest(_a, ["className", "onReady"]);
|
|
21806
21853
|
const {
|
|
21807
21854
|
status,
|
|
21855
|
+
isReady,
|
|
21808
21856
|
suggestions,
|
|
21809
21857
|
transcripts,
|
|
21810
21858
|
sentiment,
|
|
21811
21859
|
botConfig
|
|
21812
21860
|
} = useExotelAIAssist(params);
|
|
21861
|
+
const onReadyRef = reactExports.useRef(onReady);
|
|
21862
|
+
onReadyRef.current = onReady;
|
|
21863
|
+
reactExports.useEffect(() => {
|
|
21864
|
+
var _a;
|
|
21865
|
+
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef, isReady);
|
|
21866
|
+
}, [isReady]);
|
|
21813
21867
|
const connected = status === "connected";
|
|
21814
21868
|
return jsxRuntimeExports.jsx(Provider, {
|
|
21815
21869
|
children: jsxRuntimeExports.jsx("div", {
|
package/dist/react/index.js
CHANGED
|
@@ -5047,6 +5047,7 @@ class BroadcastChannelTransport {
|
|
|
5047
5047
|
this.pendingUrl = null;
|
|
5048
5048
|
this.pendingAuthToken = null;
|
|
5049
5049
|
this.lockReleaser = null;
|
|
5050
|
+
this.ackReceived = false;
|
|
5050
5051
|
this._destroyed = false;
|
|
5051
5052
|
const channelName = `exotel-ai-assist:${sessionHash}`;
|
|
5052
5053
|
const lockName = `exotel-ai-assist-leader:${sessionHash}`;
|
|
@@ -5055,12 +5056,15 @@ class BroadcastChannelTransport {
|
|
|
5055
5056
|
var _a;
|
|
5056
5057
|
const data = ev.data;
|
|
5057
5058
|
if (this.isLeader) {
|
|
5058
|
-
// A new follower tab has opened. If our socket is already connected,
|
|
5059
|
-
// broadcast CONNECTED so the follower can sync its status immediately.
|
|
5060
5059
|
if (data.type === "JOIN" && this.socketConnected) {
|
|
5061
5060
|
this.channel.postMessage({
|
|
5062
5061
|
type: "CONNECTED"
|
|
5063
5062
|
});
|
|
5063
|
+
if (this.ackReceived) {
|
|
5064
|
+
this.channel.postMessage({
|
|
5065
|
+
type: "ACKNOWLEDGED"
|
|
5066
|
+
});
|
|
5067
|
+
}
|
|
5064
5068
|
}
|
|
5065
5069
|
return;
|
|
5066
5070
|
}
|
|
@@ -5115,6 +5119,7 @@ class BroadcastChannelTransport {
|
|
|
5115
5119
|
var _a;
|
|
5116
5120
|
if (this._destroyed) return;
|
|
5117
5121
|
this.socketConnected = true;
|
|
5122
|
+
this.ackReceived = false;
|
|
5118
5123
|
this.socket.send(JSON.stringify({
|
|
5119
5124
|
type: "auth",
|
|
5120
5125
|
access_token: authToken
|
|
@@ -5149,6 +5154,7 @@ class BroadcastChannelTransport {
|
|
|
5149
5154
|
var _a;
|
|
5150
5155
|
if (this._destroyed) return;
|
|
5151
5156
|
this.socketConnected = false;
|
|
5157
|
+
this.ackReceived = false;
|
|
5152
5158
|
const msg = {
|
|
5153
5159
|
type: "DISCONNECTED",
|
|
5154
5160
|
code: ev.code
|
|
@@ -5181,6 +5187,9 @@ class BroadcastChannelTransport {
|
|
|
5181
5187
|
onMessage(handler) {
|
|
5182
5188
|
this.handler = handler;
|
|
5183
5189
|
}
|
|
5190
|
+
markAcknowledged() {
|
|
5191
|
+
this.ackReceived = true;
|
|
5192
|
+
}
|
|
5184
5193
|
destroy() {
|
|
5185
5194
|
var _a;
|
|
5186
5195
|
this._destroyed = true;
|
|
@@ -6201,6 +6210,18 @@ function requireObject_hash() {
|
|
|
6201
6210
|
var object_hashExports = requireObject_hash();
|
|
6202
6211
|
var objectHash = /*@__PURE__*/getDefaultExportFromCjs(object_hashExports);
|
|
6203
6212
|
|
|
6213
|
+
var Environment;
|
|
6214
|
+
(function (Environment) {
|
|
6215
|
+
Environment["PRODUCTION"] = "production";
|
|
6216
|
+
Environment["UAT"] = "uat";
|
|
6217
|
+
Environment["DEVELOPMENT"] = "development";
|
|
6218
|
+
})(Environment || (Environment = {}));
|
|
6219
|
+
const WssBaseUrl = {
|
|
6220
|
+
[Environment.PRODUCTION]: "wss://ai-assist.in.exotel.com",
|
|
6221
|
+
[Environment.UAT]: "wss://oneassist-uat.in.exotel.com",
|
|
6222
|
+
[Environment.DEVELOPMENT]: "ws://localhost:8081"
|
|
6223
|
+
};
|
|
6224
|
+
|
|
6204
6225
|
class Utils {
|
|
6205
6226
|
/**
|
|
6206
6227
|
* Returns a deterministic SHA-1 hex digest for any plain object.
|
|
@@ -6232,10 +6253,16 @@ class Utils {
|
|
|
6232
6253
|
* @param accountId - The account ID to get the base WebSocket URL for.
|
|
6233
6254
|
* @returns The base WebSocket URL.
|
|
6234
6255
|
*/
|
|
6235
|
-
static getWssBaseUrl(accountId) {
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6256
|
+
static getWssBaseUrl(accountId, env) {
|
|
6257
|
+
if (env === Environment.DEVELOPMENT) {
|
|
6258
|
+
return `${WssBaseUrl[Environment.DEVELOPMENT]}/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
|
|
6259
|
+
} else if (env === Environment.UAT) {
|
|
6260
|
+
return `${WssBaseUrl[Environment.UAT]}/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
|
|
6261
|
+
} else if (env === Environment.PRODUCTION) {
|
|
6262
|
+
return `${WssBaseUrl[Environment.PRODUCTION]}/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
|
|
6263
|
+
} else {
|
|
6264
|
+
return `${WssBaseUrl[Environment.PRODUCTION]}/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
|
|
6265
|
+
}
|
|
6239
6266
|
}
|
|
6240
6267
|
/**
|
|
6241
6268
|
* Builds a WebSocket URL from the given parameters.
|
|
@@ -6243,17 +6270,18 @@ class Utils {
|
|
|
6243
6270
|
* @returns The WebSocket URL.
|
|
6244
6271
|
*/
|
|
6245
6272
|
static buildWsUrl(params) {
|
|
6273
|
+
var _a;
|
|
6246
6274
|
const {
|
|
6247
6275
|
wssBaseUrl
|
|
6248
6276
|
} = params;
|
|
6249
|
-
const resolved = wssBaseUrl !== null && wssBaseUrl !== void 0 ? wssBaseUrl : Utils.getWssBaseUrl(params.accountId);
|
|
6277
|
+
const resolved = wssBaseUrl !== null && wssBaseUrl !== void 0 ? wssBaseUrl : Utils.getWssBaseUrl(params.accountId, (_a = params === null || params === void 0 ? void 0 : params.env) !== null && _a !== void 0 ? _a : Environment.PRODUCTION);
|
|
6250
6278
|
const base = resolved.endsWith("/") ? resolved.slice(0, -1) : resolved;
|
|
6251
6279
|
const query = Utils.constructQueryParams(params);
|
|
6252
6280
|
return `${base}?${query}`;
|
|
6253
6281
|
}
|
|
6254
6282
|
static constructQueryParams(params) {
|
|
6255
6283
|
// Keys consumed by the library — never forwarded to the server.
|
|
6256
|
-
const INTERNAL_KEYS = new Set(["authToken", "accountId", "wssBaseUrl", "reconnectInterval", "maxReconnectAttempts"]);
|
|
6284
|
+
const INTERNAL_KEYS = new Set(["authToken", "accountId", "wssBaseUrl", "reconnectInterval", "maxReconnectAttempts", "env"]);
|
|
6257
6285
|
// Mandatory server params remapped to the names the API expects.
|
|
6258
6286
|
// accountId is embedded in the URL path — never sent as a query param.
|
|
6259
6287
|
const pairs = [];
|
|
@@ -6299,6 +6327,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6299
6327
|
// connectionEstablished === false → connection never opened → retry
|
|
6300
6328
|
// connectionEstablished === true → server ended a live connection → destroy
|
|
6301
6329
|
this.connectionEstablished = false;
|
|
6330
|
+
this.readyFired = false;
|
|
6302
6331
|
this.reconnectAttempt = 0;
|
|
6303
6332
|
this.reconnectTimer = null;
|
|
6304
6333
|
this.params = Object.assign({}, params);
|
|
@@ -6307,6 +6336,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6307
6336
|
if (this.destroyed) return;
|
|
6308
6337
|
this._clearReconnectTimer();
|
|
6309
6338
|
this.connectionEstablished = false;
|
|
6339
|
+
this.readyFired = false;
|
|
6310
6340
|
this.reconnectAttempt = 0;
|
|
6311
6341
|
this._setStatus("connecting");
|
|
6312
6342
|
this._ensureTransport();
|
|
@@ -6316,6 +6346,10 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6316
6346
|
var _a;
|
|
6317
6347
|
this._clearReconnectTimer();
|
|
6318
6348
|
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
6349
|
+
if (this.readyFired) {
|
|
6350
|
+
this.readyFired = false;
|
|
6351
|
+
this.emit("onReady", false);
|
|
6352
|
+
}
|
|
6319
6353
|
this._setStatus("disconnected");
|
|
6320
6354
|
}
|
|
6321
6355
|
setParams(patch) {
|
|
@@ -6326,6 +6360,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6326
6360
|
if (hashChanged) {
|
|
6327
6361
|
this._clearReconnectTimer();
|
|
6328
6362
|
this.connectionEstablished = false;
|
|
6363
|
+
this.readyFired = false;
|
|
6329
6364
|
this.reconnectAttempt = 0;
|
|
6330
6365
|
// Tear down the current transport and start fresh with the new params.
|
|
6331
6366
|
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
@@ -6366,22 +6401,23 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6366
6401
|
this._setStatus("connected");
|
|
6367
6402
|
this.emit("onCallStart");
|
|
6368
6403
|
break;
|
|
6404
|
+
case "ACKNOWLEDGED":
|
|
6405
|
+
this._fireReady();
|
|
6406
|
+
break;
|
|
6369
6407
|
case "DISCONNECTED":
|
|
6370
6408
|
if (this.connectionEstablished) {
|
|
6371
|
-
|
|
6372
|
-
|
|
6409
|
+
if (this.readyFired) {
|
|
6410
|
+
this.readyFired = false;
|
|
6411
|
+
this.emit("onReady", false);
|
|
6412
|
+
}
|
|
6373
6413
|
this._setStatus("disconnected");
|
|
6374
6414
|
this.emit("onCallEnd");
|
|
6375
6415
|
this.destroy();
|
|
6376
6416
|
} else {
|
|
6377
|
-
// WebSocket never opened — network/DNS/refused error.
|
|
6378
|
-
// Retry up to MAX_RECONNECT_ATTEMPTS then give up.
|
|
6379
6417
|
this._scheduleReconnect();
|
|
6380
6418
|
}
|
|
6381
6419
|
break;
|
|
6382
6420
|
case "ERROR":
|
|
6383
|
-
// onerror fires before onclose; DISCONNECTED will arrive right after
|
|
6384
|
-
// and drive the retry / destroy decision. Just surface the error here.
|
|
6385
6421
|
this.emit("error", new Error((_a = msg.message) !== null && _a !== void 0 ? _a : "Unknown transport error"));
|
|
6386
6422
|
break;
|
|
6387
6423
|
case "MESSAGE":
|
|
@@ -6415,18 +6451,21 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6415
6451
|
this.reconnectTimer = null;
|
|
6416
6452
|
}
|
|
6417
6453
|
}
|
|
6454
|
+
_fireReady() {
|
|
6455
|
+
if (this.readyFired) return;
|
|
6456
|
+
this.readyFired = true;
|
|
6457
|
+
this.emit("onReady", true);
|
|
6458
|
+
}
|
|
6418
6459
|
_handleServerPayload(raw) {
|
|
6419
|
-
var _a;
|
|
6460
|
+
var _a, _b;
|
|
6420
6461
|
let parsed;
|
|
6421
6462
|
try {
|
|
6422
6463
|
parsed = JSON.parse(raw);
|
|
6423
|
-
} catch (
|
|
6464
|
+
} catch (_c) {
|
|
6424
6465
|
this.emit("error", new Error(`Failed to parse server message: ${raw}`));
|
|
6425
6466
|
return;
|
|
6426
6467
|
}
|
|
6427
6468
|
this.emit("raw", parsed);
|
|
6428
|
-
// Auth error messages arrive before the server closes the connection.
|
|
6429
|
-
// Treat both as fatal — the server will follow up with a 4001/4002 close.
|
|
6430
6469
|
const msgType = parsed.type;
|
|
6431
6470
|
if (msgType === "auth_failed" || msgType === "auth_timeout") {
|
|
6432
6471
|
const detail = (_a = parsed.error) !== null && _a !== void 0 ? _a : msgType;
|
|
@@ -6436,6 +6475,10 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6436
6475
|
this.emit("error", err);
|
|
6437
6476
|
return;
|
|
6438
6477
|
}
|
|
6478
|
+
if (msgType === "ack" && !this.readyFired) {
|
|
6479
|
+
(_b = this.transport) === null || _b === void 0 ? void 0 : _b.markAcknowledged();
|
|
6480
|
+
this._fireReady();
|
|
6481
|
+
}
|
|
6439
6482
|
if (parsed.config) {
|
|
6440
6483
|
this.emit("botConfig", parsed.config);
|
|
6441
6484
|
}
|
|
@@ -6489,6 +6532,7 @@ ExotelAIAssistController.RECONNECT_BASE_MS = 3000;
|
|
|
6489
6532
|
function useExotelAIAssist(params) {
|
|
6490
6533
|
const controllerRef = React.useRef(null);
|
|
6491
6534
|
const [status, setStatus] = React.useState("idle");
|
|
6535
|
+
const [isReady, setIsReady] = React.useState(false);
|
|
6492
6536
|
const [suggestions, setSuggestions] = React.useState([]);
|
|
6493
6537
|
const [transcripts, setTranscripts] = React.useState([]);
|
|
6494
6538
|
const [sentiment, setSentiment] = React.useState(null);
|
|
@@ -6496,8 +6540,8 @@ function useExotelAIAssist(params) {
|
|
|
6496
6540
|
const [lastError, setLastError] = React.useState(null);
|
|
6497
6541
|
const paramHash = Utils.hash(params);
|
|
6498
6542
|
React.useEffect(() => {
|
|
6499
|
-
// Clear all stale state from the previous session before connecting.
|
|
6500
6543
|
setStatus("idle");
|
|
6544
|
+
setIsReady(false);
|
|
6501
6545
|
setSuggestions([]);
|
|
6502
6546
|
setTranscripts([]);
|
|
6503
6547
|
setSentiment(null);
|
|
@@ -6506,6 +6550,7 @@ function useExotelAIAssist(params) {
|
|
|
6506
6550
|
const ctrl = new ExotelAIAssistController(params);
|
|
6507
6551
|
controllerRef.current = ctrl;
|
|
6508
6552
|
ctrl.on("statusChange", setStatus);
|
|
6553
|
+
ctrl.on("onReady", setIsReady);
|
|
6509
6554
|
ctrl.on("botConfig", setBotConfig);
|
|
6510
6555
|
ctrl.on("suggestion", s => setSuggestions(prev => [...prev, s].slice(-50)));
|
|
6511
6556
|
ctrl.on("sentiment", setSentiment);
|
|
@@ -6536,6 +6581,7 @@ function useExotelAIAssist(params) {
|
|
|
6536
6581
|
}, []);
|
|
6537
6582
|
return {
|
|
6538
6583
|
status,
|
|
6584
|
+
isReady,
|
|
6539
6585
|
suggestions,
|
|
6540
6586
|
transcripts,
|
|
6541
6587
|
sentiment,
|
|
@@ -7086,16 +7132,24 @@ function TranscriptTab({
|
|
|
7086
7132
|
|
|
7087
7133
|
function ExotelAIAssist(_a) {
|
|
7088
7134
|
var {
|
|
7089
|
-
className
|
|
7135
|
+
className,
|
|
7136
|
+
onReady
|
|
7090
7137
|
} = _a,
|
|
7091
|
-
params = __rest(_a, ["className"]);
|
|
7138
|
+
params = __rest(_a, ["className", "onReady"]);
|
|
7092
7139
|
const {
|
|
7093
7140
|
status,
|
|
7141
|
+
isReady,
|
|
7094
7142
|
suggestions,
|
|
7095
7143
|
transcripts,
|
|
7096
7144
|
sentiment,
|
|
7097
7145
|
botConfig
|
|
7098
7146
|
} = useExotelAIAssist(params);
|
|
7147
|
+
const onReadyRef = React.useRef(onReady);
|
|
7148
|
+
onReadyRef.current = onReady;
|
|
7149
|
+
React.useEffect(() => {
|
|
7150
|
+
var _a;
|
|
7151
|
+
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef, isReady);
|
|
7152
|
+
}, [isReady]);
|
|
7099
7153
|
const connected = status === "connected";
|
|
7100
7154
|
return jsxRuntime.jsx(Provider, {
|
|
7101
7155
|
children: jsxRuntime.jsx("div", {
|
|
@@ -5,6 +5,9 @@ export interface ITransport {
|
|
|
5
5
|
disconnect(): void;
|
|
6
6
|
send(payload: string): void;
|
|
7
7
|
onMessage(handler: TransportMessageHandler): void;
|
|
8
|
+
/** Signal that the server ack has been received. The leader stores this so
|
|
9
|
+
* follower tabs joining later get an immediate ACKNOWLEDGED broadcast. */
|
|
10
|
+
markAcknowledged(): void;
|
|
8
11
|
destroy(): void;
|
|
9
12
|
}
|
|
10
13
|
export declare class BroadcastChannelTransport implements ITransport {
|
|
@@ -16,6 +19,7 @@ export declare class BroadcastChannelTransport implements ITransport {
|
|
|
16
19
|
private pendingUrl;
|
|
17
20
|
private pendingAuthToken;
|
|
18
21
|
private lockReleaser;
|
|
22
|
+
private ackReceived;
|
|
19
23
|
private _destroyed;
|
|
20
24
|
/**
|
|
21
25
|
* @param sessionHash - A hash of all connection-relevant params (see
|
|
@@ -30,6 +34,7 @@ export declare class BroadcastChannelTransport implements ITransport {
|
|
|
30
34
|
disconnect(): void;
|
|
31
35
|
send(payload: string): void;
|
|
32
36
|
onMessage(handler: TransportMessageHandler): void;
|
|
37
|
+
markAcknowledged(): void;
|
|
33
38
|
destroy(): void;
|
|
34
39
|
}
|
|
35
40
|
export declare function createTransport(sessionHash: string): ITransport;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export declare enum Environment {
|
|
2
|
+
PRODUCTION = "production",
|
|
3
|
+
UAT = "uat",
|
|
4
|
+
DEVELOPMENT = "development"
|
|
5
|
+
}
|
|
6
|
+
export declare const WssBaseUrl: Record<Environment, string>;
|
|
1
7
|
export interface ExotelAIAssistParams {
|
|
2
8
|
authToken: string;
|
|
3
9
|
call_sid: string;
|
|
@@ -7,6 +13,7 @@ export interface ExotelAIAssistParams {
|
|
|
7
13
|
wssBaseUrl?: string;
|
|
8
14
|
reconnectInterval?: number;
|
|
9
15
|
maxReconnectAttempts?: number;
|
|
16
|
+
env?: Environment;
|
|
10
17
|
[key: string]: unknown;
|
|
11
18
|
}
|
|
12
19
|
/** A single AI-generated suggestion for the agent. */
|
|
@@ -69,6 +76,14 @@ export interface ControllerEvents {
|
|
|
69
76
|
sentiment: (data: Sentiment) => void;
|
|
70
77
|
/** Internal: bot feature-flag config. Consumed by the UI component only. */
|
|
71
78
|
botConfig: (config: BotConfig) => void;
|
|
79
|
+
/**
|
|
80
|
+
* Fires with `true` once the WebSocket connection is established AND the
|
|
81
|
+
* server has acknowledged it (first successful server message after auth).
|
|
82
|
+
* In multi-tab scenarios where a follower tab joins an already-acknowledged
|
|
83
|
+
* connection, this fires immediately with `true`.
|
|
84
|
+
* Fires with `false` on disconnect.
|
|
85
|
+
*/
|
|
86
|
+
onReady: (ready: boolean) => void;
|
|
72
87
|
onCallStart: () => void;
|
|
73
88
|
onCallEnd: () => void;
|
|
74
89
|
statusChange: (status: ConnectionStatus) => void;
|
|
@@ -80,6 +95,8 @@ export type WorkerInboundMessage = {
|
|
|
80
95
|
payload: string;
|
|
81
96
|
} | {
|
|
82
97
|
type: "CONNECTED";
|
|
98
|
+
} | {
|
|
99
|
+
type: "ACKNOWLEDGED";
|
|
83
100
|
} | {
|
|
84
101
|
type: "DISCONNECTED";
|
|
85
102
|
code?: number;
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ExotelAIAssistParams } from "../types";
|
|
1
|
+
import { Environment, ExotelAIAssistParams } from "../types";
|
|
2
2
|
export declare class Utils {
|
|
3
3
|
static MAX_EXTRA_QUERY_PARAMS: number;
|
|
4
4
|
/**
|
|
@@ -20,7 +20,7 @@ export declare class Utils {
|
|
|
20
20
|
* @param accountId - The account ID to get the base WebSocket URL for.
|
|
21
21
|
* @returns The base WebSocket URL.
|
|
22
22
|
*/
|
|
23
|
-
static getWssBaseUrl(accountId: string): string;
|
|
23
|
+
static getWssBaseUrl(accountId: string, env: Environment): string;
|
|
24
24
|
/**
|
|
25
25
|
* Builds a WebSocket URL from the given parameters.
|
|
26
26
|
* @param params - The parameters to build the WebSocket URL from.
|
package/package.json
CHANGED