@exotel-npm-dev/exotel-ai-assist 1.1.7 → 1.1.8

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.
Files changed (34) hide show
  1. package/README.md +45 -15
  2. package/dist/components/EmptyState.d.ts +2 -0
  3. package/dist/components/key-components/SuggestionTab.d.ts +1 -1
  4. package/dist/components/key-components/TranscriptTab.d.ts +1 -1
  5. package/dist/controller/index.d.ts +4 -1
  6. package/dist/controller/index.js +30 -12
  7. package/dist/hooks/useExotelAIAssist.d.ts +9 -2
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +104 -56
  10. package/dist/react/index.d.ts +1 -1
  11. package/dist/react/index.js +104 -56
  12. package/dist/types/index.d.ts +16 -5
  13. package/package.json +7 -3
  14. package/rollup.config.mjs +126 -0
  15. package/src/components/EmptyState.tsx +36 -0
  16. package/src/components/ExotelAIAssistApp.tsx +78 -0
  17. package/src/components/ExotelAIAssistProvider.tsx +26 -0
  18. package/src/components/LoadingBox.tsx +134 -0
  19. package/src/components/Toast.tsx +148 -0
  20. package/src/components/key-components/Header.tsx +72 -0
  21. package/src/components/key-components/SuggestionTab.tsx +92 -0
  22. package/src/components/key-components/TranscriptTab.tsx +75 -0
  23. package/src/components/key-components/WarningBanner.tsx +19 -0
  24. package/src/components/logos/NoInteraction.tsx +30 -0
  25. package/src/components/logos/Sparkle.tsx +10 -0
  26. package/src/controller/index.ts +275 -0
  27. package/src/hooks/useExotelAIAssist.ts +93 -0
  28. package/src/index.ts +15 -0
  29. package/src/react/index.ts +9 -0
  30. package/src/styles/index.css +374 -0
  31. package/src/transport/index.ts +182 -0
  32. package/src/types/index.ts +114 -0
  33. package/src/utils/index.ts +88 -0
  34. package/tsconfig.json +19 -0
package/README.md CHANGED
@@ -113,7 +113,7 @@ 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 { isReady, status, suggestions, transcripts, lastError } = useExotelAIAssist({
116
+ const { isReady, streamState, suggestions, transcripts, lastError } = useExotelAIAssist({
117
117
  authToken: "your-auth-token",
118
118
  call_sid,
119
119
  accountId: "your-account-id",
@@ -121,9 +121,11 @@ function MyCustomUI({ call_sid }: { call_sid: string }) {
121
121
 
122
122
  if (!isReady) return <p>Connecting…</p>;
123
123
 
124
+ if (streamState === "throttled") return <p>AI Assist is at capacity — available on your next call.</p>;
125
+ if (streamState === "disconnected") return <p>AI Assist disconnected.</p>;
126
+
124
127
  return (
125
128
  <div>
126
- <p>Status: {status}</p>
127
129
  {suggestions.map((s) => (
128
130
  <p key={s.id}>{s.text}</p>
129
131
  ))}
@@ -184,7 +186,10 @@ ctrl.on("onReady", (ready) => {
184
186
 
185
187
  ctrl.on("suggestion", (s) => console.log("Suggestion:", s));
186
188
  ctrl.on("transcript", (t) => console.log("Transcript:", t));
187
- ctrl.on("statusChange", (status) => console.log("Status:", status));
189
+ ctrl.on("streamState", (state) => {
190
+ console.log("Stream state:", state);
191
+ // state: "connected" | "throttled" | "disconnected"
192
+ });
188
193
  ctrl.on("error", (err) => console.error("Error:", err));
189
194
 
190
195
  ctrl.connect();
@@ -240,6 +245,7 @@ Extends `EventEmitter`.
240
245
  | `setParams(patch)` | Merge params; reconnects if `call_sid` changes |
241
246
  | `destroy()` | Dispose controller and remove all listeners |
242
247
  | `getStatus()` | Returns current `ConnectionStatus` |
248
+ | `getStreamState()` | Returns current `StreamState \| null` |
243
249
 
244
250
  #### Events
245
251
 
@@ -249,9 +255,10 @@ Extends `EventEmitter`.
249
255
  | `suggestion` | `Suggestion` | New AI suggestion (capped at last 50) |
250
256
  | `transcript` | `TranscriptLine[]` | Live transcript update |
251
257
  | `sentiment` | `Sentiment` | Sentiment label update |
258
+ | `streamState` | `StreamState` | Server stream state change (`connected`, `throttled`, `disconnected`) |
252
259
  | `onCallStart` | — | Connection opened |
253
260
  | `onCallEnd` | — | Connection closed |
254
- | `statusChange` | `ConnectionStatus` | Status transition |
261
+ | `statusChange` | `ConnectionStatus` | WebSocket-level status transition (internal) |
255
262
  | `error` | `Error` | Any error (auth, parse, max-reconnect) |
256
263
  | `raw` | `unknown` | Every raw server message |
257
264
 
@@ -259,23 +266,25 @@ Extends `EventEmitter`.
259
266
 
260
267
  ### `useExotelAIAssist(params)` — Hook return values
261
268
 
262
- | Field | Type | Description |
263
- | ------------- | ------------------ | ---------------------------------------------------------------------- |
264
- | `isReady` | `boolean` | `true` after connected + server ack. Multi-tab safe. `false` on disconnect |
265
- | `status` | `ConnectionStatus` | Current connection status |
266
- | `suggestions` | `Suggestion[]` | AI suggestions, oldest first, capped at 50 |
267
- | `transcripts` | `TranscriptLine[]` | Live transcript lines, ordered by start time |
268
- | `sentiment` | `Sentiment \| null`| Latest sentiment reading |
269
- | `lastError` | `Error \| null` | Most recent error |
270
- | `connect()` | `() => void` | Manually open the connection |
271
- | `disconnect()` | `() => void` | Manually close the connection |
272
- | `setParams()` | `(patch) => void` | Merge params; reconnects if connection params change |
269
+ | Field | Type | Description |
270
+ | -------------- | ----------------------- | ---------------------------------------------------------------------------- |
271
+ | `isReady` | `boolean` | `true` after connected + server ack. Multi-tab safe. `false` on disconnect |
272
+ | `streamState` | `StreamState \| null` | Server stream state: `"connected"`, `"throttled"`, or `"disconnected"`. `null` until the first `stream_status` message |
273
+ | `suggestions` | `Suggestion[]` | AI suggestions, oldest first, capped at 50 |
274
+ | `transcripts` | `TranscriptLine[]` | Live transcript lines, ordered by start time |
275
+ | `sentiment` | `Sentiment \| null` | Latest sentiment reading |
276
+ | `lastError` | `Error \| null` | Most recent error |
277
+ | `connect()` | `() => void` | Manually open the connection |
278
+ | `disconnect()` | `() => void` | Manually close the connection |
279
+ | `setParams()` | `(patch) => void` | Merge params; reconnects if connection params change |
273
280
 
274
281
  ---
275
282
 
276
283
  ### TypeScript Types
277
284
 
278
285
  ```ts
286
+ type StreamState = "connected" | "throttled" | "disconnected";
287
+
279
288
  type ConnectionStatus = "idle" | "connecting" | "connected" | "disconnected" | "error";
280
289
 
281
290
  interface Suggestion {
@@ -310,6 +319,27 @@ When `wssBaseUrl` is provided it overrides the host + path portion:
310
319
  wss://<wssBaseUrl>?[customParam1=value1&customParam2=value2&...]
311
320
  ```
312
321
 
322
+ ### Message flow
323
+
324
+ 1. Client opens WebSocket, sends `{ "type": "auth", "access_token": "..." }`
325
+ 2. Server responds with `{ "type": "ack", "config": {...}, "events": [...] }` — triggers `isReady`
326
+ 3. Server sends `stream_status` messages whenever the stream state changes:
327
+
328
+ ```json
329
+ {
330
+ "type": "stream_status",
331
+ "config": { /* bot config, populated on connected, null otherwise */ },
332
+ "events": [ /* accumulated events since last message, usually [] */ ],
333
+ "stream_state": "connected" | "throttled" | "disconnected"
334
+ }
335
+ ```
336
+
337
+ | `stream_state` | Meaning |
338
+ | --------------- | --------------------------------------------------------- |
339
+ | `connected` | Stream is active — suggestions, transcript, and sentiment flow normally |
340
+ | `throttled` | Capacity full — AI Assist cannot join this call; available on the next call |
341
+ | `disconnected` | Stream ended or dropped |
342
+
313
343
  ### Reconnection
314
344
 
315
345
  - Exponential back-off: `delay = Math.min(baseInterval × 2^attempt, 30 000)`
@@ -2,6 +2,8 @@ import React from "react";
2
2
  interface EmptyStateProps {
3
3
  title: string;
4
4
  subtitle: string;
5
+ showWarningBanner?: boolean;
6
+ warningBannerMessage?: string;
5
7
  }
6
8
  export declare const EmptyState: React.FC<EmptyStateProps>;
7
9
  export {};
@@ -1,6 +1,6 @@
1
1
  import { Suggestion, BotConfig } from "../../types";
2
2
  import "../../styles/index.css";
3
- export declare function SuggestionsTab({ suggestions, connected, botConfig }: {
3
+ export declare function SuggestionsTab({ suggestions, connected, botConfig, }: {
4
4
  suggestions: Suggestion[];
5
5
  connected: boolean;
6
6
  botConfig: BotConfig | null;
@@ -1,6 +1,6 @@
1
1
  import { TranscriptLine, BotConfig } from "../../types";
2
2
  import "../../styles/index.css";
3
- export declare function TranscriptTab({ transcripts, connected, botConfig }: {
3
+ export declare function TranscriptTab({ transcripts, connected, botConfig, }: {
4
4
  transcripts: TranscriptLine[];
5
5
  connected: boolean;
6
6
  botConfig: BotConfig | null;
@@ -1,5 +1,5 @@
1
1
  import EventEmitter from "eventemitter3";
2
- import { ExotelAIAssistParams, ConnectionStatus, ControllerEvents } from "../types";
2
+ import { ExotelAIAssistParams, ConnectionStatus, ControllerEvents, StreamState } from "../types";
3
3
  export declare class ExotelAIAssistController extends EventEmitter<ControllerEvents> {
4
4
  private params;
5
5
  private transport;
@@ -7,6 +7,7 @@ export declare class ExotelAIAssistController extends EventEmitter<ControllerEve
7
7
  private destroyed;
8
8
  private connectionEstablished;
9
9
  private readyFired;
10
+ private _streamState;
10
11
  private reconnectAttempt;
11
12
  private reconnectTimer;
12
13
  private static readonly MAX_RECONNECT_ATTEMPTS;
@@ -17,11 +18,13 @@ export declare class ExotelAIAssistController extends EventEmitter<ControllerEve
17
18
  setParams(patch: Partial<ExotelAIAssistParams>): void;
18
19
  destroy(): void;
19
20
  getStatus(): ConnectionStatus;
21
+ getStreamState(): StreamState | null;
20
22
  private _ensureTransport;
21
23
  private _handleTransportMessage;
22
24
  private _scheduleReconnect;
23
25
  private _clearReconnectTimer;
24
26
  private _fireReady;
25
27
  private _handleServerPayload;
28
+ private _handleStreamStatus;
26
29
  private _setStatus;
27
30
  }
@@ -1538,7 +1538,7 @@ class Utils {
1538
1538
  * @returns The base WebSocket URL.
1539
1539
  */
1540
1540
  static getWssBaseUrl(accountId) {
1541
- return `wss://aiassist-uat.in.exotel.com/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
1541
+ return `ws://localhost:3456/ai-assist/ws/v1/accounts/${accountId}/ai-assistants/conversation-events`;
1542
1542
  }
1543
1543
  /**
1544
1544
  * Builds a WebSocket URL from the given parameters.
@@ -1603,6 +1603,7 @@ class ExotelAIAssistController extends EventEmitter {
1603
1603
  // connectionEstablished === true → server ended a live connection → destroy
1604
1604
  this.connectionEstablished = false;
1605
1605
  this.readyFired = false;
1606
+ this._streamState = null;
1606
1607
  this.reconnectAttempt = 0;
1607
1608
  this.reconnectTimer = null;
1608
1609
  this.params = Object.assign({}, params);
@@ -1612,6 +1613,7 @@ class ExotelAIAssistController extends EventEmitter {
1612
1613
  this._clearReconnectTimer();
1613
1614
  this.connectionEstablished = false;
1614
1615
  this.readyFired = false;
1616
+ this._streamState = null;
1615
1617
  this.reconnectAttempt = 0;
1616
1618
  this._setStatus("connecting");
1617
1619
  this._ensureTransport();
@@ -1662,6 +1664,9 @@ class ExotelAIAssistController extends EventEmitter {
1662
1664
  getStatus() {
1663
1665
  return this.status;
1664
1666
  }
1667
+ getStreamState() {
1668
+ return this._streamState;
1669
+ }
1665
1670
  _ensureTransport() {
1666
1671
  if (this.transport) return;
1667
1672
  this.transport = createTransport(Utils.hash(this.params));
@@ -1677,7 +1682,6 @@ class ExotelAIAssistController extends EventEmitter {
1677
1682
  this.emit("onCallStart");
1678
1683
  break;
1679
1684
  case "ACKNOWLEDGED":
1680
- this._fireReady();
1681
1685
  break;
1682
1686
  case "DISCONNECTED":
1683
1687
  if (this.connectionEstablished) {
@@ -1732,11 +1736,11 @@ class ExotelAIAssistController extends EventEmitter {
1732
1736
  this.emit("onReady", true);
1733
1737
  }
1734
1738
  _handleServerPayload(raw) {
1735
- var _a, _b;
1739
+ var _a, _b, _c;
1736
1740
  let parsed;
1737
1741
  try {
1738
1742
  parsed = JSON.parse(raw);
1739
- } catch (_c) {
1743
+ } catch (_d) {
1740
1744
  this.emit("error", new Error(`Failed to parse server message: ${raw}`));
1741
1745
  return;
1742
1746
  }
@@ -1752,7 +1756,12 @@ class ExotelAIAssistController extends EventEmitter {
1752
1756
  }
1753
1757
  if (msgType === "ack") {
1754
1758
  (_b = this.transport) === null || _b === void 0 ? void 0 : _b.markAcknowledged();
1755
- this._fireReady();
1759
+ const state = parsed.stream_state;
1760
+ if (state) this._handleStreamStatus(state);
1761
+ }
1762
+ if (msgType === "stream_status") {
1763
+ const state = parsed.stream_state;
1764
+ if (state) this._handleStreamStatus(state);
1756
1765
  }
1757
1766
  if (parsed.config) {
1758
1767
  this.emit("botConfig", parsed.config);
@@ -1763,14 +1772,15 @@ class ExotelAIAssistController extends EventEmitter {
1763
1772
  const now = Date.now();
1764
1773
  for (const event of events) {
1765
1774
  if (!event) continue;
1766
- if (event.transcript && event.transcript.length > 0) {
1767
- const lines = event.transcript.map(msg => {
1775
+ if (event.event_type === "transcript" && event.value) {
1776
+ const transcripts = Array.isArray(event === null || event === void 0 ? void 0 : event.value) ? event.value : (_c = event === null || event === void 0 ? void 0 : event.transcript) !== null && _c !== void 0 ? _c : [];
1777
+ const lines = transcripts.map(msg => {
1768
1778
  var _a, _b, _c, _d, _e;
1769
1779
  const first = (_a = msg.transcript_segments) === null || _a === void 0 ? void 0 : _a[0];
1770
1780
  const last = (_b = msg.transcript_segments) === null || _b === void 0 ? void 0 : _b[msg.transcript_segments.length - 1];
1771
1781
  return {
1772
1782
  id: String(msg.sequence),
1773
- text: (_e = (_d = (_c = msg.transcript_segments) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.text) !== null && _e !== void 0 ? _e : "",
1783
+ value: (_e = (_d = (_c = msg.transcript_segments) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.text) !== null && _e !== void 0 ? _e : "",
1774
1784
  startTime: (first === null || first === void 0 ? void 0 : first.start_timestamp) ? Date.parse(first.start_timestamp) : now,
1775
1785
  endTime: (last === null || last === void 0 ? void 0 : last.end_timestamp) ? Date.parse(last.end_timestamp) : now,
1776
1786
  isFinal: msg.transcript_segments.every(s => s.is_final)
@@ -1778,23 +1788,31 @@ class ExotelAIAssistController extends EventEmitter {
1778
1788
  });
1779
1789
  this.emit("transcript", lines);
1780
1790
  }
1781
- if (event.event_type === "suggestion" && event.text) {
1791
+ if (event.event_type === "suggestion" && event.value) {
1782
1792
  const suggestion = {
1783
1793
  id: Utils.getUniqueId(),
1784
- text: event.text,
1794
+ value: event.value,
1785
1795
  timestamp: now
1786
1796
  };
1787
1797
  this.emit("suggestion", suggestion);
1788
1798
  }
1789
- if (event.event_type === "sentiment" && event.text) {
1799
+ if (event.event_type === "sentiment" && event.value) {
1790
1800
  const sentiment = {
1791
- label: event.text.toLowerCase(),
1801
+ label: event.value.toLowerCase(),
1792
1802
  timestamp: now
1793
1803
  };
1794
1804
  this.emit("sentiment", sentiment);
1795
1805
  }
1796
1806
  }
1797
1807
  }
1808
+ _handleStreamStatus(state) {
1809
+ this._streamState = state;
1810
+ this.emit("streamState", state);
1811
+ // We are firing ready because we need to show the UI immediately when the stream is connected or throttled.
1812
+ if (state === "connected" || state === "throttled") {
1813
+ this._fireReady();
1814
+ }
1815
+ }
1798
1816
  _setStatus(status) {
1799
1817
  if (this.status === status) return;
1800
1818
  this.status = status;
@@ -1,6 +1,5 @@
1
- import { ExotelAIAssistParams, Suggestion, TranscriptLine, Sentiment, BotConfig, ConnectionStatus } from "../types";
1
+ import { ExotelAIAssistParams, Suggestion, TranscriptLine, Sentiment, BotConfig, StreamState } from "../types";
2
2
  export interface UseExotelAIAssistReturn {
3
- status: ConnectionStatus;
4
3
  /**
5
4
  * `true` once the WebSocket connection is established and the server has
6
5
  * acknowledged it. In multi-tab scenarios where this tab joins an
@@ -8,6 +7,14 @@ export interface UseExotelAIAssistReturn {
8
7
  * Resets to `false` on disconnect.
9
8
  */
10
9
  isReady: boolean;
10
+ /**
11
+ * Server-reported stream state from `stream_status` messages.
12
+ * `null` until the first `stream_status` is received.
13
+ * - `"connected"` – stream is active and healthy
14
+ * - `"throttled"` – capacity full; bot cannot join this call
15
+ * - `"disconnected"` – stream ended or dropped
16
+ */
17
+ streamState: StreamState | null;
11
18
  /** AI suggestions, oldest first, capped at 50. */
12
19
  suggestions: Suggestion[];
13
20
  /** Live transcript lines, ordered by start time. */
package/dist/index.d.ts CHANGED
@@ -2,4 +2,4 @@ import { ExotelAIAssistParams } from "./types";
2
2
  export declare function mountExotelAIAssist(container: HTMLElement, params: ExotelAIAssistParams): void;
3
3
  export declare function unmountExotelAIAssist(container: HTMLElement): void;
4
4
  export { ExotelAIAssistController } from "./controller";
5
- export type { ExotelAIAssistParams, Suggestion, TranscriptLine, Sentiment, ConnectionStatus } from "./types";
5
+ export type { ExotelAIAssistParams, Suggestion, TranscriptLine, Sentiment, ConnectionStatus, StreamState } from "./types";