rio-assist-widget 0.1.9 → 0.1.11
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 +94 -65
- package/package.json +1 -1
- package/src/components/conversations-panel/conversations-panel.styles.ts +22 -1
- package/src/components/conversations-panel/conversations-panel.template.ts +36 -21
- package/src/components/fullscreen/fullscreen.template.ts +3 -1
- package/src/components/rio-assist/rio-assist.ts +671 -122
- package/src/services/rioWebsocket.ts +119 -74
|
@@ -1,52 +1,68 @@
|
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
30
|
+
async sendMessage(message: string, conversationId?: string | null) {
|
|
31
|
+
const socket = await this.ensureConnection();
|
|
32
|
+
|
|
33
|
+
const payload = {
|
|
34
|
+
action: 'sendMessage',
|
|
35
|
+
message,
|
|
36
|
+
conversationId: conversationId ?? null,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
console.info('[RioAssist][ws] enviando payload de mensagem', payload);
|
|
40
|
+
socket.send(JSON.stringify(payload));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async requestHistory(options: { conversationId?: string | null; limit?: number } = {}) {
|
|
44
|
+
const socket = await this.ensureConnection();
|
|
45
|
+
const payload: Record<string, unknown> = {
|
|
46
|
+
action: 'getHistory',
|
|
47
|
+
limit: options.limit ?? 50,
|
|
48
|
+
conversationId: options.conversationId ?? null,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
socket.send(JSON.stringify(payload));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onMessage(listener: (message: RioIncomingMessage) => void) {
|
|
55
|
+
this.listeners.add(listener);
|
|
56
|
+
return () => this.listeners.delete(listener);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
close() {
|
|
60
|
+
this.stopHeartbeat();
|
|
61
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
62
|
+
this.socket.close();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.connectPromise = null;
|
|
50
66
|
this.socket = null;
|
|
51
67
|
this.listeners.clear();
|
|
52
68
|
}
|
|
@@ -65,27 +81,32 @@ export class RioWebsocketClient {
|
|
|
65
81
|
`${WEBSOCKET_URL}?token=${encodeURIComponent(this.token)}`,
|
|
66
82
|
);
|
|
67
83
|
|
|
68
|
-
this.socket.addEventListener('message', (event) => this.handleMessage(event));
|
|
69
|
-
this.socket.addEventListener('close', () => {
|
|
70
|
-
this.connectPromise = null;
|
|
71
|
-
this.socket = null;
|
|
72
|
-
|
|
84
|
+
this.socket.addEventListener('message', (event) => this.handleMessage(event));
|
|
85
|
+
this.socket.addEventListener('close', () => {
|
|
86
|
+
this.connectPromise = null;
|
|
87
|
+
this.socket = null;
|
|
88
|
+
this.stopHeartbeat();
|
|
89
|
+
});
|
|
73
90
|
|
|
74
91
|
this.connectPromise = new Promise((resolve, reject) => {
|
|
75
92
|
if (!this.socket) {
|
|
76
93
|
reject(new Error('Falha ao criar conexão WebSocket.'));
|
|
77
94
|
return;
|
|
78
95
|
}
|
|
79
|
-
|
|
80
|
-
const handleOpen = () => {
|
|
81
|
-
cleanup();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
|
|
97
|
+
const handleOpen = () => {
|
|
98
|
+
cleanup();
|
|
99
|
+
if (this.socket) {
|
|
100
|
+
this.startHeartbeat(this.socket);
|
|
101
|
+
}
|
|
102
|
+
resolve();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleError = () => {
|
|
106
|
+
cleanup();
|
|
107
|
+
this.stopHeartbeat();
|
|
108
|
+
this.socket?.close();
|
|
109
|
+
this.socket = null;
|
|
89
110
|
this.connectPromise = null;
|
|
90
111
|
reject(
|
|
91
112
|
new Error(
|
|
@@ -109,33 +130,57 @@ export class RioWebsocketClient {
|
|
|
109
130
|
throw new Error('Conexão WebSocket do Rio Insight não está pronta.');
|
|
110
131
|
}
|
|
111
132
|
|
|
112
|
-
return this.socket;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
return this.socket;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private startHeartbeat(socket: WebSocket) {
|
|
137
|
+
this.stopHeartbeat();
|
|
138
|
+
this.heartbeatId = window.setInterval(() => {
|
|
139
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
140
|
+
socket.send(JSON.stringify({ action: 'ping' }));
|
|
141
|
+
}
|
|
142
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private stopHeartbeat() {
|
|
146
|
+
if (this.heartbeatId !== null) {
|
|
147
|
+
window.clearInterval(this.heartbeatId);
|
|
148
|
+
this.heartbeatId = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async handleMessage(event: MessageEvent) {
|
|
153
|
+
const raw = await this.readMessage(event.data);
|
|
154
|
+
let parsed: unknown = null;
|
|
155
|
+
let text = raw;
|
|
156
|
+
let action: string | undefined;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(raw);
|
|
160
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
161
|
+
const maybeAction =
|
|
162
|
+
(parsed as any).action ?? (parsed as any).type ?? (parsed as any).event;
|
|
163
|
+
|
|
164
|
+
if (typeof maybeAction === 'string') {
|
|
165
|
+
action = maybeAction;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const maybeText =
|
|
169
|
+
(parsed as any).message ??
|
|
170
|
+
(parsed as any).response ??
|
|
171
|
+
(parsed as any).text ??
|
|
172
|
+
(parsed as any).content;
|
|
128
173
|
|
|
129
174
|
if (typeof maybeText === 'string') {
|
|
130
175
|
text = maybeText;
|
|
131
176
|
}
|
|
132
177
|
}
|
|
133
|
-
} catch {
|
|
134
|
-
parsed = null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
this.listeners.forEach((listener) => listener({ text, raw, data: parsed }));
|
|
138
|
-
}
|
|
178
|
+
} catch {
|
|
179
|
+
parsed = null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.listeners.forEach((listener) => listener({ text, raw, data: parsed, action }));
|
|
183
|
+
}
|
|
139
184
|
|
|
140
185
|
private async readMessage(
|
|
141
186
|
data: MessageEvent['data'],
|