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.
@@ -1,52 +1,68 @@
1
- const WEBSOCKET_URL = 'wss://ws.volkswagen.latam-sandbox.rio.cloud';
2
- const DEFAULT_AGENT_MODEL = 'eu.amazon.nova-pro-v1:0';
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
- constructor(token: string) {
20
- this.token = token;
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
- agentModel: DEFAULT_AGENT_MODEL,
34
- };
35
-
36
- socket.send(JSON.stringify(payload));
37
- }
38
-
39
- onMessage(listener: (message: RioIncomingMessage) => void) {
40
- this.listeners.add(listener);
41
- return () => this.listeners.delete(listener);
42
- }
43
-
44
- close() {
45
- if (this.socket && this.socket.readyState === WebSocket.OPEN) {
46
- this.socket.close();
47
- }
48
-
49
- this.connectPromise = null;
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
- resolve();
83
- };
84
-
85
- const handleError = () => {
86
- cleanup();
87
- this.socket?.close();
88
- this.socket = null;
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 async handleMessage(event: MessageEvent) {
116
- const raw = await this.readMessage(event.data);
117
- let parsed: unknown = null;
118
- let text = raw;
119
-
120
- try {
121
- parsed = JSON.parse(raw);
122
- if (typeof parsed === 'object' && parsed !== null) {
123
- const maybeText =
124
- (parsed as any).message ??
125
- (parsed as any).response ??
126
- (parsed as any).text ??
127
- (parsed as any).content;
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'],