rio-assist-widget 0.1.9 → 0.1.10
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 +1 -2
- package/dist/rio-assist.js +46 -46
- package/package.json +1 -1
- package/src/components/rio-assist/rio-assist.ts +239 -70
- package/src/services/rioWebsocket.ts +120 -74
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit';
|
|
2
2
|
import { widgetStyles } from './rio-assist.styles';
|
|
3
3
|
import { renderRioAssist } from './rio-assist.template';
|
|
4
4
|
import {
|
|
@@ -47,8 +47,9 @@ export class RioAssistWidget extends LitElement {
|
|
|
47
47
|
conversationMenuPlacement: { state: true },
|
|
48
48
|
isFullscreen: { type: Boolean, state: true },
|
|
49
49
|
conversationScrollbar: { state: true },
|
|
50
|
-
showNewConversationShortcut: { type: Boolean, state: true },
|
|
51
|
-
|
|
50
|
+
showNewConversationShortcut: { type: Boolean, state: true },
|
|
51
|
+
conversations: { state: true },
|
|
52
|
+
};
|
|
52
53
|
|
|
53
54
|
open = false;
|
|
54
55
|
|
|
@@ -116,25 +117,7 @@ export class RioAssistWidget extends LitElement {
|
|
|
116
117
|
breaks: true,
|
|
117
118
|
}).use(markdownItTaskLists);
|
|
118
119
|
|
|
119
|
-
conversations: ConversationItem[] =
|
|
120
|
-
(_, index) => ({
|
|
121
|
-
id: `${index + 1}`,
|
|
122
|
-
title: [
|
|
123
|
-
'Caminhões com problema na frota de veículos.',
|
|
124
|
-
'Próximas manutenções periódicas preventivas.',
|
|
125
|
-
'Quais revisões meu plano inclui?',
|
|
126
|
-
'Como automatizar preenchimento de odômetro.',
|
|
127
|
-
'Valor das peças da próxima revisão.',
|
|
128
|
-
'O que é revisão de assentamento?',
|
|
129
|
-
'Alertas críticos ativos.',
|
|
130
|
-
'Veículo superaquecendo, causas e recomendações.',
|
|
131
|
-
'Calibragem recomendada nos pneus do e-Delivery.',
|
|
132
|
-
'Quantos mil km trocar o óleo do motor.',
|
|
133
|
-
'Qual a vida útil da bateria Moura M100HE.',
|
|
134
|
-
][index % 11],
|
|
135
|
-
updatedAt: new Date(Date.now() - index * 3600_000).toISOString(),
|
|
136
|
-
}),
|
|
137
|
-
);
|
|
120
|
+
conversations: ConversationItem[] = [];
|
|
138
121
|
|
|
139
122
|
get suggestions(): string[] {
|
|
140
123
|
if (!this.suggestionsSource) {
|
|
@@ -221,21 +204,25 @@ export class RioAssistWidget extends LitElement {
|
|
|
221
204
|
}
|
|
222
205
|
}
|
|
223
206
|
|
|
224
|
-
openConversationsPanel() {
|
|
225
|
-
this.showConversations = true;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
207
|
+
openConversationsPanel() {
|
|
208
|
+
this.showConversations = true;
|
|
209
|
+
this.requestConversationHistory();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
closeConversationsPanel() {
|
|
213
|
+
this.showConversations = false;
|
|
214
|
+
this.conversationMenuId = null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
toggleConversationsPanel() {
|
|
218
|
+
this.showConversations = !this.showConversations;
|
|
219
|
+
if (!this.showConversations) {
|
|
220
|
+
this.conversationMenuId = null;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.requestConversationHistory();
|
|
225
|
+
}
|
|
239
226
|
|
|
240
227
|
toggleNewConversationShortcut() {
|
|
241
228
|
this.showNewConversationShortcut = !this.showNewConversationShortcut;
|
|
@@ -550,11 +537,11 @@ export class RioAssistWidget extends LitElement {
|
|
|
550
537
|
}
|
|
551
538
|
}
|
|
552
539
|
|
|
553
|
-
private ensureRioClient() {
|
|
554
|
-
const token = this.rioToken.trim();
|
|
555
|
-
if (!token) {
|
|
556
|
-
throw new Error(
|
|
557
|
-
'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
|
|
540
|
+
private ensureRioClient() {
|
|
541
|
+
const token = this.rioToken.trim();
|
|
542
|
+
if (!token) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
|
|
558
545
|
);
|
|
559
546
|
}
|
|
560
547
|
|
|
@@ -565,16 +552,22 @@ export class RioAssistWidget extends LitElement {
|
|
|
565
552
|
this.handleIncomingMessage(incoming);
|
|
566
553
|
});
|
|
567
554
|
}
|
|
568
|
-
|
|
569
|
-
return this.rioClient;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
private handleIncomingMessage(message: RioIncomingMessage) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
555
|
+
|
|
556
|
+
return this.rioClient;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private handleIncomingMessage(message: RioIncomingMessage) {
|
|
560
|
+
if (this.isHistoryPayload(message)) {
|
|
561
|
+
this.logHistoryPayload(message);
|
|
562
|
+
this.applyConversationHistory(message.data);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const assistantMessage = this.createMessage('assistant', message.text);
|
|
567
|
+
this.messages = [...this.messages, assistantMessage];
|
|
568
|
+
this.clearLoadingGuard();
|
|
569
|
+
this.isLoading = false;
|
|
570
|
+
}
|
|
578
571
|
|
|
579
572
|
private teardownRioClient() {
|
|
580
573
|
if (this.rioUnsubscribe) {
|
|
@@ -582,15 +575,196 @@ export class RioAssistWidget extends LitElement {
|
|
|
582
575
|
this.rioUnsubscribe = null;
|
|
583
576
|
}
|
|
584
577
|
|
|
585
|
-
if (this.rioClient) {
|
|
586
|
-
this.rioClient.close();
|
|
587
|
-
this.rioClient = null;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
578
|
+
if (this.rioClient) {
|
|
579
|
+
this.rioClient.close();
|
|
580
|
+
this.rioClient = null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async requestConversationHistory(conversationId?: string) {
|
|
585
|
+
try {
|
|
586
|
+
const client = this.ensureRioClient();
|
|
587
|
+
const limit = 50;
|
|
588
|
+
|
|
589
|
+
console.info('[RioAssist][history] solicitando historico de conversas', {
|
|
590
|
+
conversationId: conversationId ?? null,
|
|
591
|
+
limit,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
await client.requestHistory({ conversationId, limit });
|
|
595
|
+
} catch (error) {
|
|
596
|
+
console.error('[RioAssist][history] erro ao solicitar historico', error);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private isHistoryPayload(message: RioIncomingMessage) {
|
|
601
|
+
if (
|
|
602
|
+
typeof message.action === 'string' &&
|
|
603
|
+
message.action.toLowerCase().includes('history')
|
|
604
|
+
) {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const data = message.data;
|
|
609
|
+
if (data && typeof data === 'object') {
|
|
610
|
+
const action = (data as any).action;
|
|
611
|
+
if (typeof action === 'string' && action.toLowerCase().includes('history')) {
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (Array.isArray((data as any).history) || Array.isArray((data as any).conversations)) {
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private logHistoryPayload(message: RioIncomingMessage) {
|
|
624
|
+
const label = '[RioAssist][history] payload recebido do websocket';
|
|
625
|
+
if (message.data !== null && message.data !== undefined) {
|
|
626
|
+
console.info(label, message.data);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
console.info(label, message.raw);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private applyConversationHistory(payload: unknown) {
|
|
634
|
+
const entries = this.extractHistoryEntries(payload);
|
|
635
|
+
if (entries.length === 0) {
|
|
636
|
+
console.info('[RioAssist][history] payload sem itens para montar lista de conversas');
|
|
637
|
+
this.conversations = [];
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const map = new Map<string, ConversationItem>();
|
|
642
|
+
|
|
643
|
+
entries.forEach((entry, index) => {
|
|
644
|
+
if (!entry || typeof entry !== 'object') {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const normalized = this.normalizeConversationItem(
|
|
649
|
+
entry as Record<string, unknown>,
|
|
650
|
+
index,
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
if (!normalized) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const current = map.get(normalized.id);
|
|
658
|
+
if (!current) {
|
|
659
|
+
map.set(normalized.id, normalized);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const currentTime = Date.parse(current.updatedAt);
|
|
664
|
+
const nextTime = Date.parse(normalized.updatedAt);
|
|
665
|
+
|
|
666
|
+
if (Number.isFinite(nextTime) && nextTime > currentTime) {
|
|
667
|
+
map.set(normalized.id, normalized);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
const conversations = Array.from(map.values()).sort((a, b) => {
|
|
672
|
+
const order = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
|
|
673
|
+
return Number.isFinite(order) ? order : 0;
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
this.conversations = conversations;
|
|
677
|
+
console.info('[RioAssist][history] conversas normalizadas', conversations);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private extractHistoryEntries(payload: unknown): unknown[] {
|
|
681
|
+
if (Array.isArray(payload)) {
|
|
682
|
+
return payload;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (payload && typeof payload === 'object') {
|
|
686
|
+
const record = payload as Record<string, unknown>;
|
|
687
|
+
const candidates = [
|
|
688
|
+
record.history,
|
|
689
|
+
record.conversations,
|
|
690
|
+
record.data,
|
|
691
|
+
record.items,
|
|
692
|
+
record.messages,
|
|
693
|
+
];
|
|
694
|
+
|
|
695
|
+
for (const candidate of candidates) {
|
|
696
|
+
if (Array.isArray(candidate)) {
|
|
697
|
+
return candidate;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (record.data && typeof record.data === 'object' && !Array.isArray(record.data)) {
|
|
702
|
+
const nested = this.extractHistoryEntries(record.data);
|
|
703
|
+
if (nested.length > 0) {
|
|
704
|
+
return nested;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return [];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private normalizeConversationItem(
|
|
713
|
+
value: Record<string, unknown>,
|
|
714
|
+
index: number,
|
|
715
|
+
): ConversationItem | null {
|
|
716
|
+
const rawId =
|
|
717
|
+
value.id ??
|
|
718
|
+
value.conversationId ??
|
|
719
|
+
value.conversationUUID ??
|
|
720
|
+
value.conversationUuid ??
|
|
721
|
+
value.uuid;
|
|
722
|
+
|
|
723
|
+
const id = rawId !== undefined && rawId !== null ? String(rawId) : `history-${index + 1}`;
|
|
724
|
+
|
|
725
|
+
const rawTitle =
|
|
726
|
+
value.title ??
|
|
727
|
+
value.name ??
|
|
728
|
+
value.topic ??
|
|
729
|
+
value.subject ??
|
|
730
|
+
value.question ??
|
|
731
|
+
value.query ??
|
|
732
|
+
value.message;
|
|
733
|
+
|
|
734
|
+
const title =
|
|
735
|
+
typeof rawTitle === 'string' && rawTitle.trim().length > 0
|
|
736
|
+
? rawTitle.trim()
|
|
737
|
+
: `Conversa ${index + 1}`;
|
|
738
|
+
|
|
739
|
+
const rawUpdated =
|
|
740
|
+
value.updatedAt ??
|
|
741
|
+
value.updated_at ??
|
|
742
|
+
value.lastMessageAt ??
|
|
743
|
+
value.last_message_at ??
|
|
744
|
+
value.createdAt ??
|
|
745
|
+
value.created_at ??
|
|
746
|
+
value.timestamp ??
|
|
747
|
+
value.date;
|
|
748
|
+
|
|
749
|
+
const updatedAt = this.toIsoString(rawUpdated);
|
|
750
|
+
|
|
751
|
+
return { id, title, updatedAt };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private toIsoString(value: unknown) {
|
|
755
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
756
|
+
const date = new Date(value);
|
|
757
|
+
if (!Number.isNaN(date.getTime())) {
|
|
758
|
+
return date.toISOString();
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return new Date().toISOString();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private startLoadingGuard() {
|
|
766
|
+
this.clearLoadingGuard();
|
|
767
|
+
this.loadingTimer = window.setTimeout(() => {
|
|
594
768
|
this.loadingTimer = null;
|
|
595
769
|
this.isLoading = false;
|
|
596
770
|
}, 15000);
|
|
@@ -686,12 +860,7 @@ declare global {
|
|
|
686
860
|
'rio-assist-widget': RioAssistWidget;
|
|
687
861
|
}
|
|
688
862
|
}
|
|
689
|
-
|
|
690
|
-
if (!customElements.get('rio-assist-widget')) {
|
|
691
|
-
customElements.define('rio-assist-widget', RioAssistWidget);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
863
|
+
|
|
864
|
+
if (!customElements.get('rio-assist-widget')) {
|
|
865
|
+
customElements.define('rio-assist-widget', RioAssistWidget);
|
|
866
|
+
}
|
|
@@ -1,52 +1,69 @@
|
|
|
1
|
-
const WEBSOCKET_URL = 'wss://ws.volkswagen.latam-sandbox.rio.cloud';
|
|
2
|
-
const
|
|
1
|
+
const WEBSOCKET_URL = 'wss://ws.volkswagen.latam-sandbox.rio.cloud';
|
|
2
|
+
const HEARTBEAT_INTERVAL_MS = 5 * 60_000; // keep-alive before the 10min idle timeout
|
|
3
3
|
|
|
4
|
-
export type RioIncomingMessage = {
|
|
5
|
-
text: string;
|
|
6
|
-
raw: string;
|
|
7
|
-
data: unknown;
|
|
8
|
-
|
|
4
|
+
export type RioIncomingMessage = {
|
|
5
|
+
text: string;
|
|
6
|
+
raw: string;
|
|
7
|
+
data: unknown;
|
|
8
|
+
action?: string;
|
|
9
|
+
};
|
|
9
10
|
|
|
10
11
|
export class RioWebsocketClient {
|
|
11
12
|
readonly token: string;
|
|
12
13
|
|
|
13
14
|
private socket: WebSocket | null = null;
|
|
14
15
|
|
|
15
|
-
private connectPromise: Promise<void> | null = null;
|
|
16
|
-
|
|
17
|
-
private readonly listeners = new Set<(message: RioIncomingMessage) => void>();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
private connectPromise: Promise<void> | null = null;
|
|
17
|
+
|
|
18
|
+
private readonly listeners = new Set<(message: RioIncomingMessage) => void>();
|
|
19
|
+
|
|
20
|
+
private heartbeatId: number | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(token: string) {
|
|
23
|
+
this.token = token;
|
|
24
|
+
}
|
|
22
25
|
|
|
23
26
|
matchesToken(value: string) {
|
|
24
27
|
return this.token === value;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
async sendMessage(message: string) {
|
|
28
|
-
const socket = await this.ensureConnection();
|
|
29
|
-
|
|
30
|
-
const payload = {
|
|
31
|
-
action: 'sendMessage',
|
|
32
|
-
message,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
30
|
+
async sendMessage(message: string) {
|
|
31
|
+
const socket = await this.ensureConnection();
|
|
32
|
+
|
|
33
|
+
const payload = {
|
|
34
|
+
action: 'sendMessage',
|
|
35
|
+
message,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
socket.send(JSON.stringify(payload));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async requestHistory(options: { conversationId?: string; limit?: number } = {}) {
|
|
42
|
+
const socket = await this.ensureConnection();
|
|
43
|
+
const payload: Record<string, unknown> = {
|
|
44
|
+
action: 'getHistory',
|
|
45
|
+
limit: options.limit ?? 50,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (options.conversationId) {
|
|
49
|
+
payload.conversationId = options.conversationId;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
socket.send(JSON.stringify(payload));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onMessage(listener: (message: RioIncomingMessage) => void) {
|
|
56
|
+
this.listeners.add(listener);
|
|
57
|
+
return () => this.listeners.delete(listener);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
close() {
|
|
61
|
+
this.stopHeartbeat();
|
|
62
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
63
|
+
this.socket.close();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.connectPromise = null;
|
|
50
67
|
this.socket = null;
|
|
51
68
|
this.listeners.clear();
|
|
52
69
|
}
|
|
@@ -65,27 +82,32 @@ export class RioWebsocketClient {
|
|
|
65
82
|
`${WEBSOCKET_URL}?token=${encodeURIComponent(this.token)}`,
|
|
66
83
|
);
|
|
67
84
|
|
|
68
|
-
this.socket.addEventListener('message', (event) => this.handleMessage(event));
|
|
69
|
-
this.socket.addEventListener('close', () => {
|
|
70
|
-
this.connectPromise = null;
|
|
71
|
-
this.socket = null;
|
|
72
|
-
|
|
85
|
+
this.socket.addEventListener('message', (event) => this.handleMessage(event));
|
|
86
|
+
this.socket.addEventListener('close', () => {
|
|
87
|
+
this.connectPromise = null;
|
|
88
|
+
this.socket = null;
|
|
89
|
+
this.stopHeartbeat();
|
|
90
|
+
});
|
|
73
91
|
|
|
74
92
|
this.connectPromise = new Promise((resolve, reject) => {
|
|
75
93
|
if (!this.socket) {
|
|
76
94
|
reject(new Error('Falha ao criar conexão WebSocket.'));
|
|
77
95
|
return;
|
|
78
96
|
}
|
|
79
|
-
|
|
80
|
-
const handleOpen = () => {
|
|
81
|
-
cleanup();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
|
|
98
|
+
const handleOpen = () => {
|
|
99
|
+
cleanup();
|
|
100
|
+
if (this.socket) {
|
|
101
|
+
this.startHeartbeat(this.socket);
|
|
102
|
+
}
|
|
103
|
+
resolve();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleError = () => {
|
|
107
|
+
cleanup();
|
|
108
|
+
this.stopHeartbeat();
|
|
109
|
+
this.socket?.close();
|
|
110
|
+
this.socket = null;
|
|
89
111
|
this.connectPromise = null;
|
|
90
112
|
reject(
|
|
91
113
|
new Error(
|
|
@@ -109,33 +131,57 @@ export class RioWebsocketClient {
|
|
|
109
131
|
throw new Error('Conexão WebSocket do Rio Insight não está pronta.');
|
|
110
132
|
}
|
|
111
133
|
|
|
112
|
-
return this.socket;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
return this.socket;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private startHeartbeat(socket: WebSocket) {
|
|
138
|
+
this.stopHeartbeat();
|
|
139
|
+
this.heartbeatId = window.setInterval(() => {
|
|
140
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
141
|
+
socket.send(JSON.stringify({ action: 'ping' }));
|
|
142
|
+
}
|
|
143
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private stopHeartbeat() {
|
|
147
|
+
if (this.heartbeatId !== null) {
|
|
148
|
+
window.clearInterval(this.heartbeatId);
|
|
149
|
+
this.heartbeatId = null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async handleMessage(event: MessageEvent) {
|
|
154
|
+
const raw = await this.readMessage(event.data);
|
|
155
|
+
let parsed: unknown = null;
|
|
156
|
+
let text = raw;
|
|
157
|
+
let action: string | undefined;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(raw);
|
|
161
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
162
|
+
const maybeAction =
|
|
163
|
+
(parsed as any).action ?? (parsed as any).type ?? (parsed as any).event;
|
|
164
|
+
|
|
165
|
+
if (typeof maybeAction === 'string') {
|
|
166
|
+
action = maybeAction;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const maybeText =
|
|
170
|
+
(parsed as any).message ??
|
|
171
|
+
(parsed as any).response ??
|
|
172
|
+
(parsed as any).text ??
|
|
173
|
+
(parsed as any).content;
|
|
128
174
|
|
|
129
175
|
if (typeof maybeText === 'string') {
|
|
130
176
|
text = maybeText;
|
|
131
177
|
}
|
|
132
178
|
}
|
|
133
|
-
} catch {
|
|
134
|
-
parsed = null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
this.listeners.forEach((listener) => listener({ text, raw, data: parsed }));
|
|
138
|
-
}
|
|
179
|
+
} catch {
|
|
180
|
+
parsed = null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.listeners.forEach((listener) => listener({ text, raw, data: parsed, action }));
|
|
184
|
+
}
|
|
139
185
|
|
|
140
186
|
private async readMessage(
|
|
141
187
|
data: MessageEvent['data'],
|