@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.
- package/README.md +45 -15
- package/dist/components/EmptyState.d.ts +2 -0
- package/dist/components/key-components/SuggestionTab.d.ts +1 -1
- package/dist/components/key-components/TranscriptTab.d.ts +1 -1
- package/dist/controller/index.d.ts +4 -1
- package/dist/controller/index.js +30 -12
- package/dist/hooks/useExotelAIAssist.d.ts +9 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +104 -56
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +104 -56
- package/dist/types/index.d.ts +16 -5
- package/package.json +7 -3
- package/rollup.config.mjs +126 -0
- package/src/components/EmptyState.tsx +36 -0
- package/src/components/ExotelAIAssistApp.tsx +78 -0
- package/src/components/ExotelAIAssistProvider.tsx +26 -0
- package/src/components/LoadingBox.tsx +134 -0
- package/src/components/Toast.tsx +148 -0
- package/src/components/key-components/Header.tsx +72 -0
- package/src/components/key-components/SuggestionTab.tsx +92 -0
- package/src/components/key-components/TranscriptTab.tsx +75 -0
- package/src/components/key-components/WarningBanner.tsx +19 -0
- package/src/components/logos/NoInteraction.tsx +30 -0
- package/src/components/logos/Sparkle.tsx +10 -0
- package/src/controller/index.ts +275 -0
- package/src/hooks/useExotelAIAssist.ts +93 -0
- package/src/index.ts +15 -0
- package/src/react/index.ts +9 -0
- package/src/styles/index.css +374 -0
- package/src/transport/index.ts +182 -0
- package/src/types/index.ts +114 -0
- package/src/utils/index.ts +88 -0
- 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,
|
|
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("
|
|
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` |
|
|
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
|
|
263
|
-
|
|
|
264
|
-
| `isReady`
|
|
265
|
-
| `
|
|
266
|
-
| `suggestions`
|
|
267
|
-
| `transcripts`
|
|
268
|
-
| `sentiment`
|
|
269
|
-
| `lastError`
|
|
270
|
-
| `connect()`
|
|
271
|
-
| `disconnect()` | `() => void`
|
|
272
|
-
| `setParams()`
|
|
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)`
|
|
@@ -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
|
}
|
package/dist/controller/index.js
CHANGED
|
@@ -1538,7 +1538,7 @@ class Utils {
|
|
|
1538
1538
|
* @returns The base WebSocket URL.
|
|
1539
1539
|
*/
|
|
1540
1540
|
static getWssBaseUrl(accountId) {
|
|
1541
|
-
return `
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
1767
|
-
const
|
|
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
|
-
|
|
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.
|
|
1791
|
+
if (event.event_type === "suggestion" && event.value) {
|
|
1782
1792
|
const suggestion = {
|
|
1783
1793
|
id: Utils.getUniqueId(),
|
|
1784
|
-
|
|
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.
|
|
1799
|
+
if (event.event_type === "sentiment" && event.value) {
|
|
1790
1800
|
const sentiment = {
|
|
1791
|
-
label: event.
|
|
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,
|
|
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";
|