@exotel-npm-dev/exotel-ai-assist 1.3.0 → 1.3.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/dist/controller/index.js +40 -10
- package/dist/index.js +54 -11
- package/dist/react/index.js +54 -11
- package/dist/transport/index.d.ts +3 -0
- package/dist/types/index.d.ts +13 -0
- package/package.json +1 -1
- package/src/controller/index.ts +11 -2
- package/src/hooks/useExotelAIAssist.ts +9 -1
- package/src/transport/index.ts +22 -5
- package/src/types/index.ts +6 -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,6 +499,14 @@ 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;
|
|
490
512
|
if (this.isLeader) {
|
|
@@ -1685,6 +1707,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1685
1707
|
};
|
|
1686
1708
|
try {
|
|
1687
1709
|
this.transport.send(JSON.stringify(message));
|
|
1710
|
+
this.transport.broadcastFeedbackSync(sequence, feedbackType, badFeedbackReason);
|
|
1688
1711
|
return true;
|
|
1689
1712
|
} catch (error) {
|
|
1690
1713
|
console.error("[ExotelAIAssist] Failed to send feedback:", error);
|
|
@@ -1728,6 +1751,13 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1728
1751
|
case "MESSAGE":
|
|
1729
1752
|
if (msg.payload) this._handleServerPayload(msg.payload);
|
|
1730
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;
|
|
1731
1761
|
}
|
|
1732
1762
|
}
|
|
1733
1763
|
_scheduleReconnect() {
|
|
@@ -1762,11 +1792,11 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1762
1792
|
this.emit("onReady", true);
|
|
1763
1793
|
}
|
|
1764
1794
|
_handleServerPayload(raw) {
|
|
1765
|
-
var _a, _b;
|
|
1795
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1766
1796
|
let parsed;
|
|
1767
1797
|
try {
|
|
1768
1798
|
parsed = JSON.parse(raw);
|
|
1769
|
-
} catch (
|
|
1799
|
+
} catch (_g) {
|
|
1770
1800
|
this.emit("error", new Error(`Failed to parse server message: ${raw}`));
|
|
1771
1801
|
return;
|
|
1772
1802
|
}
|
|
@@ -1820,8 +1850,8 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
1820
1850
|
value: event.value.text,
|
|
1821
1851
|
timestamp: now,
|
|
1822
1852
|
sequence: event.value.sequence,
|
|
1823
|
-
feedbackType: null,
|
|
1824
|
-
badFeedbackReason: null
|
|
1853
|
+
feedbackType: (_d = (_c = event.value) === null || _c === void 0 ? void 0 : _c.feedback_type) !== null && _d !== void 0 ? _d : null,
|
|
1854
|
+
badFeedbackReason: (_f = (_e = event.value) === null || _e === void 0 ? void 0 : _e.bad_feedback_reason) !== null && _f !== void 0 ? _f : null
|
|
1825
1855
|
};
|
|
1826
1856
|
this.emit("suggestion", suggestion);
|
|
1827
1857
|
}
|
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,6 +19941,14 @@ 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;
|
|
19932
19954
|
if (this.isLeader) {
|
|
@@ -21127,6 +21149,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21127
21149
|
};
|
|
21128
21150
|
try {
|
|
21129
21151
|
this.transport.send(JSON.stringify(message));
|
|
21152
|
+
this.transport.broadcastFeedbackSync(sequence, feedbackType, badFeedbackReason);
|
|
21130
21153
|
return true;
|
|
21131
21154
|
} catch (error) {
|
|
21132
21155
|
console.error("[ExotelAIAssist] Failed to send feedback:", error);
|
|
@@ -21170,6 +21193,13 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21170
21193
|
case "MESSAGE":
|
|
21171
21194
|
if (msg.payload) this._handleServerPayload(msg.payload);
|
|
21172
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;
|
|
21173
21203
|
}
|
|
21174
21204
|
}
|
|
21175
21205
|
_scheduleReconnect() {
|
|
@@ -21204,11 +21234,11 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21204
21234
|
this.emit("onReady", true);
|
|
21205
21235
|
}
|
|
21206
21236
|
_handleServerPayload(raw) {
|
|
21207
|
-
var _a, _b;
|
|
21237
|
+
var _a, _b, _c, _d, _e, _f;
|
|
21208
21238
|
let parsed;
|
|
21209
21239
|
try {
|
|
21210
21240
|
parsed = JSON.parse(raw);
|
|
21211
|
-
} catch (
|
|
21241
|
+
} catch (_g) {
|
|
21212
21242
|
this.emit("error", new Error(`Failed to parse server message: ${raw}`));
|
|
21213
21243
|
return;
|
|
21214
21244
|
}
|
|
@@ -21262,8 +21292,8 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
21262
21292
|
value: event.value.text,
|
|
21263
21293
|
timestamp: now,
|
|
21264
21294
|
sequence: event.value.sequence,
|
|
21265
|
-
feedbackType: null,
|
|
21266
|
-
badFeedbackReason: null
|
|
21295
|
+
feedbackType: (_d = (_c = event.value) === null || _c === void 0 ? void 0 : _c.feedback_type) !== null && _d !== void 0 ? _d : null,
|
|
21296
|
+
badFeedbackReason: (_f = (_e = event.value) === null || _e === void 0 ? void 0 : _e.bad_feedback_reason) !== null && _f !== void 0 ? _f : null
|
|
21267
21297
|
};
|
|
21268
21298
|
this.emit("suggestion", suggestion);
|
|
21269
21299
|
}
|
|
@@ -21325,13 +21355,26 @@ function useExotelAIAssist(params) {
|
|
|
21325
21355
|
});
|
|
21326
21356
|
ctrl.on("streamState", setStreamState);
|
|
21327
21357
|
ctrl.on("botConfig", setBotConfig);
|
|
21328
|
-
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
|
+
}));
|
|
21329
21362
|
ctrl.on("sentiment", setSentiment);
|
|
21330
21363
|
ctrl.on("transcript", lines => setTranscripts(prev => {
|
|
21331
21364
|
const map = new Map(prev.map(l => [l.id, l]));
|
|
21332
21365
|
for (const l of lines) map.set(l.id, l);
|
|
21333
21366
|
return Array.from(map.values()).sort((a, b) => a.startTime - b.startTime);
|
|
21334
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
|
+
});
|
|
21335
21378
|
ctrl.on("error", setLastError);
|
|
21336
21379
|
ctrl.connect();
|
|
21337
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,6 +5227,14 @@ 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;
|
|
5218
5240
|
if (this.isLeader) {
|
|
@@ -6413,6 +6435,7 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6413
6435
|
};
|
|
6414
6436
|
try {
|
|
6415
6437
|
this.transport.send(JSON.stringify(message));
|
|
6438
|
+
this.transport.broadcastFeedbackSync(sequence, feedbackType, badFeedbackReason);
|
|
6416
6439
|
return true;
|
|
6417
6440
|
} catch (error) {
|
|
6418
6441
|
console.error("[ExotelAIAssist] Failed to send feedback:", error);
|
|
@@ -6456,6 +6479,13 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6456
6479
|
case "MESSAGE":
|
|
6457
6480
|
if (msg.payload) this._handleServerPayload(msg.payload);
|
|
6458
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;
|
|
6459
6489
|
}
|
|
6460
6490
|
}
|
|
6461
6491
|
_scheduleReconnect() {
|
|
@@ -6490,11 +6520,11 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6490
6520
|
this.emit("onReady", true);
|
|
6491
6521
|
}
|
|
6492
6522
|
_handleServerPayload(raw) {
|
|
6493
|
-
var _a, _b;
|
|
6523
|
+
var _a, _b, _c, _d, _e, _f;
|
|
6494
6524
|
let parsed;
|
|
6495
6525
|
try {
|
|
6496
6526
|
parsed = JSON.parse(raw);
|
|
6497
|
-
} catch (
|
|
6527
|
+
} catch (_g) {
|
|
6498
6528
|
this.emit("error", new Error(`Failed to parse server message: ${raw}`));
|
|
6499
6529
|
return;
|
|
6500
6530
|
}
|
|
@@ -6548,8 +6578,8 @@ class ExotelAIAssistController extends EventEmitter {
|
|
|
6548
6578
|
value: event.value.text,
|
|
6549
6579
|
timestamp: now,
|
|
6550
6580
|
sequence: event.value.sequence,
|
|
6551
|
-
feedbackType: null,
|
|
6552
|
-
badFeedbackReason: null
|
|
6581
|
+
feedbackType: (_d = (_c = event.value) === null || _c === void 0 ? void 0 : _c.feedback_type) !== null && _d !== void 0 ? _d : null,
|
|
6582
|
+
badFeedbackReason: (_f = (_e = event.value) === null || _e === void 0 ? void 0 : _e.bad_feedback_reason) !== null && _f !== void 0 ? _f : null
|
|
6553
6583
|
};
|
|
6554
6584
|
this.emit("suggestion", suggestion);
|
|
6555
6585
|
}
|
|
@@ -6611,13 +6641,26 @@ function useExotelAIAssist(params) {
|
|
|
6611
6641
|
});
|
|
6612
6642
|
ctrl.on("streamState", setStreamState);
|
|
6613
6643
|
ctrl.on("botConfig", setBotConfig);
|
|
6614
|
-
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
|
+
}));
|
|
6615
6648
|
ctrl.on("sentiment", setSentiment);
|
|
6616
6649
|
ctrl.on("transcript", lines => setTranscripts(prev => {
|
|
6617
6650
|
const map = new Map(prev.map(l => [l.id, l]));
|
|
6618
6651
|
for (const l of lines) map.set(l.id, l);
|
|
6619
6652
|
return Array.from(map.values()).sort((a, b) => a.startTime - b.startTime);
|
|
6620
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
|
+
});
|
|
6621
6664
|
ctrl.on("error", setLastError);
|
|
6622
6665
|
ctrl.connect();
|
|
6623
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
|
@@ -67,6 +67,8 @@ interface TranscriptMessage {
|
|
|
67
67
|
export interface SuggestionValue {
|
|
68
68
|
text: string;
|
|
69
69
|
sequence: number;
|
|
70
|
+
feedback_type?: "good" | "bad" | null;
|
|
71
|
+
bad_feedback_reason?: string | null;
|
|
70
72
|
}
|
|
71
73
|
export interface SuggestionFeedbackMessage {
|
|
72
74
|
type: "suggestion_feedback";
|
|
@@ -117,6 +119,12 @@ export interface ControllerEvents {
|
|
|
117
119
|
statusChange: (status: ConnectionStatus) => void;
|
|
118
120
|
/** Fires whenever the server sends a `stream_status` message. */
|
|
119
121
|
streamState: (state: StreamState) => void;
|
|
122
|
+
/** Fires when another tab updates suggestion feedback, for cross-tab UI sync. */
|
|
123
|
+
suggestionFeedbackSync: (data: {
|
|
124
|
+
sequence: number;
|
|
125
|
+
feedbackType: "good" | "bad" | null;
|
|
126
|
+
badFeedbackReason: string | null;
|
|
127
|
+
}) => void;
|
|
120
128
|
error: (err: Error) => void;
|
|
121
129
|
raw: (data: unknown) => void;
|
|
122
130
|
}
|
|
@@ -133,5 +141,10 @@ export type WorkerInboundMessage = {
|
|
|
133
141
|
} | {
|
|
134
142
|
type: "ERROR";
|
|
135
143
|
message: string;
|
|
144
|
+
} | {
|
|
145
|
+
type: "FEEDBACK_SYNC";
|
|
146
|
+
sequence: number;
|
|
147
|
+
feedbackType: "good" | "bad" | null;
|
|
148
|
+
badFeedbackReason: string | null;
|
|
136
149
|
};
|
|
137
150
|
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);
|
|
@@ -164,6 +165,14 @@ export class ExotelAIAssistController extends EventEmitter<ControllerEvents> {
|
|
|
164
165
|
case "MESSAGE":
|
|
165
166
|
if (msg.payload) this._handleServerPayload(msg.payload);
|
|
166
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;
|
|
167
176
|
}
|
|
168
177
|
}
|
|
169
178
|
|
|
@@ -270,8 +279,8 @@ export class ExotelAIAssistController extends EventEmitter<ControllerEvents> {
|
|
|
270
279
|
value: event.value.text,
|
|
271
280
|
timestamp: now,
|
|
272
281
|
sequence: event.value.sequence,
|
|
273
|
-
feedbackType: null,
|
|
274
|
-
badFeedbackReason: null,
|
|
282
|
+
feedbackType: event.value?.feedback_type ?? null,
|
|
283
|
+
badFeedbackReason: event.value?.bad_feedback_reason ?? null,
|
|
275
284
|
};
|
|
276
285
|
this.emit("suggestion", suggestion);
|
|
277
286
|
}
|
|
@@ -73,7 +73,12 @@ export function useExotelAIAssist(params: ExotelAIAssistParams): UseExotelAIAssi
|
|
|
73
73
|
});
|
|
74
74
|
ctrl.on("streamState", setStreamState);
|
|
75
75
|
ctrl.on("botConfig", setBotConfig);
|
|
76
|
-
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
|
+
);
|
|
77
82
|
ctrl.on("sentiment", setSentiment);
|
|
78
83
|
ctrl.on("transcript", (lines) =>
|
|
79
84
|
setTranscripts((prev) => {
|
|
@@ -82,6 +87,9 @@ export function useExotelAIAssist(params: ExotelAIAssistParams): UseExotelAIAssi
|
|
|
82
87
|
return Array.from(map.values()).sort((a, b) => a.startTime - b.startTime);
|
|
83
88
|
}),
|
|
84
89
|
);
|
|
90
|
+
ctrl.on("suggestionFeedbackSync", ({ sequence, feedbackType, badFeedbackReason }) => {
|
|
91
|
+
setSuggestions((prev) => prev.map((s) => (s.sequence === sequence ? { ...s, feedbackType, badFeedbackReason } : s)));
|
|
92
|
+
});
|
|
85
93
|
ctrl.on("error", setLastError);
|
|
86
94
|
|
|
87
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,6 +180,10 @@ 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 {
|
|
171
188
|
if (this.isLeader) {
|
|
172
189
|
this.channel.postMessage({ type: "DISCONNECTED", code: 1000 } satisfies WorkerInboundMessage);
|
package/src/types/index.ts
CHANGED
|
@@ -80,6 +80,8 @@ interface TranscriptMessage {
|
|
|
80
80
|
export interface SuggestionValue {
|
|
81
81
|
text: string;
|
|
82
82
|
sequence: number;
|
|
83
|
+
feedback_type?: "good" | "bad" | null;
|
|
84
|
+
bad_feedback_reason?: string | null;
|
|
83
85
|
}
|
|
84
86
|
export interface SuggestionFeedbackMessage {
|
|
85
87
|
type: "suggestion_feedback";
|
|
@@ -137,6 +139,8 @@ export interface ControllerEvents {
|
|
|
137
139
|
statusChange: (status: ConnectionStatus) => void;
|
|
138
140
|
/** Fires whenever the server sends a `stream_status` message. */
|
|
139
141
|
streamState: (state: StreamState) => void;
|
|
142
|
+
/** Fires when another tab updates suggestion feedback, for cross-tab UI sync. */
|
|
143
|
+
suggestionFeedbackSync: (data: { sequence: number; feedbackType: "good" | "bad" | null; badFeedbackReason: string | null }) => void;
|
|
140
144
|
error: (err: Error) => void;
|
|
141
145
|
raw: (data: unknown) => void;
|
|
142
146
|
}
|
|
@@ -146,4 +150,5 @@ export type WorkerInboundMessage =
|
|
|
146
150
|
| { type: "CONNECTED" }
|
|
147
151
|
| { type: "ACKNOWLEDGED" }
|
|
148
152
|
| { type: "DISCONNECTED"; code?: number }
|
|
149
|
-
| { type: "ERROR"; message: string }
|
|
153
|
+
| { type: "ERROR"; message: string }
|
|
154
|
+
| { type: "FEEDBACK_SYNC"; sequence: number; feedbackType: "good" | "bad" | null; badFeedbackReason: string | null };
|