@exotel-npm-dev/exotel-ai-assist 1.2.9 → 1.3.1
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/dist/controller/index.js +45 -8
- package/dist/index.js +67 -10
- package/dist/react/index.js +67 -10
- package/dist/transport/index.d.ts +3 -0
- package/dist/types/index.d.ts +11 -0
- package/package.json +1 -1
- package/src/controller/index.ts +12 -2
- package/src/hooks/useExotelAIAssist.ts +17 -2
- package/src/transport/index.ts +25 -5
- package/src/types/index.ts +4 -1
package/dist/controller/index.js
CHANGED
|
@@ -349,7 +349,7 @@ class BroadcastChannelTransport {
|
|
|
349
349
|
const lockName = `exotel-ai-assist-leader:${sessionHash}`;
|
|
350
350
|
this.channel = new BroadcastChannel(channelName);
|
|
351
351
|
this.channel.onmessage = ev => {
|
|
352
|
-
var _a;
|
|
352
|
+
var _a, _b, _c;
|
|
353
353
|
const data = ev.data;
|
|
354
354
|
if (this.isLeader) {
|
|
355
355
|
if (data.type === "JOIN" && this.socketConnected && this.ackPayload) {
|
|
@@ -361,11 +361,18 @@ class BroadcastChannelTransport {
|
|
|
361
361
|
payload: this.ackPayload
|
|
362
362
|
});
|
|
363
363
|
}
|
|
364
|
+
if (data.type === "SEND" && ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
365
|
+
this.socket.send(data.payload);
|
|
366
|
+
}
|
|
367
|
+
if (data.type === "FEEDBACK_SYNC") {
|
|
368
|
+
(_b = this.handler) === null || _b === void 0 ? void 0 : _b.call(this, data);
|
|
369
|
+
}
|
|
364
370
|
return;
|
|
365
371
|
}
|
|
366
|
-
// Follower: forward
|
|
367
|
-
|
|
368
|
-
|
|
372
|
+
// Follower: forward server messages to the controller handler.
|
|
373
|
+
// JOIN and SEND are internal transport messages, not controller messages.
|
|
374
|
+
if (data.type !== "JOIN" && data.type !== "SEND") {
|
|
375
|
+
(_c = this.handler) === null || _c === void 0 ? void 0 : _c.call(this, data);
|
|
369
376
|
}
|
|
370
377
|
};
|
|
371
378
|
this._electLeader(lockName);
|
|
@@ -475,8 +482,15 @@ class BroadcastChannelTransport {
|
|
|
475
482
|
}
|
|
476
483
|
send(payload) {
|
|
477
484
|
var _a;
|
|
478
|
-
if (this.isLeader
|
|
479
|
-
this.socket.
|
|
485
|
+
if (this.isLeader) {
|
|
486
|
+
if (((_a = this.socket) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
487
|
+
this.socket.send(payload);
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
this.channel.postMessage({
|
|
491
|
+
type: "SEND",
|
|
492
|
+
payload
|
|
493
|
+
});
|
|
480
494
|
}
|
|
481
495
|
}
|
|
482
496
|
onMessage(handler) {
|
|
@@ -485,8 +499,22 @@ class BroadcastChannelTransport {
|
|
|
485
499
|
markAcknowledged(rawAckPayload) {
|
|
486
500
|
this.ackPayload = rawAckPayload;
|
|
487
501
|
}
|
|
502
|
+
broadcastFeedbackSync(sequence, feedbackType, badFeedbackReason) {
|
|
503
|
+
this.channel.postMessage({
|
|
504
|
+
type: "FEEDBACK_SYNC",
|
|
505
|
+
sequence,
|
|
506
|
+
feedbackType,
|
|
507
|
+
badFeedbackReason
|
|
508
|
+
});
|
|
509
|
+
}
|
|
488
510
|
destroy() {
|
|
489
511
|
var _a;
|
|
512
|
+
if (this.isLeader) {
|
|
513
|
+
this.channel.postMessage({
|
|
514
|
+
type: "DISCONNECTED",
|
|
515
|
+
code: 1000
|
|
516
|
+
});
|
|
517
|
+
}
|
|
490
518
|
this._destroyed = true;
|
|
491
519
|
this.disconnect();
|
|
492
520
|
this.channel.close();
|
|
@@ -1679,6 +1707,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1679
1707
|
};
|
|
1680
1708
|
try {
|
|
1681
1709
|
this.transport.send(JSON.stringify(message));
|
|
1710
|
+
this.transport.broadcastFeedbackSync(sequence, feedbackType, badFeedbackReason);
|
|
1682
1711
|
return true;
|
|
1683
1712
|
} catch (error) {
|
|
1684
1713
|
console.error("[ExotelAIAssist] Failed to send feedback:", error);
|
|
@@ -1705,12 +1734,13 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1705
1734
|
if (this.connectionEstablished) {
|
|
1706
1735
|
const wasThrottled = this._streamState === "throttled";
|
|
1707
1736
|
if (this.readyFired && !wasThrottled) {
|
|
1708
|
-
this.readyFired = false;
|
|
1709
1737
|
this.emit("onReady", false);
|
|
1710
1738
|
}
|
|
1739
|
+
this.connectionEstablished = false;
|
|
1740
|
+
this.readyFired = false;
|
|
1741
|
+
this._streamState = null;
|
|
1711
1742
|
this._setStatus("disconnected");
|
|
1712
1743
|
this.emit("onCallEnd");
|
|
1713
|
-
this.destroy();
|
|
1714
1744
|
} else {
|
|
1715
1745
|
this._scheduleReconnect();
|
|
1716
1746
|
}
|
|
@@ -1721,6 +1751,13 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1721
1751
|
case "MESSAGE":
|
|
1722
1752
|
if (msg.payload) this._handleServerPayload(msg.payload);
|
|
1723
1753
|
break;
|
|
1754
|
+
case "FEEDBACK_SYNC":
|
|
1755
|
+
this.emit("suggestionFeedbackSync", {
|
|
1756
|
+
sequence: msg.sequence,
|
|
1757
|
+
feedbackType: msg.feedbackType,
|
|
1758
|
+
badFeedbackReason: msg.badFeedbackReason
|
|
1759
|
+
});
|
|
1760
|
+
break;
|
|
1724
1761
|
}
|
|
1725
1762
|
}
|
|
1726
1763
|
_scheduleReconnect() {
|
package/dist/index.js
CHANGED
|
@@ -19791,7 +19791,7 @@ class BroadcastChannelTransport {
|
|
|
19791
19791
|
const lockName = `exotel-ai-assist-leader:${sessionHash}`;
|
|
19792
19792
|
this.channel = new BroadcastChannel(channelName);
|
|
19793
19793
|
this.channel.onmessage = ev => {
|
|
19794
|
-
var _a;
|
|
19794
|
+
var _a, _b, _c;
|
|
19795
19795
|
const data = ev.data;
|
|
19796
19796
|
if (this.isLeader) {
|
|
19797
19797
|
if (data.type === "JOIN" && this.socketConnected && this.ackPayload) {
|
|
@@ -19803,11 +19803,18 @@ class BroadcastChannelTransport {
|
|
|
19803
19803
|
payload: this.ackPayload
|
|
19804
19804
|
});
|
|
19805
19805
|
}
|
|
19806
|
+
if (data.type === "SEND" && ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
19807
|
+
this.socket.send(data.payload);
|
|
19808
|
+
}
|
|
19809
|
+
if (data.type === "FEEDBACK_SYNC") {
|
|
19810
|
+
(_b = this.handler) === null || _b === void 0 ? void 0 : _b.call(this, data);
|
|
19811
|
+
}
|
|
19806
19812
|
return;
|
|
19807
19813
|
}
|
|
19808
|
-
// Follower: forward
|
|
19809
|
-
|
|
19810
|
-
|
|
19814
|
+
// Follower: forward server messages to the controller handler.
|
|
19815
|
+
// JOIN and SEND are internal transport messages, not controller messages.
|
|
19816
|
+
if (data.type !== "JOIN" && data.type !== "SEND") {
|
|
19817
|
+
(_c = this.handler) === null || _c === void 0 ? void 0 : _c.call(this, data);
|
|
19811
19818
|
}
|
|
19812
19819
|
};
|
|
19813
19820
|
this._electLeader(lockName);
|
|
@@ -19917,8 +19924,15 @@ class BroadcastChannelTransport {
|
|
|
19917
19924
|
}
|
|
19918
19925
|
send(payload) {
|
|
19919
19926
|
var _a;
|
|
19920
|
-
if (this.isLeader
|
|
19921
|
-
this.socket.
|
|
19927
|
+
if (this.isLeader) {
|
|
19928
|
+
if (((_a = this.socket) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
19929
|
+
this.socket.send(payload);
|
|
19930
|
+
}
|
|
19931
|
+
} else {
|
|
19932
|
+
this.channel.postMessage({
|
|
19933
|
+
type: "SEND",
|
|
19934
|
+
payload
|
|
19935
|
+
});
|
|
19922
19936
|
}
|
|
19923
19937
|
}
|
|
19924
19938
|
onMessage(handler) {
|
|
@@ -19927,8 +19941,22 @@ class BroadcastChannelTransport {
|
|
|
19927
19941
|
markAcknowledged(rawAckPayload) {
|
|
19928
19942
|
this.ackPayload = rawAckPayload;
|
|
19929
19943
|
}
|
|
19944
|
+
broadcastFeedbackSync(sequence, feedbackType, badFeedbackReason) {
|
|
19945
|
+
this.channel.postMessage({
|
|
19946
|
+
type: "FEEDBACK_SYNC",
|
|
19947
|
+
sequence,
|
|
19948
|
+
feedbackType,
|
|
19949
|
+
badFeedbackReason
|
|
19950
|
+
});
|
|
19951
|
+
}
|
|
19930
19952
|
destroy() {
|
|
19931
19953
|
var _a;
|
|
19954
|
+
if (this.isLeader) {
|
|
19955
|
+
this.channel.postMessage({
|
|
19956
|
+
type: "DISCONNECTED",
|
|
19957
|
+
code: 1000
|
|
19958
|
+
});
|
|
19959
|
+
}
|
|
19932
19960
|
this._destroyed = true;
|
|
19933
19961
|
this.disconnect();
|
|
19934
19962
|
this.channel.close();
|
|
@@ -21121,6 +21149,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21121
21149
|
};
|
|
21122
21150
|
try {
|
|
21123
21151
|
this.transport.send(JSON.stringify(message));
|
|
21152
|
+
this.transport.broadcastFeedbackSync(sequence, feedbackType, badFeedbackReason);
|
|
21124
21153
|
return true;
|
|
21125
21154
|
} catch (error) {
|
|
21126
21155
|
console.error("[ExotelAIAssist] Failed to send feedback:", error);
|
|
@@ -21147,12 +21176,13 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21147
21176
|
if (this.connectionEstablished) {
|
|
21148
21177
|
const wasThrottled = this._streamState === "throttled";
|
|
21149
21178
|
if (this.readyFired && !wasThrottled) {
|
|
21150
|
-
this.readyFired = false;
|
|
21151
21179
|
this.emit("onReady", false);
|
|
21152
21180
|
}
|
|
21181
|
+
this.connectionEstablished = false;
|
|
21182
|
+
this.readyFired = false;
|
|
21183
|
+
this._streamState = null;
|
|
21153
21184
|
this._setStatus("disconnected");
|
|
21154
21185
|
this.emit("onCallEnd");
|
|
21155
|
-
this.destroy();
|
|
21156
21186
|
} else {
|
|
21157
21187
|
this._scheduleReconnect();
|
|
21158
21188
|
}
|
|
@@ -21163,6 +21193,13 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21163
21193
|
case "MESSAGE":
|
|
21164
21194
|
if (msg.payload) this._handleServerPayload(msg.payload);
|
|
21165
21195
|
break;
|
|
21196
|
+
case "FEEDBACK_SYNC":
|
|
21197
|
+
this.emit("suggestionFeedbackSync", {
|
|
21198
|
+
sequence: msg.sequence,
|
|
21199
|
+
feedbackType: msg.feedbackType,
|
|
21200
|
+
badFeedbackReason: msg.badFeedbackReason
|
|
21201
|
+
});
|
|
21202
|
+
break;
|
|
21166
21203
|
}
|
|
21167
21204
|
}
|
|
21168
21205
|
_scheduleReconnect() {
|
|
@@ -21308,16 +21345,36 @@ function useExotelAIAssist(params) {
|
|
|
21308
21345
|
setLastError(null);
|
|
21309
21346
|
const ctrl = new ExotelAIAssistController(params);
|
|
21310
21347
|
controllerRef.current = ctrl;
|
|
21311
|
-
ctrl.on("onReady",
|
|
21348
|
+
ctrl.on("onReady", ready => {
|
|
21349
|
+
setIsReady(ready);
|
|
21350
|
+
if (!ready) {
|
|
21351
|
+
setSuggestions([]);
|
|
21352
|
+
setTranscripts([]);
|
|
21353
|
+
setSentiment(null);
|
|
21354
|
+
}
|
|
21355
|
+
});
|
|
21312
21356
|
ctrl.on("streamState", setStreamState);
|
|
21313
21357
|
ctrl.on("botConfig", setBotConfig);
|
|
21314
|
-
ctrl.on("suggestion", s => setSuggestions(prev =>
|
|
21358
|
+
ctrl.on("suggestion", s => setSuggestions(prev => {
|
|
21359
|
+
if (prev.some(existing => existing.sequence === s.sequence)) return prev;
|
|
21360
|
+
return [...prev, s].slice(-50);
|
|
21361
|
+
}));
|
|
21315
21362
|
ctrl.on("sentiment", setSentiment);
|
|
21316
21363
|
ctrl.on("transcript", lines => setTranscripts(prev => {
|
|
21317
21364
|
const map = new Map(prev.map(l => [l.id, l]));
|
|
21318
21365
|
for (const l of lines) map.set(l.id, l);
|
|
21319
21366
|
return Array.from(map.values()).sort((a, b) => a.startTime - b.startTime);
|
|
21320
21367
|
}));
|
|
21368
|
+
ctrl.on("suggestionFeedbackSync", ({
|
|
21369
|
+
sequence,
|
|
21370
|
+
feedbackType,
|
|
21371
|
+
badFeedbackReason
|
|
21372
|
+
}) => {
|
|
21373
|
+
setSuggestions(prev => prev.map(s => s.sequence === sequence ? Object.assign(Object.assign({}, s), {
|
|
21374
|
+
feedbackType,
|
|
21375
|
+
badFeedbackReason
|
|
21376
|
+
}) : s));
|
|
21377
|
+
});
|
|
21321
21378
|
ctrl.on("error", setLastError);
|
|
21322
21379
|
ctrl.connect();
|
|
21323
21380
|
return () => {
|
package/dist/react/index.js
CHANGED
|
@@ -5077,7 +5077,7 @@ class BroadcastChannelTransport {
|
|
|
5077
5077
|
const lockName = `exotel-ai-assist-leader:${sessionHash}`;
|
|
5078
5078
|
this.channel = new BroadcastChannel(channelName);
|
|
5079
5079
|
this.channel.onmessage = ev => {
|
|
5080
|
-
var _a;
|
|
5080
|
+
var _a, _b, _c;
|
|
5081
5081
|
const data = ev.data;
|
|
5082
5082
|
if (this.isLeader) {
|
|
5083
5083
|
if (data.type === "JOIN" && this.socketConnected && this.ackPayload) {
|
|
@@ -5089,11 +5089,18 @@ class BroadcastChannelTransport {
|
|
|
5089
5089
|
payload: this.ackPayload
|
|
5090
5090
|
});
|
|
5091
5091
|
}
|
|
5092
|
+
if (data.type === "SEND" && ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
5093
|
+
this.socket.send(data.payload);
|
|
5094
|
+
}
|
|
5095
|
+
if (data.type === "FEEDBACK_SYNC") {
|
|
5096
|
+
(_b = this.handler) === null || _b === void 0 ? void 0 : _b.call(this, data);
|
|
5097
|
+
}
|
|
5092
5098
|
return;
|
|
5093
5099
|
}
|
|
5094
|
-
// Follower: forward
|
|
5095
|
-
|
|
5096
|
-
|
|
5100
|
+
// Follower: forward server messages to the controller handler.
|
|
5101
|
+
// JOIN and SEND are internal transport messages, not controller messages.
|
|
5102
|
+
if (data.type !== "JOIN" && data.type !== "SEND") {
|
|
5103
|
+
(_c = this.handler) === null || _c === void 0 ? void 0 : _c.call(this, data);
|
|
5097
5104
|
}
|
|
5098
5105
|
};
|
|
5099
5106
|
this._electLeader(lockName);
|
|
@@ -5203,8 +5210,15 @@ class BroadcastChannelTransport {
|
|
|
5203
5210
|
}
|
|
5204
5211
|
send(payload) {
|
|
5205
5212
|
var _a;
|
|
5206
|
-
if (this.isLeader
|
|
5207
|
-
this.socket.
|
|
5213
|
+
if (this.isLeader) {
|
|
5214
|
+
if (((_a = this.socket) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
5215
|
+
this.socket.send(payload);
|
|
5216
|
+
}
|
|
5217
|
+
} else {
|
|
5218
|
+
this.channel.postMessage({
|
|
5219
|
+
type: "SEND",
|
|
5220
|
+
payload
|
|
5221
|
+
});
|
|
5208
5222
|
}
|
|
5209
5223
|
}
|
|
5210
5224
|
onMessage(handler) {
|
|
@@ -5213,8 +5227,22 @@ class BroadcastChannelTransport {
|
|
|
5213
5227
|
markAcknowledged(rawAckPayload) {
|
|
5214
5228
|
this.ackPayload = rawAckPayload;
|
|
5215
5229
|
}
|
|
5230
|
+
broadcastFeedbackSync(sequence, feedbackType, badFeedbackReason) {
|
|
5231
|
+
this.channel.postMessage({
|
|
5232
|
+
type: "FEEDBACK_SYNC",
|
|
5233
|
+
sequence,
|
|
5234
|
+
feedbackType,
|
|
5235
|
+
badFeedbackReason
|
|
5236
|
+
});
|
|
5237
|
+
}
|
|
5216
5238
|
destroy() {
|
|
5217
5239
|
var _a;
|
|
5240
|
+
if (this.isLeader) {
|
|
5241
|
+
this.channel.postMessage({
|
|
5242
|
+
type: "DISCONNECTED",
|
|
5243
|
+
code: 1000
|
|
5244
|
+
});
|
|
5245
|
+
}
|
|
5218
5246
|
this._destroyed = true;
|
|
5219
5247
|
this.disconnect();
|
|
5220
5248
|
this.channel.close();
|
|
@@ -6407,6 +6435,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6407
6435
|
};
|
|
6408
6436
|
try {
|
|
6409
6437
|
this.transport.send(JSON.stringify(message));
|
|
6438
|
+
this.transport.broadcastFeedbackSync(sequence, feedbackType, badFeedbackReason);
|
|
6410
6439
|
return true;
|
|
6411
6440
|
} catch (error) {
|
|
6412
6441
|
console.error("[ExotelAIAssist] Failed to send feedback:", error);
|
|
@@ -6433,12 +6462,13 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6433
6462
|
if (this.connectionEstablished) {
|
|
6434
6463
|
const wasThrottled = this._streamState === "throttled";
|
|
6435
6464
|
if (this.readyFired && !wasThrottled) {
|
|
6436
|
-
this.readyFired = false;
|
|
6437
6465
|
this.emit("onReady", false);
|
|
6438
6466
|
}
|
|
6467
|
+
this.connectionEstablished = false;
|
|
6468
|
+
this.readyFired = false;
|
|
6469
|
+
this._streamState = null;
|
|
6439
6470
|
this._setStatus("disconnected");
|
|
6440
6471
|
this.emit("onCallEnd");
|
|
6441
|
-
this.destroy();
|
|
6442
6472
|
} else {
|
|
6443
6473
|
this._scheduleReconnect();
|
|
6444
6474
|
}
|
|
@@ -6449,6 +6479,13 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6449
6479
|
case "MESSAGE":
|
|
6450
6480
|
if (msg.payload) this._handleServerPayload(msg.payload);
|
|
6451
6481
|
break;
|
|
6482
|
+
case "FEEDBACK_SYNC":
|
|
6483
|
+
this.emit("suggestionFeedbackSync", {
|
|
6484
|
+
sequence: msg.sequence,
|
|
6485
|
+
feedbackType: msg.feedbackType,
|
|
6486
|
+
badFeedbackReason: msg.badFeedbackReason
|
|
6487
|
+
});
|
|
6488
|
+
break;
|
|
6452
6489
|
}
|
|
6453
6490
|
}
|
|
6454
6491
|
_scheduleReconnect() {
|
|
@@ -6594,16 +6631,36 @@ function useExotelAIAssist(params) {
|
|
|
6594
6631
|
setLastError(null);
|
|
6595
6632
|
const ctrl = new ExotelAIAssistController(params);
|
|
6596
6633
|
controllerRef.current = ctrl;
|
|
6597
|
-
ctrl.on("onReady",
|
|
6634
|
+
ctrl.on("onReady", ready => {
|
|
6635
|
+
setIsReady(ready);
|
|
6636
|
+
if (!ready) {
|
|
6637
|
+
setSuggestions([]);
|
|
6638
|
+
setTranscripts([]);
|
|
6639
|
+
setSentiment(null);
|
|
6640
|
+
}
|
|
6641
|
+
});
|
|
6598
6642
|
ctrl.on("streamState", setStreamState);
|
|
6599
6643
|
ctrl.on("botConfig", setBotConfig);
|
|
6600
|
-
ctrl.on("suggestion", s => setSuggestions(prev =>
|
|
6644
|
+
ctrl.on("suggestion", s => setSuggestions(prev => {
|
|
6645
|
+
if (prev.some(existing => existing.sequence === s.sequence)) return prev;
|
|
6646
|
+
return [...prev, s].slice(-50);
|
|
6647
|
+
}));
|
|
6601
6648
|
ctrl.on("sentiment", setSentiment);
|
|
6602
6649
|
ctrl.on("transcript", lines => setTranscripts(prev => {
|
|
6603
6650
|
const map = new Map(prev.map(l => [l.id, l]));
|
|
6604
6651
|
for (const l of lines) map.set(l.id, l);
|
|
6605
6652
|
return Array.from(map.values()).sort((a, b) => a.startTime - b.startTime);
|
|
6606
6653
|
}));
|
|
6654
|
+
ctrl.on("suggestionFeedbackSync", ({
|
|
6655
|
+
sequence,
|
|
6656
|
+
feedbackType,
|
|
6657
|
+
badFeedbackReason
|
|
6658
|
+
}) => {
|
|
6659
|
+
setSuggestions(prev => prev.map(s => s.sequence === sequence ? Object.assign(Object.assign({}, s), {
|
|
6660
|
+
feedbackType,
|
|
6661
|
+
badFeedbackReason
|
|
6662
|
+
}) : s));
|
|
6663
|
+
});
|
|
6607
6664
|
ctrl.on("error", setLastError);
|
|
6608
6665
|
ctrl.connect();
|
|
6609
6666
|
return () => {
|
|
@@ -8,6 +8,8 @@ export interface ITransport {
|
|
|
8
8
|
/** Store the raw ack payload so follower tabs joining later receive it as a
|
|
9
9
|
* MESSAGE replay instead of a bare ACKNOWLEDGED signal. */
|
|
10
10
|
markAcknowledged(rawAckPayload: string): void;
|
|
11
|
+
/** Broadcast a feedback state change to other tabs for UI sync. */
|
|
12
|
+
broadcastFeedbackSync(sequence: number, feedbackType: "good" | "bad" | null, badFeedbackReason: string | null): void;
|
|
11
13
|
destroy(): void;
|
|
12
14
|
}
|
|
13
15
|
export declare class BroadcastChannelTransport implements ITransport {
|
|
@@ -35,6 +37,7 @@ export declare class BroadcastChannelTransport implements ITransport {
|
|
|
35
37
|
send(payload: string): void;
|
|
36
38
|
onMessage(handler: TransportMessageHandler): void;
|
|
37
39
|
markAcknowledged(rawAckPayload: string): void;
|
|
40
|
+
broadcastFeedbackSync(sequence: number, feedbackType: "good" | "bad" | null, badFeedbackReason: string | null): void;
|
|
38
41
|
destroy(): void;
|
|
39
42
|
}
|
|
40
43
|
export declare function createTransport(sessionHash: string): ITransport;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -117,6 +117,12 @@ export interface ControllerEvents {
|
|
|
117
117
|
statusChange: (status: ConnectionStatus) => void;
|
|
118
118
|
/** Fires whenever the server sends a `stream_status` message. */
|
|
119
119
|
streamState: (state: StreamState) => void;
|
|
120
|
+
/** Fires when another tab updates suggestion feedback, for cross-tab UI sync. */
|
|
121
|
+
suggestionFeedbackSync: (data: {
|
|
122
|
+
sequence: number;
|
|
123
|
+
feedbackType: "good" | "bad" | null;
|
|
124
|
+
badFeedbackReason: string | null;
|
|
125
|
+
}) => void;
|
|
120
126
|
error: (err: Error) => void;
|
|
121
127
|
raw: (data: unknown) => void;
|
|
122
128
|
}
|
|
@@ -133,5 +139,10 @@ export type WorkerInboundMessage = {
|
|
|
133
139
|
} | {
|
|
134
140
|
type: "ERROR";
|
|
135
141
|
message: string;
|
|
142
|
+
} | {
|
|
143
|
+
type: "FEEDBACK_SYNC";
|
|
144
|
+
sequence: number;
|
|
145
|
+
feedbackType: "good" | "bad" | null;
|
|
146
|
+
badFeedbackReason: string | null;
|
|
136
147
|
};
|
|
137
148
|
export {};
|
package/package.json
CHANGED
package/src/controller/index.ts
CHANGED
|
@@ -116,6 +116,7 @@ export class ExotelAIAssistController extends EventEmitter<ControllerEvents> {
|
|
|
116
116
|
|
|
117
117
|
try {
|
|
118
118
|
this.transport.send(JSON.stringify(message));
|
|
119
|
+
this.transport.broadcastFeedbackSync(sequence, feedbackType, badFeedbackReason);
|
|
119
120
|
return true;
|
|
120
121
|
} catch (error) {
|
|
121
122
|
console.error("[ExotelAIAssist] Failed to send feedback:", error);
|
|
@@ -145,12 +146,13 @@ export class ExotelAIAssistController extends EventEmitter<ControllerEvents> {
|
|
|
145
146
|
if (this.connectionEstablished) {
|
|
146
147
|
const wasThrottled = this._streamState === "throttled";
|
|
147
148
|
if (this.readyFired && !wasThrottled) {
|
|
148
|
-
this.readyFired = false;
|
|
149
149
|
this.emit("onReady", false);
|
|
150
150
|
}
|
|
151
|
+
this.connectionEstablished = false;
|
|
152
|
+
this.readyFired = false;
|
|
153
|
+
this._streamState = null;
|
|
151
154
|
this._setStatus("disconnected");
|
|
152
155
|
this.emit("onCallEnd");
|
|
153
|
-
this.destroy();
|
|
154
156
|
} else {
|
|
155
157
|
this._scheduleReconnect();
|
|
156
158
|
}
|
|
@@ -163,6 +165,14 @@ export class ExotelAIAssistController extends EventEmitter<ControllerEvents> {
|
|
|
163
165
|
case "MESSAGE":
|
|
164
166
|
if (msg.payload) this._handleServerPayload(msg.payload);
|
|
165
167
|
break;
|
|
168
|
+
|
|
169
|
+
case "FEEDBACK_SYNC":
|
|
170
|
+
this.emit("suggestionFeedbackSync", {
|
|
171
|
+
sequence: msg.sequence,
|
|
172
|
+
feedbackType: msg.feedbackType,
|
|
173
|
+
badFeedbackReason: msg.badFeedbackReason,
|
|
174
|
+
});
|
|
175
|
+
break;
|
|
166
176
|
}
|
|
167
177
|
}
|
|
168
178
|
|
|
@@ -63,10 +63,22 @@ export function useExotelAIAssist(params: ExotelAIAssistParams): UseExotelAIAssi
|
|
|
63
63
|
const ctrl = new ExotelAIAssistController(params);
|
|
64
64
|
controllerRef.current = ctrl;
|
|
65
65
|
|
|
66
|
-
ctrl.on("onReady",
|
|
66
|
+
ctrl.on("onReady", (ready) => {
|
|
67
|
+
setIsReady(ready);
|
|
68
|
+
if (!ready) {
|
|
69
|
+
setSuggestions([]);
|
|
70
|
+
setTranscripts([]);
|
|
71
|
+
setSentiment(null);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
67
74
|
ctrl.on("streamState", setStreamState);
|
|
68
75
|
ctrl.on("botConfig", setBotConfig);
|
|
69
|
-
ctrl.on("suggestion", (s) =>
|
|
76
|
+
ctrl.on("suggestion", (s) =>
|
|
77
|
+
setSuggestions((prev) => {
|
|
78
|
+
if (prev.some((existing) => existing.sequence === s.sequence)) return prev;
|
|
79
|
+
return [...prev, s].slice(-MAX_SUGGESTIONS);
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
70
82
|
ctrl.on("sentiment", setSentiment);
|
|
71
83
|
ctrl.on("transcript", (lines) =>
|
|
72
84
|
setTranscripts((prev) => {
|
|
@@ -75,6 +87,9 @@ export function useExotelAIAssist(params: ExotelAIAssistParams): UseExotelAIAssi
|
|
|
75
87
|
return Array.from(map.values()).sort((a, b) => a.startTime - b.startTime);
|
|
76
88
|
}),
|
|
77
89
|
);
|
|
90
|
+
ctrl.on("suggestionFeedbackSync", ({ sequence, feedbackType, badFeedbackReason }) => {
|
|
91
|
+
setSuggestions((prev) => prev.map((s) => (s.sequence === sequence ? { ...s, feedbackType, badFeedbackReason } : s)));
|
|
92
|
+
});
|
|
78
93
|
ctrl.on("error", setLastError);
|
|
79
94
|
|
|
80
95
|
ctrl.connect();
|
package/src/transport/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ type TransportMessageHandler = (msg: WorkerInboundMessage) => void;
|
|
|
4
4
|
|
|
5
5
|
// Internal-only message used for the leader-sync handshake.
|
|
6
6
|
// Never forwarded to the controller.
|
|
7
|
-
type InternalMessage = WorkerInboundMessage | { type: "JOIN" };
|
|
7
|
+
type InternalMessage = WorkerInboundMessage | { type: "JOIN" } | { type: "SEND"; payload: string };
|
|
8
8
|
|
|
9
9
|
export interface ITransport {
|
|
10
10
|
connect(url: string, authToken: string): void;
|
|
@@ -14,6 +14,8 @@ export interface ITransport {
|
|
|
14
14
|
/** Store the raw ack payload so follower tabs joining later receive it as a
|
|
15
15
|
* MESSAGE replay instead of a bare ACKNOWLEDGED signal. */
|
|
16
16
|
markAcknowledged(rawAckPayload: string): void;
|
|
17
|
+
/** Broadcast a feedback state change to other tabs for UI sync. */
|
|
18
|
+
broadcastFeedbackSync(sequence: number, feedbackType: "good" | "bad" | null, badFeedbackReason: string | null): void;
|
|
17
19
|
destroy(): void;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -48,11 +50,18 @@ export class BroadcastChannelTransport implements ITransport {
|
|
|
48
50
|
this.channel.postMessage({ type: "CONNECTED" } satisfies WorkerInboundMessage);
|
|
49
51
|
this.channel.postMessage({ type: "MESSAGE", payload: this.ackPayload } satisfies WorkerInboundMessage);
|
|
50
52
|
}
|
|
53
|
+
if (data.type === "SEND" && this.socket?.readyState === WebSocket.OPEN) {
|
|
54
|
+
this.socket.send((data as { type: "SEND"; payload: string }).payload);
|
|
55
|
+
}
|
|
56
|
+
if (data.type === "FEEDBACK_SYNC") {
|
|
57
|
+
this.handler?.(data as WorkerInboundMessage);
|
|
58
|
+
}
|
|
51
59
|
return;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
// Follower: forward
|
|
55
|
-
|
|
62
|
+
// Follower: forward server messages to the controller handler.
|
|
63
|
+
// JOIN and SEND are internal transport messages, not controller messages.
|
|
64
|
+
if (data.type !== "JOIN" && data.type !== "SEND") {
|
|
56
65
|
this.handler?.(data as WorkerInboundMessage);
|
|
57
66
|
}
|
|
58
67
|
};
|
|
@@ -154,8 +163,12 @@ export class BroadcastChannelTransport implements ITransport {
|
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
send(payload: string): void {
|
|
157
|
-
if (this.isLeader
|
|
158
|
-
this.socket.
|
|
166
|
+
if (this.isLeader) {
|
|
167
|
+
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
168
|
+
this.socket.send(payload);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
this.channel.postMessage({ type: "SEND", payload } satisfies InternalMessage);
|
|
159
172
|
}
|
|
160
173
|
}
|
|
161
174
|
|
|
@@ -167,7 +180,14 @@ export class BroadcastChannelTransport implements ITransport {
|
|
|
167
180
|
this.ackPayload = rawAckPayload;
|
|
168
181
|
}
|
|
169
182
|
|
|
183
|
+
broadcastFeedbackSync(sequence: number, feedbackType: "good" | "bad" | null, badFeedbackReason: string | null): void {
|
|
184
|
+
this.channel.postMessage({ type: "FEEDBACK_SYNC", sequence, feedbackType, badFeedbackReason } satisfies WorkerInboundMessage);
|
|
185
|
+
}
|
|
186
|
+
|
|
170
187
|
destroy(): void {
|
|
188
|
+
if (this.isLeader) {
|
|
189
|
+
this.channel.postMessage({ type: "DISCONNECTED", code: 1000 } satisfies WorkerInboundMessage);
|
|
190
|
+
}
|
|
171
191
|
this._destroyed = true;
|
|
172
192
|
this.disconnect();
|
|
173
193
|
this.channel.close();
|
package/src/types/index.ts
CHANGED
|
@@ -137,6 +137,8 @@ export interface ControllerEvents {
|
|
|
137
137
|
statusChange: (status: ConnectionStatus) => void;
|
|
138
138
|
/** Fires whenever the server sends a `stream_status` message. */
|
|
139
139
|
streamState: (state: StreamState) => void;
|
|
140
|
+
/** Fires when another tab updates suggestion feedback, for cross-tab UI sync. */
|
|
141
|
+
suggestionFeedbackSync: (data: { sequence: number; feedbackType: "good" | "bad" | null; badFeedbackReason: string | null }) => void;
|
|
140
142
|
error: (err: Error) => void;
|
|
141
143
|
raw: (data: unknown) => void;
|
|
142
144
|
}
|
|
@@ -146,4 +148,5 @@ export type WorkerInboundMessage =
|
|
|
146
148
|
| { type: "CONNECTED" }
|
|
147
149
|
| { type: "ACKNOWLEDGED" }
|
|
148
150
|
| { type: "DISCONNECTED"; code?: number }
|
|
149
|
-
| { type: "ERROR"; message: string }
|
|
151
|
+
| { type: "ERROR"; message: string }
|
|
152
|
+
| { type: "FEEDBACK_SYNC"; sequence: number; feedbackType: "good" | "bad" | null; badFeedbackReason: string | null };
|