@exotel-npm-dev/exotel-ai-assist 1.3.0 → 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 +36 -6
- package/dist/index.js +50 -7
- package/dist/react/index.js +50 -7
- 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 +9 -0
- package/src/hooks/useExotelAIAssist.ts +9 -1
- package/src/transport/index.ts +22 -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,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() {
|
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() {
|
|
@@ -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() {
|
|
@@ -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
|
@@ -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);
|
|
@@ -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
|
|
|
@@ -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
|
@@ -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 };
|