@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 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 | Required | Default | Description |
214
- | ---------------------- | -------- | -------- | ------------------------ | -------------------------------------------------------- |
215
- | `authToken` | `string` | ✓ | — | Bearer token |
216
- | `call_sid` | `string` | ✓ | — | Active call SID |
217
- | `accountId` | `string` | ✓ | — | Exotel account identifier |
218
- | `wssBaseUrl` | `string` | — | Exotel AI Assist backend | Override only when pointing at a non-production endpoint |
219
- | `reconnectInterval` | `number` | — | `3000` | Base reconnect delay in ms |
220
- | `maxReconnectAttempts` | `number` | — | `5` | Max retries before error |
221
- | `[customParam]` | `string` | — | — | Max 3 params you can send |
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
- | `suggestion` | `Suggestion` | New AI suggestion (capped at last 50) |
238
- | `transcript` | `TranscriptLine[]` | Live transcript update |
239
- | `sentiment` | `Sentiment` | Sentiment label update |
240
- | `onCallStart` | `unknown` | Connection opened |
241
- | `onCallEnd` | `unknown` | Connection closed |
242
- | `statusChange` | `ConnectionStatus` | Status transition |
243
- | `error` | `Error` | Any error (auth, parse, max-reconnect) |
244
- | `raw` | `unknown` | Every raw server message |
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
  }
@@ -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
- // return `wss://ai-assist.in.exotel.com/ai-assist/accounts/${accountId}/one-assistant-event-publisher`;
1533
- // return `wss://oneassist-uat.in.exotel.com/ai-assist/one-assistant-event-publisher/${accountId}`;
1534
- return `wss://oneassist-uat.in.exotel.com/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
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
- // Server ended an already-established connection (call ended, stream
1668
- // not found, auth failure, etc.) — do not retry.
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 (_b) {
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
- // return `wss://ai-assist.in.exotel.com/ai-assist/accounts/${accountId}/one-assistant-event-publisher`;
20951
- // return `wss://oneassist-uat.in.exotel.com/ai-assist/one-assistant-event-publisher/${accountId}`;
20952
- return `wss://oneassist-uat.in.exotel.com/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
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
- // Server ended an already-established connection (call ended, stream
21086
- // not found, auth failure, etc.) — do not retry.
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 (_b) {
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", {
@@ -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
- // return `wss://ai-assist.in.exotel.com/ai-assist/accounts/${accountId}/one-assistant-event-publisher`;
6237
- // return `wss://oneassist-uat.in.exotel.com/ai-assist/one-assistant-event-publisher/${accountId}`;
6238
- return `wss://oneassist-uat.in.exotel.com/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
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
- // Server ended an already-established connection (call ended, stream
6372
- // not found, auth failure, etc.) — do not retry.
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 (_b) {
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;
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exotel-npm-dev/exotel-ai-assist",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "AI-powered real-time suggestions, transcript, and sentiment for agent calls",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",