rio-assist-widget 0.1.28 → 0.1.34
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 +62 -62
- package/dist/rio-assist.js +209 -132
- package/index.html +38 -38
- package/package.json +40 -40
- package/src/components/conversations-panel/conversations-panel.template.ts +30 -34
- package/src/components/floating-button/floating-button.styles.ts +7 -0
- package/src/components/floating-button/floating-button.template.ts +10 -2
- package/src/components/fullscreen/fullscreen.styles.ts +75 -65
- package/src/components/fullscreen/fullscreen.template.ts +34 -0
- package/src/components/mini-panel/mini-panel.styles.ts +52 -51
- package/src/components/mini-panel/mini-panel.template.ts +167 -189
- package/src/components/rio-assist/rio-assist.styles.ts +101 -92
- package/src/components/rio-assist/rio-assist.template.ts +106 -75
- package/src/components/rio-assist/rio-assist.ts +1676 -1520
- package/src/main.ts +15 -15
- package/src/playground.ts +31 -31
- package/src/services/rioWebsocket.ts +142 -142
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
-
import { widgetStyles } from './rio-assist.styles';
|
|
3
|
-
import { renderRioAssist } from './rio-assist.template';
|
|
4
|
-
import {
|
|
5
|
-
RioWebsocketClient,
|
|
6
|
-
type RioIncomingMessage,
|
|
7
|
-
} from '../../services/rioWebsocket';
|
|
8
|
-
import MarkdownIt from 'markdown-it';
|
|
9
|
-
import markdownItTaskLists from 'markdown-it-task-lists';
|
|
10
|
-
import DOMPurify from 'dompurify';
|
|
11
|
-
|
|
12
|
-
type ChatRole = 'user' | 'assistant';
|
|
13
|
-
|
|
14
|
-
export type ChatMessage = {
|
|
15
|
-
id: string;
|
|
16
|
-
role: ChatRole;
|
|
17
|
-
text: string;
|
|
18
|
-
html?: string;
|
|
19
|
-
timestamp: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
type ConversationItem = {
|
|
23
|
-
id: string;
|
|
24
|
-
title: string;
|
|
25
|
-
updatedAt: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type ConversationDeleteTarget = {
|
|
29
|
-
id: string;
|
|
30
|
-
title: string;
|
|
31
|
-
index: number;
|
|
32
|
-
};
|
|
33
|
-
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
+
import { widgetStyles } from './rio-assist.styles';
|
|
3
|
+
import { renderRioAssist } from './rio-assist.template';
|
|
4
|
+
import {
|
|
5
|
+
RioWebsocketClient,
|
|
6
|
+
type RioIncomingMessage,
|
|
7
|
+
} from '../../services/rioWebsocket';
|
|
8
|
+
import MarkdownIt from 'markdown-it';
|
|
9
|
+
import markdownItTaskLists from 'markdown-it-task-lists';
|
|
10
|
+
import DOMPurify from 'dompurify';
|
|
11
|
+
|
|
12
|
+
type ChatRole = 'user' | 'assistant';
|
|
13
|
+
|
|
14
|
+
export type ChatMessage = {
|
|
15
|
+
id: string;
|
|
16
|
+
role: ChatRole;
|
|
17
|
+
text: string;
|
|
18
|
+
html?: string;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ConversationItem = {
|
|
23
|
+
id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
updatedAt: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type ConversationDeleteTarget = {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
index: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
34
|
type ConversationRenameTarget = {
|
|
35
35
|
id: string;
|
|
36
36
|
title: string;
|
|
@@ -60,31 +60,31 @@ export type HeaderActionConfig = {
|
|
|
60
60
|
iconUrl: string;
|
|
61
61
|
ariaLabel?: string;
|
|
62
62
|
onClick?: () => void;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export class RioAssistWidget extends LitElement {
|
|
66
|
-
static styles = widgetStyles;
|
|
67
|
-
|
|
68
|
-
static properties = {
|
|
69
|
-
open: { type: Boolean, state: true },
|
|
70
|
-
message: { type: String, state: true },
|
|
71
|
-
titleText: { type: String, attribute: 'data-title' },
|
|
72
|
-
buttonLabel: { type: String, attribute: 'data-button-label' },
|
|
73
|
-
placeholder: { type: String, attribute: 'data-placeholder' },
|
|
74
|
-
accentColor: { type: String, attribute: 'data-accent-color' },
|
|
75
|
-
apiBaseUrl: { type: String, attribute: 'data-api-base-url' },
|
|
76
|
-
rioToken: { type: String, attribute: 'data-rio-token' },
|
|
77
|
-
suggestionsSource: { type: String, attribute: 'data-suggestions' },
|
|
78
|
-
messages: { state: true },
|
|
79
|
-
isLoading: { type: Boolean, state: true },
|
|
80
|
-
errorMessage: { type: String, state: true },
|
|
81
|
-
showConversations: { type: Boolean, state: true },
|
|
82
|
-
conversationSearch: { type: String, state: true },
|
|
83
|
-
conversationMenuId: { state: true },
|
|
84
|
-
conversationMenuPlacement: { state: true },
|
|
85
|
-
isFullscreen: { type: Boolean, state: true },
|
|
86
|
-
conversationScrollbar: { state: true },
|
|
87
|
-
showNewConversationShortcut: { type: Boolean, state: true },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export class RioAssistWidget extends LitElement {
|
|
66
|
+
static styles = widgetStyles;
|
|
67
|
+
|
|
68
|
+
static properties = {
|
|
69
|
+
open: { type: Boolean, state: true },
|
|
70
|
+
message: { type: String, state: true },
|
|
71
|
+
titleText: { type: String, attribute: 'data-title' },
|
|
72
|
+
buttonLabel: { type: String, attribute: 'data-button-label' },
|
|
73
|
+
placeholder: { type: String, attribute: 'data-placeholder' },
|
|
74
|
+
accentColor: { type: String, attribute: 'data-accent-color' },
|
|
75
|
+
apiBaseUrl: { type: String, attribute: 'data-api-base-url' },
|
|
76
|
+
rioToken: { type: String, attribute: 'data-rio-token' },
|
|
77
|
+
suggestionsSource: { type: String, attribute: 'data-suggestions' },
|
|
78
|
+
messages: { state: true },
|
|
79
|
+
isLoading: { type: Boolean, state: true },
|
|
80
|
+
errorMessage: { type: String, state: true },
|
|
81
|
+
showConversations: { type: Boolean, state: true },
|
|
82
|
+
conversationSearch: { type: String, state: true },
|
|
83
|
+
conversationMenuId: { state: true },
|
|
84
|
+
conversationMenuPlacement: { state: true },
|
|
85
|
+
isFullscreen: { type: Boolean, state: true },
|
|
86
|
+
conversationScrollbar: { state: true },
|
|
87
|
+
showNewConversationShortcut: { type: Boolean, state: true },
|
|
88
88
|
conversations: { state: true },
|
|
89
89
|
conversationHistoryLoading: { type: Boolean, state: true },
|
|
90
90
|
activeConversationTitle: { state: true },
|
|
@@ -92,78 +92,84 @@ export class RioAssistWidget extends LitElement {
|
|
|
92
92
|
deleteConversationTarget: { attribute: false },
|
|
93
93
|
renameConversationTarget: { attribute: false },
|
|
94
94
|
shortAnswerEnabled: { type: Boolean, state: true },
|
|
95
|
+
newConversationConfirmOpen: { type: Boolean, state: true },
|
|
95
96
|
conversationActionError: { attribute: false },
|
|
96
97
|
headerActions: { attribute: false },
|
|
97
98
|
homeUrl: { type: String, attribute: 'data-home-url' },
|
|
99
|
+
floatingButtonOffset: { type: Number, state: true },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
open = false;
|
|
103
|
+
|
|
104
|
+
message = '';
|
|
105
|
+
|
|
106
|
+
titleText = 'Rio Insight';
|
|
107
|
+
|
|
108
|
+
buttonLabel = 'Rio Insight';
|
|
109
|
+
|
|
110
|
+
placeholder = 'Pergunte alguma coisa';
|
|
111
|
+
|
|
112
|
+
accentColor = '#008B9A';
|
|
113
|
+
|
|
114
|
+
floatingButtonOffset = 32;
|
|
115
|
+
|
|
116
|
+
apiBaseUrl = '';
|
|
117
|
+
|
|
118
|
+
rioToken = '';
|
|
119
|
+
|
|
120
|
+
suggestionsSource = '';
|
|
121
|
+
|
|
122
|
+
private randomizedSuggestions: string[] = [];
|
|
123
|
+
|
|
124
|
+
messages: ChatMessage[] = [];
|
|
125
|
+
|
|
126
|
+
isLoading = false;
|
|
127
|
+
|
|
128
|
+
errorMessage = '';
|
|
129
|
+
|
|
130
|
+
get loadingLabel() {
|
|
131
|
+
return this.loadingLabelInternal;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
showConversations = false;
|
|
135
|
+
|
|
136
|
+
conversationSearch = '';
|
|
137
|
+
|
|
138
|
+
conversationMenuId: string | null = null;
|
|
139
|
+
|
|
140
|
+
conversationMenuPlacement: 'above' | 'below' = 'below';
|
|
141
|
+
|
|
142
|
+
isFullscreen = false;
|
|
143
|
+
|
|
144
|
+
showNewConversationShortcut = false;
|
|
145
|
+
|
|
146
|
+
conversationScrollbar = {
|
|
147
|
+
height: 0,
|
|
148
|
+
top: 0,
|
|
149
|
+
visible: false,
|
|
98
150
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
titleText = 'Rio Insight';
|
|
105
|
-
|
|
106
|
-
buttonLabel = 'Rio Insight';
|
|
107
|
-
|
|
108
|
-
placeholder = 'Pergunte alguma coisa';
|
|
109
|
-
|
|
110
|
-
accentColor = '#008B9A';
|
|
111
|
-
|
|
112
|
-
apiBaseUrl = '';
|
|
113
|
-
|
|
114
|
-
rioToken = '';
|
|
115
|
-
|
|
116
|
-
suggestionsSource = '';
|
|
117
|
-
|
|
118
|
-
private randomizedSuggestions: string[] = [];
|
|
119
|
-
|
|
120
|
-
messages: ChatMessage[] = [];
|
|
121
|
-
|
|
122
|
-
isLoading = false;
|
|
123
|
-
|
|
124
|
-
errorMessage = '';
|
|
125
|
-
|
|
126
|
-
get loadingLabel() {
|
|
127
|
-
return this.loadingLabelInternal;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
showConversations = false;
|
|
131
|
-
|
|
132
|
-
conversationSearch = '';
|
|
133
|
-
|
|
134
|
-
conversationMenuId: string | null = null;
|
|
135
|
-
|
|
136
|
-
conversationMenuPlacement: 'above' | 'below' = 'below';
|
|
137
|
-
|
|
138
|
-
isFullscreen = false;
|
|
139
|
-
|
|
140
|
-
showNewConversationShortcut = false;
|
|
141
|
-
|
|
142
|
-
conversationScrollbar = {
|
|
143
|
-
height: 0,
|
|
144
|
-
top: 0,
|
|
145
|
-
visible: false,
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
conversationHistoryLoading = false;
|
|
149
|
-
|
|
150
|
-
conversationHistoryError = '';
|
|
151
|
+
|
|
152
|
+
conversationHistoryLoading = false;
|
|
153
|
+
|
|
154
|
+
conversationHistoryError = '';
|
|
151
155
|
|
|
152
156
|
deleteConversationTarget: ConversationDeleteTarget | null = null;
|
|
153
157
|
|
|
154
158
|
renameConversationTarget: ConversationRenameTarget | null = null;
|
|
155
159
|
|
|
156
|
-
shortAnswerEnabled =
|
|
160
|
+
shortAnswerEnabled = true;
|
|
161
|
+
|
|
162
|
+
newConversationConfirmOpen = false;
|
|
157
163
|
|
|
158
164
|
conversationActionError: ConversationActionErrorState | null = null;
|
|
159
165
|
|
|
160
166
|
private loadingLabelInternal = 'Rio Insight está respondendo...';
|
|
161
|
-
private loadingTimerSlow: number | null = null;
|
|
162
|
-
private loadingTimerTimeout: number | null = null;
|
|
163
|
-
|
|
164
|
-
private refreshConversationsAfterResponse = false;
|
|
165
|
-
|
|
166
|
-
activeConversationTitle: string | null = null;
|
|
167
|
+
private loadingTimerSlow: number | null = null;
|
|
168
|
+
private loadingTimerTimeout: number | null = null;
|
|
169
|
+
|
|
170
|
+
private refreshConversationsAfterResponse = false;
|
|
171
|
+
|
|
172
|
+
activeConversationTitle: string | null = null;
|
|
167
173
|
|
|
168
174
|
headerActions: HeaderActionConfig[] = [];
|
|
169
175
|
|
|
@@ -171,219 +177,312 @@ export class RioAssistWidget extends LitElement {
|
|
|
171
177
|
|
|
172
178
|
private pendingConversationAction: ConversationActionAttempt | null = null;
|
|
173
179
|
|
|
174
|
-
private
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
private
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
private
|
|
236
|
-
|
|
237
|
-
private
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
private
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
private
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
) {
|
|
311
|
-
this.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (this.
|
|
347
|
-
this.
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
this.
|
|
352
|
-
this.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
this.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
180
|
+
private inferUserIdFromToken(): string | null {
|
|
181
|
+
const token = this.rioToken.trim();
|
|
182
|
+
if (!token || !token.includes('.')) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const [, payload] = token.split('.');
|
|
187
|
+
try {
|
|
188
|
+
const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
|
|
189
|
+
const candidate =
|
|
190
|
+
decoded?.userId ??
|
|
191
|
+
decoded?.user_id ??
|
|
192
|
+
decoded?.sub ??
|
|
193
|
+
decoded?.id ??
|
|
194
|
+
decoded?.email ??
|
|
195
|
+
decoded?.username;
|
|
196
|
+
|
|
197
|
+
if (candidate && typeof candidate === 'string') {
|
|
198
|
+
return candidate.replace(/[^a-zA-Z0-9_:-]/g, '');
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private repairConversationId(rawId: string): string {
|
|
208
|
+
if (!rawId || rawId.includes(':')) {
|
|
209
|
+
return rawId;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const uuidMatch = rawId.match(
|
|
213
|
+
/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (!uuidMatch || uuidMatch.index === undefined) {
|
|
217
|
+
return rawId;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const uuid = uuidMatch[0];
|
|
221
|
+
const prefix = rawId.slice(0, uuidMatch.index).replace(/[-:]?$/, '');
|
|
222
|
+
const suffix = rawId.slice(uuidMatch.index + uuid.length);
|
|
223
|
+
|
|
224
|
+
const prefixPart = prefix ? `${prefix}:` : '';
|
|
225
|
+
return `${prefixPart}${uuid}${suffix}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private randomId(length: number) {
|
|
229
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
230
|
+
let result = '';
|
|
231
|
+
for (let i = 0; i < length; i += 1) {
|
|
232
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private clamp(value: number, min: number, max: number) {
|
|
238
|
+
return Math.min(Math.max(value, min), max);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private conversationScrollbarRaf: number | null = null;
|
|
242
|
+
|
|
243
|
+
private rioClient: RioWebsocketClient | null = null;
|
|
244
|
+
|
|
245
|
+
private rioUnsubscribe: (() => void) | null = null;
|
|
246
|
+
|
|
247
|
+
private loadingTimer: number | null = null;
|
|
248
|
+
|
|
249
|
+
private currentConversationId: string | null = null;
|
|
250
|
+
|
|
251
|
+
private conversationCounter = 0;
|
|
252
|
+
|
|
253
|
+
private conversationUserId: string | null = null;
|
|
254
|
+
|
|
255
|
+
private conversationScrollbarDraggingId: number | null = null;
|
|
256
|
+
|
|
257
|
+
private conversationScrollbarDragState: {
|
|
258
|
+
startY: number;
|
|
259
|
+
startThumbTop: number;
|
|
260
|
+
trackHeight: number;
|
|
261
|
+
thumbHeight: number;
|
|
262
|
+
list: HTMLElement;
|
|
263
|
+
} | null = null;
|
|
264
|
+
|
|
265
|
+
private floatingButtonDragState: {
|
|
266
|
+
pointerId: number;
|
|
267
|
+
startY: number;
|
|
268
|
+
startOffset: number;
|
|
269
|
+
buttonHeight: number;
|
|
270
|
+
} | null = null;
|
|
271
|
+
|
|
272
|
+
private floatingButtonDragged = false;
|
|
273
|
+
|
|
274
|
+
private suppressFloatingButtonClick = false;
|
|
275
|
+
|
|
276
|
+
private markdownRenderer = new MarkdownIt({
|
|
277
|
+
html: false,
|
|
278
|
+
linkify: true,
|
|
279
|
+
breaks: true,
|
|
280
|
+
}).use(markdownItTaskLists);
|
|
281
|
+
|
|
282
|
+
conversations: ConversationItem[] = [];
|
|
283
|
+
|
|
284
|
+
get suggestions(): string[] {
|
|
285
|
+
return this.randomizedSuggestions;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private parseSuggestions(source: string): string[] {
|
|
289
|
+
if (!source) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return source
|
|
294
|
+
.split('|')
|
|
295
|
+
.map((item) => item.trim())
|
|
296
|
+
.filter(Boolean);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private pickRandomSuggestions(options: string[], count: number): string[] {
|
|
300
|
+
if (options.length <= count) {
|
|
301
|
+
return [...options];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const pool = [...options];
|
|
305
|
+
for (let index = pool.length - 1; index > 0; index -= 1) {
|
|
306
|
+
const swapIndex = Math.floor(Math.random() * (index + 1));
|
|
307
|
+
[pool[index], pool[swapIndex]] = [pool[swapIndex], pool[index]];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return pool.slice(0, count);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
protected willUpdate(changedProperties: PropertyValues): void {
|
|
314
|
+
super.willUpdate(changedProperties);
|
|
315
|
+
|
|
316
|
+
if (changedProperties.has('suggestionsSource')) {
|
|
317
|
+
this.randomizedSuggestions = this.pickRandomSuggestions(
|
|
318
|
+
this.parseSuggestions(this.suggestionsSource),
|
|
319
|
+
3,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
protected updated(changedProperties: PropertyValues): void {
|
|
325
|
+
super.updated(changedProperties);
|
|
326
|
+
this.style.setProperty('--accent-color', this.accentColor);
|
|
327
|
+
|
|
328
|
+
if (
|
|
329
|
+
changedProperties.has('isFullscreen') ||
|
|
330
|
+
changedProperties.has('showConversations') ||
|
|
331
|
+
changedProperties.has('conversations')
|
|
332
|
+
) {
|
|
333
|
+
this.enqueueConversationScrollbarMeasure();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (
|
|
337
|
+
changedProperties.has('messages') ||
|
|
338
|
+
(changedProperties.has('isLoading') && this.isLoading) ||
|
|
339
|
+
(changedProperties.has('open') && this.open) ||
|
|
340
|
+
(changedProperties.has('isFullscreen') && this.isFullscreen)
|
|
341
|
+
) {
|
|
342
|
+
this.scrollConversationToBottom();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
protected firstUpdated(): void {
|
|
347
|
+
this.enqueueConversationScrollbarMeasure();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
disconnectedCallback(): void {
|
|
351
|
+
super.disconnectedCallback();
|
|
352
|
+
if (this.conversationScrollbarRaf !== null) {
|
|
353
|
+
cancelAnimationFrame(this.conversationScrollbarRaf);
|
|
354
|
+
this.conversationScrollbarRaf = null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.teardownRioClient();
|
|
358
|
+
this.clearLoadingGuard();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
get filteredConversations() {
|
|
362
|
+
const query = this.conversationSearch.trim().toLowerCase();
|
|
363
|
+
if (!query) {
|
|
364
|
+
return this.conversations;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return this.conversations.filter((conversation) =>
|
|
368
|
+
conversation.title.toLowerCase().includes(query),
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
get hasActiveConversation() {
|
|
373
|
+
return this.messages.length > 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
handleFloatingButtonClick(event: Event) {
|
|
377
|
+
if (this.suppressFloatingButtonClick) {
|
|
378
|
+
event.preventDefault();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
this.togglePanel();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
handleFloatingButtonPointerDown(event: PointerEvent) {
|
|
386
|
+
const target = event.currentTarget as HTMLElement;
|
|
387
|
+
target.setPointerCapture(event.pointerId);
|
|
388
|
+
|
|
389
|
+
this.floatingButtonDragState = {
|
|
390
|
+
pointerId: event.pointerId,
|
|
391
|
+
startY: event.clientY,
|
|
392
|
+
startOffset: this.floatingButtonOffset,
|
|
393
|
+
buttonHeight: target.getBoundingClientRect().height,
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
this.floatingButtonDragged = false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
handleFloatingButtonPointerMove(event: PointerEvent) {
|
|
400
|
+
if (!this.floatingButtonDragState || this.floatingButtonDragState.pointerId !== event.pointerId) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const { startY, startOffset, buttonHeight } = this.floatingButtonDragState;
|
|
405
|
+
const deltaY = event.clientY - startY;
|
|
406
|
+
const viewportHeight = window.innerHeight || this.getBoundingClientRect().height || 0;
|
|
407
|
+
const margin = 12;
|
|
408
|
+
const maxBottom = Math.max(margin, viewportHeight - buttonHeight - margin);
|
|
409
|
+
|
|
410
|
+
this.floatingButtonOffset = this.clamp(startOffset - deltaY, margin, maxBottom);
|
|
411
|
+
this.floatingButtonDragged = this.floatingButtonDragged || Math.abs(deltaY) > 3;
|
|
412
|
+
event.preventDefault();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
handleFloatingButtonPointerUp(event: PointerEvent) {
|
|
416
|
+
this.finishFloatingButtonDrag(event);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
handleFloatingButtonPointerCancel(event: PointerEvent) {
|
|
420
|
+
this.finishFloatingButtonDrag(event);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private finishFloatingButtonDrag(event: PointerEvent) {
|
|
424
|
+
if (!this.floatingButtonDragState || this.floatingButtonDragState.pointerId !== event.pointerId) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const target = event.currentTarget as HTMLElement | null;
|
|
429
|
+
if (target && target.hasPointerCapture(event.pointerId)) {
|
|
430
|
+
target.releasePointerCapture(event.pointerId);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (this.floatingButtonDragged) {
|
|
434
|
+
this.suppressFloatingButtonClick = true;
|
|
435
|
+
window.setTimeout(() => {
|
|
436
|
+
this.suppressFloatingButtonClick = false;
|
|
437
|
+
}, 0);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
this.floatingButtonDragState = null;
|
|
441
|
+
this.floatingButtonDragged = false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
togglePanel() {
|
|
445
|
+
if (this.isFullscreen) {
|
|
446
|
+
this.exitFullscreen(false);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
this.open = !this.open;
|
|
451
|
+
this.dispatchEvent(
|
|
452
|
+
new CustomEvent(this.open ? 'rioassist:open' : 'rioassist:close', {
|
|
453
|
+
bubbles: true,
|
|
454
|
+
composed: true,
|
|
455
|
+
}),
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
closePanel() {
|
|
460
|
+
this.isFullscreen = false;
|
|
461
|
+
if (this.open) {
|
|
462
|
+
this.togglePanel();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
openConversationsPanel() {
|
|
467
|
+
this.showConversations = true;
|
|
468
|
+
this.requestConversationHistory();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
closeConversationsPanel() {
|
|
472
|
+
this.showConversations = false;
|
|
473
|
+
this.conversationMenuId = null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
toggleConversationsPanel() {
|
|
477
|
+
this.showConversations = !this.showConversations;
|
|
478
|
+
if (!this.showConversations) {
|
|
479
|
+
this.conversationMenuId = null;
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
this.requestConversationHistory();
|
|
484
|
+
}
|
|
485
|
+
|
|
387
486
|
toggleNewConversationShortcut() {
|
|
388
487
|
this.showNewConversationShortcut = !this.showNewConversationShortcut;
|
|
389
488
|
}
|
|
@@ -396,139 +495,139 @@ export class RioAssistWidget extends LitElement {
|
|
|
396
495
|
if (!conversationId) {
|
|
397
496
|
return;
|
|
398
497
|
}
|
|
399
|
-
|
|
400
|
-
this.showConversations = false;
|
|
401
|
-
this.conversationMenuId = null;
|
|
402
|
-
this.errorMessage = '';
|
|
403
|
-
this.currentConversationId = conversationId;
|
|
404
|
-
this.activeConversationTitle = this.lookupConversationTitle(conversationId);
|
|
405
|
-
|
|
406
|
-
console.info('[RioAssist][history] carregando conversa', conversationId);
|
|
407
|
-
this.requestConversationHistory(conversationId);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
handleConversationSearch(event: InputEvent) {
|
|
411
|
-
this.conversationSearch = (event.target as HTMLInputElement).value;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
handleConversationMenuToggle(event: Event, id: string) {
|
|
415
|
-
event.stopPropagation();
|
|
416
|
-
|
|
417
|
-
if (this.conversationMenuId === id) {
|
|
418
|
-
this.conversationMenuId = null;
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const button = event.currentTarget as HTMLElement;
|
|
423
|
-
const container = this.renderRoot.querySelector(
|
|
424
|
-
'.conversations-panel__surface',
|
|
425
|
-
) as HTMLElement | null;
|
|
426
|
-
|
|
427
|
-
if (button && container) {
|
|
428
|
-
const buttonRect = button.getBoundingClientRect();
|
|
429
|
-
const containerRect = container.getBoundingClientRect();
|
|
430
|
-
const spaceBelow = containerRect.bottom - buttonRect.bottom;
|
|
431
|
-
this.conversationMenuPlacement = spaceBelow < 140 ? 'above' : 'below';
|
|
432
|
-
} else {
|
|
433
|
-
this.conversationMenuPlacement = 'below';
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
this.conversationMenuId = id;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
handleConversationsPanelPointer(event: PointerEvent) {
|
|
440
|
-
const target = event.target as HTMLElement;
|
|
441
|
-
if (
|
|
442
|
-
!target.closest('.conversation-menu') &&
|
|
443
|
-
!target.closest('.conversation-menu-button')
|
|
444
|
-
) {
|
|
445
|
-
this.conversationMenuId = null;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
handleConversationAction(action: 'rename' | 'delete', id: string) {
|
|
450
|
-
this.conversationMenuId = null;
|
|
451
|
-
const conversationIndex = this.conversations.findIndex((item) => item.id === id);
|
|
452
|
-
if (conversationIndex === -1) {
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const conversation = this.conversations[conversationIndex];
|
|
457
|
-
if (action === 'delete') {
|
|
458
|
-
this.deleteConversationTarget = {
|
|
459
|
-
id: conversation.id,
|
|
460
|
-
title: conversation.title,
|
|
461
|
-
index: conversationIndex,
|
|
462
|
-
};
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
this.renameConversationTarget = {
|
|
467
|
-
id: conversation.id,
|
|
468
|
-
title: conversation.title,
|
|
469
|
-
index: conversationIndex,
|
|
470
|
-
draft: conversation.title,
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
handleHomeNavigation() {
|
|
475
|
-
const detail = { url: this.homeUrl || null };
|
|
476
|
-
const allowed = this.dispatchEvent(
|
|
477
|
-
new CustomEvent('rioassist:home', {
|
|
478
|
-
detail,
|
|
479
|
-
bubbles: true,
|
|
480
|
-
composed: true,
|
|
481
|
-
cancelable: true,
|
|
482
|
-
}),
|
|
483
|
-
);
|
|
484
|
-
|
|
485
|
-
if (!allowed) {
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if (this.homeUrl) {
|
|
490
|
-
window.location.assign(this.homeUrl);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
applyConversationRename(id: string, newTitle: string) {
|
|
495
|
-
if (!id || !newTitle) {
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
let changed = false;
|
|
500
|
-
this.conversations = this.conversations.map((conversation) => {
|
|
501
|
-
if (conversation.id === id) {
|
|
502
|
-
changed = true;
|
|
503
|
-
return { ...conversation, title: newTitle };
|
|
504
|
-
}
|
|
505
|
-
return conversation;
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
if (!changed) {
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (this.currentConversationId === id) {
|
|
513
|
-
this.activeConversationTitle = newTitle;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
498
|
+
|
|
499
|
+
this.showConversations = false;
|
|
500
|
+
this.conversationMenuId = null;
|
|
501
|
+
this.errorMessage = '';
|
|
502
|
+
this.currentConversationId = conversationId;
|
|
503
|
+
this.activeConversationTitle = this.lookupConversationTitle(conversationId);
|
|
504
|
+
|
|
505
|
+
console.info('[RioAssist][history] carregando conversa', conversationId);
|
|
506
|
+
this.requestConversationHistory(conversationId);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
handleConversationSearch(event: InputEvent) {
|
|
510
|
+
this.conversationSearch = (event.target as HTMLInputElement).value;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
handleConversationMenuToggle(event: Event, id: string) {
|
|
514
|
+
event.stopPropagation();
|
|
515
|
+
|
|
516
|
+
if (this.conversationMenuId === id) {
|
|
517
|
+
this.conversationMenuId = null;
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const button = event.currentTarget as HTMLElement;
|
|
522
|
+
const container = this.renderRoot.querySelector(
|
|
523
|
+
'.conversations-panel__surface',
|
|
524
|
+
) as HTMLElement | null;
|
|
525
|
+
|
|
526
|
+
if (button && container) {
|
|
527
|
+
const buttonRect = button.getBoundingClientRect();
|
|
528
|
+
const containerRect = container.getBoundingClientRect();
|
|
529
|
+
const spaceBelow = containerRect.bottom - buttonRect.bottom;
|
|
530
|
+
this.conversationMenuPlacement = spaceBelow < 140 ? 'above' : 'below';
|
|
531
|
+
} else {
|
|
532
|
+
this.conversationMenuPlacement = 'below';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
this.conversationMenuId = id;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
handleConversationsPanelPointer(event: PointerEvent) {
|
|
539
|
+
const target = event.target as HTMLElement;
|
|
540
|
+
if (
|
|
541
|
+
!target.closest('.conversation-menu') &&
|
|
542
|
+
!target.closest('.conversation-menu-button')
|
|
543
|
+
) {
|
|
544
|
+
this.conversationMenuId = null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
handleConversationAction(action: 'rename' | 'delete', id: string) {
|
|
549
|
+
this.conversationMenuId = null;
|
|
550
|
+
const conversationIndex = this.conversations.findIndex((item) => item.id === id);
|
|
551
|
+
if (conversationIndex === -1) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const conversation = this.conversations[conversationIndex];
|
|
556
|
+
if (action === 'delete') {
|
|
557
|
+
this.deleteConversationTarget = {
|
|
558
|
+
id: conversation.id,
|
|
559
|
+
title: conversation.title,
|
|
560
|
+
index: conversationIndex,
|
|
561
|
+
};
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
this.renameConversationTarget = {
|
|
566
|
+
id: conversation.id,
|
|
567
|
+
title: conversation.title,
|
|
568
|
+
index: conversationIndex,
|
|
569
|
+
draft: conversation.title,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
handleHomeNavigation() {
|
|
574
|
+
const detail = { url: this.homeUrl || null };
|
|
575
|
+
const allowed = this.dispatchEvent(
|
|
576
|
+
new CustomEvent('rioassist:home', {
|
|
577
|
+
detail,
|
|
578
|
+
bubbles: true,
|
|
579
|
+
composed: true,
|
|
580
|
+
cancelable: true,
|
|
581
|
+
}),
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
if (!allowed) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (this.homeUrl) {
|
|
589
|
+
window.location.assign(this.homeUrl);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
applyConversationRename(id: string, newTitle: string) {
|
|
594
|
+
if (!id || !newTitle) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
let changed = false;
|
|
599
|
+
this.conversations = this.conversations.map((conversation) => {
|
|
600
|
+
if (conversation.id === id) {
|
|
601
|
+
changed = true;
|
|
602
|
+
return { ...conversation, title: newTitle };
|
|
603
|
+
}
|
|
604
|
+
return conversation;
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
if (!changed) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (this.currentConversationId === id) {
|
|
612
|
+
this.activeConversationTitle = newTitle;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
517
616
|
applyConversationDeletion(id: string) {
|
|
518
617
|
if (!id) {
|
|
519
618
|
return;
|
|
520
619
|
}
|
|
521
620
|
|
|
522
|
-
const wasActive = this.currentConversationId === id;
|
|
523
|
-
const next = this.conversations.filter((conversation) => conversation.id !== id);
|
|
524
|
-
|
|
525
|
-
if (next.length === this.conversations.length) {
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
this.conversations = next;
|
|
530
|
-
|
|
531
|
-
if (wasActive) {
|
|
621
|
+
const wasActive = this.currentConversationId === id;
|
|
622
|
+
const next = this.conversations.filter((conversation) => conversation.id !== id);
|
|
623
|
+
|
|
624
|
+
if (next.length === this.conversations.length) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
this.conversations = next;
|
|
629
|
+
|
|
630
|
+
if (wasActive) {
|
|
532
631
|
this.currentConversationId = null;
|
|
533
632
|
this.activeConversationTitle = null;
|
|
534
633
|
this.messages = [];
|
|
@@ -550,7 +649,7 @@ export class RioAssistWidget extends LitElement {
|
|
|
550
649
|
next.splice(position, 0, snapshot);
|
|
551
650
|
this.conversations = next;
|
|
552
651
|
}
|
|
553
|
-
|
|
652
|
+
|
|
554
653
|
async confirmDeleteConversation() {
|
|
555
654
|
const target = this.deleteConversationTarget;
|
|
556
655
|
if (!target) {
|
|
@@ -587,28 +686,28 @@ export class RioAssistWidget extends LitElement {
|
|
|
587
686
|
|
|
588
687
|
this.pendingConversationAction = null;
|
|
589
688
|
}
|
|
590
|
-
|
|
591
|
-
cancelDeleteConversation() {
|
|
592
|
-
this.deleteConversationTarget = null;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
handleRenameDraft(event: InputEvent) {
|
|
596
|
-
if (!this.renameConversationTarget) {
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
this.renameConversationTarget = {
|
|
601
|
-
...this.renameConversationTarget,
|
|
602
|
-
draft: (event.target as HTMLInputElement).value,
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
async confirmRenameConversation() {
|
|
607
|
-
const target = this.renameConversationTarget;
|
|
608
|
-
if (!target) {
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
|
|
689
|
+
|
|
690
|
+
cancelDeleteConversation() {
|
|
691
|
+
this.deleteConversationTarget = null;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
handleRenameDraft(event: InputEvent) {
|
|
695
|
+
if (!this.renameConversationTarget) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
this.renameConversationTarget = {
|
|
700
|
+
...this.renameConversationTarget,
|
|
701
|
+
draft: (event.target as HTMLInputElement).value,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async confirmRenameConversation() {
|
|
706
|
+
const target = this.renameConversationTarget;
|
|
707
|
+
if (!target) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
612
711
|
const newTitle = target.draft.trim();
|
|
613
712
|
if (!newTitle) {
|
|
614
713
|
return;
|
|
@@ -635,7 +734,7 @@ export class RioAssistWidget extends LitElement {
|
|
|
635
734
|
|
|
636
735
|
this.pendingConversationAction = null;
|
|
637
736
|
}
|
|
638
|
-
|
|
737
|
+
|
|
639
738
|
cancelRenameConversation() {
|
|
640
739
|
this.renameConversationTarget = null;
|
|
641
740
|
}
|
|
@@ -696,100 +795,108 @@ export class RioAssistWidget extends LitElement {
|
|
|
696
795
|
conversation: Pick<ConversationItem, 'id' | 'title'>,
|
|
697
796
|
index: number,
|
|
698
797
|
newTitle?: string,
|
|
699
|
-
) {
|
|
700
|
-
const eventName =
|
|
701
|
-
action === 'rename' ? 'rioassist:conversation-rename' : 'rioassist:conversation-delete';
|
|
702
|
-
const detail = {
|
|
703
|
-
id: conversation.id,
|
|
704
|
-
title: conversation.title,
|
|
705
|
-
index,
|
|
706
|
-
action,
|
|
707
|
-
};
|
|
708
|
-
|
|
709
|
-
const allowed = this.dispatchEvent(
|
|
710
|
-
new CustomEvent(eventName, {
|
|
711
|
-
detail,
|
|
712
|
-
bubbles: true,
|
|
713
|
-
composed: true,
|
|
714
|
-
cancelable: true,
|
|
715
|
-
}),
|
|
716
|
-
);
|
|
717
|
-
|
|
718
|
-
if (!allowed) {
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
if (action === 'delete') {
|
|
723
|
-
const ok = await this.syncConversationDeleteBackend(conversation.id);
|
|
724
|
-
return ok;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
if (action === 'rename' && newTitle) {
|
|
728
|
-
const ok = await this.syncConversationRenameBackend(conversation.id, newTitle);
|
|
729
|
-
return ok;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
return false;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
private async syncConversationRenameBackend(conversationId: string, newTitle: string) {
|
|
736
|
-
try {
|
|
737
|
-
const client = this.ensureRioClient();
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
this.conversationHistoryError =
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
this.conversationHistoryError =
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
)
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
798
|
+
) {
|
|
799
|
+
const eventName =
|
|
800
|
+
action === 'rename' ? 'rioassist:conversation-rename' : 'rioassist:conversation-delete';
|
|
801
|
+
const detail = {
|
|
802
|
+
id: conversation.id,
|
|
803
|
+
title: conversation.title,
|
|
804
|
+
index,
|
|
805
|
+
action,
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
const allowed = this.dispatchEvent(
|
|
809
|
+
new CustomEvent(eventName, {
|
|
810
|
+
detail,
|
|
811
|
+
bubbles: true,
|
|
812
|
+
composed: true,
|
|
813
|
+
cancelable: true,
|
|
814
|
+
}),
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
if (!allowed) {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (action === 'delete') {
|
|
822
|
+
const ok = await this.syncConversationDeleteBackend(conversation.id);
|
|
823
|
+
return ok;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (action === 'rename' && newTitle) {
|
|
827
|
+
const ok = await this.syncConversationRenameBackend(conversation.id, newTitle);
|
|
828
|
+
return ok;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
private async syncConversationRenameBackend(conversationId: string, newTitle: string) {
|
|
835
|
+
try {
|
|
836
|
+
const client = this.ensureRioClient();
|
|
837
|
+
console.info('[RioAssist][ws] enviando renameConversation', {
|
|
838
|
+
conversationId,
|
|
839
|
+
newTitle,
|
|
840
|
+
});
|
|
841
|
+
await client.renameConversation(conversationId, newTitle);
|
|
842
|
+
this.applyConversationRename(conversationId, newTitle);
|
|
843
|
+
this.conversationHistoryError = '';
|
|
844
|
+
return true;
|
|
845
|
+
} catch (error) {
|
|
846
|
+
console.error('[RioAssist][history] erro ao renomear conversa', error);
|
|
847
|
+
this.conversationHistoryError =
|
|
848
|
+
error instanceof Error && error.message
|
|
849
|
+
? error.message
|
|
850
|
+
: 'Nao foi possivel renomear a conversa.';
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
private async syncConversationDeleteBackend(conversationId: string) {
|
|
856
|
+
try {
|
|
857
|
+
const client = this.ensureRioClient();
|
|
858
|
+
await client.deleteConversation(conversationId);
|
|
859
|
+
this.applyConversationDeletion(conversationId);
|
|
860
|
+
this.conversationHistoryError = '';
|
|
861
|
+
return true;
|
|
862
|
+
} catch (error) {
|
|
863
|
+
console.error('[RioAssist][history] erro ao excluir conversa', error);
|
|
864
|
+
this.conversationHistoryError =
|
|
865
|
+
error instanceof Error && error.message
|
|
866
|
+
? error.message
|
|
867
|
+
: 'Nao foi possivel excluir a conversa.';
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
private handleConversationSystemAction(message: RioIncomingMessage) {
|
|
873
|
+
const action = (message.action ?? '').toLowerCase();
|
|
874
|
+
if (action === 'conversationrenamed') {
|
|
875
|
+
const data = message.data as Record<string, unknown>;
|
|
876
|
+
const id = this.repairConversationId(
|
|
877
|
+
this.extractString(data, ['conversationId', 'id']) ?? '',
|
|
878
|
+
);
|
|
879
|
+
const newTitle = this.extractString(data, ['newTitle', 'title']);
|
|
880
|
+
if (id && newTitle) {
|
|
881
|
+
this.applyConversationRename(id, newTitle);
|
|
882
|
+
this.conversationHistoryError = '';
|
|
883
|
+
if (
|
|
884
|
+
this.pendingConversationAction &&
|
|
885
|
+
this.pendingConversationAction.conversationId === id &&
|
|
886
|
+
this.pendingConversationAction.action === 'rename'
|
|
887
|
+
) {
|
|
888
|
+
this.pendingConversationAction = null;
|
|
889
|
+
this.conversationActionError = null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return true;
|
|
788
893
|
}
|
|
789
894
|
|
|
790
895
|
if (action === 'conversationdeleted') {
|
|
791
|
-
const data = message.data as Record<string, unknown>;
|
|
792
|
-
const id = this.
|
|
896
|
+
const data = message.data as Record<string, unknown>;
|
|
897
|
+
const id = this.repairConversationId(
|
|
898
|
+
this.extractString(data, ['conversationId', 'id']) ?? '',
|
|
899
|
+
);
|
|
793
900
|
if (id) {
|
|
794
901
|
this.applyConversationDeletion(id);
|
|
795
902
|
this.conversationHistoryError = '';
|
|
@@ -804,7 +911,7 @@ export class RioAssistWidget extends LitElement {
|
|
|
804
911
|
}
|
|
805
912
|
return true;
|
|
806
913
|
}
|
|
807
|
-
|
|
914
|
+
|
|
808
915
|
if (action === 'processing') {
|
|
809
916
|
return true;
|
|
810
917
|
}
|
|
@@ -819,6 +926,11 @@ export class RioAssistWidget extends LitElement {
|
|
|
819
926
|
}
|
|
820
927
|
|
|
821
928
|
const data = message.data as Record<string, unknown>;
|
|
929
|
+
console.error('[RioAssist][ws] erro em acao de conversa recebido do backend', {
|
|
930
|
+
text: message.text,
|
|
931
|
+
data,
|
|
932
|
+
raw: message.raw,
|
|
933
|
+
});
|
|
822
934
|
const errorText =
|
|
823
935
|
this.extractString(data, ['error', 'message', 'detail', 'description']) ||
|
|
824
936
|
(typeof message.text === 'string' && message.text.trim()
|
|
@@ -861,273 +973,296 @@ export class RioAssistWidget extends LitElement {
|
|
|
861
973
|
return false;
|
|
862
974
|
}
|
|
863
975
|
const normalized = action.toLowerCase();
|
|
864
|
-
return (
|
|
865
|
-
normalized === 'processing' ||
|
|
866
|
-
normalized === 'conversationrenamed' ||
|
|
867
|
-
normalized === 'conversationdeleted'
|
|
868
|
-
);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
private extractString(
|
|
872
|
-
data: Record<string, unknown> | undefined,
|
|
873
|
-
keys: string[],
|
|
874
|
-
): string | null {
|
|
875
|
-
if (!data || typeof data !== 'object') {
|
|
876
|
-
return null;
|
|
877
|
-
}
|
|
878
|
-
for (const key of keys) {
|
|
879
|
-
const value = data[key];
|
|
880
|
-
if (typeof value === 'string' && value.trim()) {
|
|
881
|
-
return value;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
return null;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
handleHeaderActionClick(action: HeaderActionConfig, index: number) {
|
|
888
|
-
const detail = {
|
|
889
|
-
index,
|
|
890
|
-
id: action.id ?? null,
|
|
891
|
-
ariaLabel: action.ariaLabel ?? null,
|
|
892
|
-
iconUrl: action.iconUrl,
|
|
893
|
-
};
|
|
894
|
-
|
|
895
|
-
const allowed = this.dispatchEvent(
|
|
896
|
-
new CustomEvent('rioassist:header-action', {
|
|
897
|
-
detail,
|
|
898
|
-
bubbles: true,
|
|
899
|
-
composed: true,
|
|
900
|
-
cancelable: true,
|
|
901
|
-
}),
|
|
902
|
-
);
|
|
903
|
-
|
|
904
|
-
if (!allowed) {
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
if (typeof action.onClick === 'function') {
|
|
909
|
-
action.onClick();
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
handleCloseAction() {
|
|
914
|
-
if (this.isFullscreen) {
|
|
915
|
-
this.exitFullscreen(true);
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
if (this.showConversations) {
|
|
920
|
-
this.closeConversationsPanel();
|
|
921
|
-
} else {
|
|
922
|
-
this.closePanel();
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
enterFullscreen() {
|
|
927
|
-
if (this.isFullscreen) {
|
|
928
|
-
return;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
this.isFullscreen = true;
|
|
932
|
-
this.open = false;
|
|
933
|
-
this.showConversations = false;
|
|
934
|
-
this.requestConversationHistory();
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
exitFullscreen(restorePanel: boolean) {
|
|
938
|
-
if (!this.isFullscreen) {
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
this.isFullscreen = false;
|
|
943
|
-
this.conversationMenuId = null;
|
|
944
|
-
this.showNewConversationShortcut = false;
|
|
945
|
-
if (restorePanel) {
|
|
946
|
-
this.open = true;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
handleCreateConversation() {
|
|
951
|
-
if (!this.hasActiveConversation) {
|
|
952
|
-
return;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
this.
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
this.
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
this.
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
this.
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
visible
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
};
|
|
1129
|
-
}
|
|
1130
|
-
|
|
976
|
+
return (
|
|
977
|
+
normalized === 'processing' ||
|
|
978
|
+
normalized === 'conversationrenamed' ||
|
|
979
|
+
normalized === 'conversationdeleted'
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private extractString(
|
|
984
|
+
data: Record<string, unknown> | undefined,
|
|
985
|
+
keys: string[],
|
|
986
|
+
): string | null {
|
|
987
|
+
if (!data || typeof data !== 'object') {
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
for (const key of keys) {
|
|
991
|
+
const value = data[key];
|
|
992
|
+
if (typeof value === 'string' && value.trim()) {
|
|
993
|
+
return value;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
handleHeaderActionClick(action: HeaderActionConfig, index: number) {
|
|
1000
|
+
const detail = {
|
|
1001
|
+
index,
|
|
1002
|
+
id: action.id ?? null,
|
|
1003
|
+
ariaLabel: action.ariaLabel ?? null,
|
|
1004
|
+
iconUrl: action.iconUrl,
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
const allowed = this.dispatchEvent(
|
|
1008
|
+
new CustomEvent('rioassist:header-action', {
|
|
1009
|
+
detail,
|
|
1010
|
+
bubbles: true,
|
|
1011
|
+
composed: true,
|
|
1012
|
+
cancelable: true,
|
|
1013
|
+
}),
|
|
1014
|
+
);
|
|
1015
|
+
|
|
1016
|
+
if (!allowed) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (typeof action.onClick === 'function') {
|
|
1021
|
+
action.onClick();
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
handleCloseAction() {
|
|
1026
|
+
if (this.isFullscreen) {
|
|
1027
|
+
this.exitFullscreen(true);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (this.showConversations) {
|
|
1032
|
+
this.closeConversationsPanel();
|
|
1033
|
+
} else {
|
|
1034
|
+
this.closePanel();
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
enterFullscreen() {
|
|
1039
|
+
if (this.isFullscreen) {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
this.isFullscreen = true;
|
|
1044
|
+
this.open = false;
|
|
1045
|
+
this.showConversations = false;
|
|
1046
|
+
this.requestConversationHistory();
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
exitFullscreen(restorePanel: boolean) {
|
|
1050
|
+
if (!this.isFullscreen) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
this.isFullscreen = false;
|
|
1055
|
+
this.conversationMenuId = null;
|
|
1056
|
+
this.showNewConversationShortcut = false;
|
|
1057
|
+
if (restorePanel) {
|
|
1058
|
+
this.open = true;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
handleCreateConversation() {
|
|
1063
|
+
if (!this.hasActiveConversation) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
this.newConversationConfirmOpen = true;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
confirmCreateConversation() {
|
|
1071
|
+
if (!this.hasActiveConversation) {
|
|
1072
|
+
this.newConversationConfirmOpen = false;
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
this.newConversationConfirmOpen = false;
|
|
1077
|
+
|
|
1078
|
+
this.startNewConversation();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
cancelCreateConversation() {
|
|
1082
|
+
this.newConversationConfirmOpen = false;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
private startNewConversation() {
|
|
1086
|
+
if (!this.hasActiveConversation) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
this.clearLoadingGuard();
|
|
1091
|
+
this.isLoading = false;
|
|
1092
|
+
this.messages = [];
|
|
1093
|
+
this.message = '';
|
|
1094
|
+
this.errorMessage = '';
|
|
1095
|
+
this.showConversations = false;
|
|
1096
|
+
this.teardownRioClient();
|
|
1097
|
+
this.currentConversationId = null;
|
|
1098
|
+
this.activeConversationTitle = null;
|
|
1099
|
+
this.showNewConversationShortcut = false;
|
|
1100
|
+
this.dispatchEvent(
|
|
1101
|
+
new CustomEvent('rioassist:new-conversation', {
|
|
1102
|
+
bubbles: true,
|
|
1103
|
+
composed: true,
|
|
1104
|
+
}),
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
handleConversationListScroll(event: Event) {
|
|
1109
|
+
const target = event.currentTarget as HTMLElement | null;
|
|
1110
|
+
if (!target) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
this.updateConversationScrollbar(target);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
handleConversationScrollbarPointerDown(event: PointerEvent) {
|
|
1117
|
+
const track = event.currentTarget as HTMLElement | null;
|
|
1118
|
+
const list = this.renderRoot.querySelector(
|
|
1119
|
+
'.conversation-list--sidebar',
|
|
1120
|
+
) as HTMLElement | null;
|
|
1121
|
+
|
|
1122
|
+
if (!track || !list) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const trackRect = track.getBoundingClientRect();
|
|
1127
|
+
const thumbHeight = trackRect.height * (this.conversationScrollbar.height / 100);
|
|
1128
|
+
const maxThumbTop = Math.max(trackRect.height - thumbHeight, 0);
|
|
1129
|
+
const scrollRange = Math.max(list.scrollHeight - list.clientHeight, 1);
|
|
1130
|
+
const currentThumbTop = (list.scrollTop / scrollRange) * maxThumbTop;
|
|
1131
|
+
const offsetY = event.clientY - trackRect.top;
|
|
1132
|
+
const isOnThumb = offsetY >= currentThumbTop && offsetY <= currentThumbTop + thumbHeight;
|
|
1133
|
+
|
|
1134
|
+
const nextThumbTop = isOnThumb
|
|
1135
|
+
? currentThumbTop
|
|
1136
|
+
: Math.min(Math.max(offsetY - thumbHeight / 2, 0), maxThumbTop);
|
|
1137
|
+
|
|
1138
|
+
if (!isOnThumb) {
|
|
1139
|
+
list.scrollTop = (nextThumbTop / Math.max(maxThumbTop, 1)) * (list.scrollHeight - list.clientHeight);
|
|
1140
|
+
this.updateConversationScrollbar(list);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
track.setPointerCapture(event.pointerId);
|
|
1144
|
+
this.conversationScrollbarDraggingId = event.pointerId;
|
|
1145
|
+
this.conversationScrollbarDragState = {
|
|
1146
|
+
startY: event.clientY,
|
|
1147
|
+
startThumbTop: nextThumbTop,
|
|
1148
|
+
trackHeight: trackRect.height,
|
|
1149
|
+
thumbHeight,
|
|
1150
|
+
list,
|
|
1151
|
+
};
|
|
1152
|
+
event.preventDefault();
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
handleConversationScrollbarPointerMove(event: PointerEvent) {
|
|
1156
|
+
if (
|
|
1157
|
+
this.conversationScrollbarDraggingId === null ||
|
|
1158
|
+
this.conversationScrollbarDraggingId !== event.pointerId ||
|
|
1159
|
+
!this.conversationScrollbarDragState
|
|
1160
|
+
) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const {
|
|
1165
|
+
startY,
|
|
1166
|
+
startThumbTop,
|
|
1167
|
+
trackHeight,
|
|
1168
|
+
thumbHeight,
|
|
1169
|
+
list,
|
|
1170
|
+
} = this.conversationScrollbarDragState;
|
|
1171
|
+
|
|
1172
|
+
const maxThumbTop = Math.max(trackHeight - thumbHeight, 0);
|
|
1173
|
+
const deltaY = event.clientY - startY;
|
|
1174
|
+
const thumbTop = Math.min(Math.max(startThumbTop + deltaY, 0), maxThumbTop);
|
|
1175
|
+
const scrollRange = list.scrollHeight - list.clientHeight;
|
|
1176
|
+
|
|
1177
|
+
if (scrollRange > 0) {
|
|
1178
|
+
list.scrollTop = (thumbTop / Math.max(maxThumbTop, 1)) * scrollRange;
|
|
1179
|
+
this.updateConversationScrollbar(list);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
event.preventDefault();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
handleConversationScrollbarPointerUp(event: PointerEvent) {
|
|
1186
|
+
if (this.conversationScrollbarDraggingId !== event.pointerId) {
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const track = event.currentTarget as HTMLElement | null;
|
|
1191
|
+
track?.releasePointerCapture(event.pointerId);
|
|
1192
|
+
|
|
1193
|
+
this.conversationScrollbarDraggingId = null;
|
|
1194
|
+
this.conversationScrollbarDragState = null;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
private enqueueConversationScrollbarMeasure() {
|
|
1198
|
+
if (this.conversationScrollbarRaf !== null) {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
this.conversationScrollbarRaf = requestAnimationFrame(() => {
|
|
1203
|
+
this.conversationScrollbarRaf = null;
|
|
1204
|
+
this.updateConversationScrollbar();
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
private updateConversationScrollbar(target?: HTMLElement | null) {
|
|
1209
|
+
const element =
|
|
1210
|
+
target ??
|
|
1211
|
+
(this.renderRoot.querySelector(
|
|
1212
|
+
'.conversation-list--sidebar',
|
|
1213
|
+
) as HTMLElement | null);
|
|
1214
|
+
|
|
1215
|
+
if (!element) {
|
|
1216
|
+
if (this.conversationScrollbar.visible) {
|
|
1217
|
+
this.conversationScrollbar = { height: 0, top: 0, visible: false };
|
|
1218
|
+
}
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const { scrollHeight, clientHeight, scrollTop } = element;
|
|
1223
|
+
if (scrollHeight <= clientHeight + 1) {
|
|
1224
|
+
if (this.conversationScrollbar.visible) {
|
|
1225
|
+
this.conversationScrollbar = { height: 0, top: 0, visible: false };
|
|
1226
|
+
}
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const ratio = clientHeight / scrollHeight;
|
|
1231
|
+
const height = Math.max(ratio * 100, 8);
|
|
1232
|
+
const maxTop = 100 - height;
|
|
1233
|
+
const top =
|
|
1234
|
+
scrollTop / (scrollHeight - clientHeight) * (maxTop > 0 ? maxTop : 0);
|
|
1235
|
+
|
|
1236
|
+
this.conversationScrollbar = {
|
|
1237
|
+
height,
|
|
1238
|
+
top,
|
|
1239
|
+
visible: true,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
async onSuggestionClick(suggestion: string) {
|
|
1244
|
+
await this.processMessage(suggestion);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
async handleSubmit(event: SubmitEvent) {
|
|
1248
|
+
event.preventDefault();
|
|
1249
|
+
await this.processMessage(this.message);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
private createMessage(role: ChatRole, text: string): ChatMessage {
|
|
1253
|
+
const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
1254
|
+
? crypto.randomUUID()
|
|
1255
|
+
: `${Date.now()}-${Math.random()}`;
|
|
1256
|
+
|
|
1257
|
+
return {
|
|
1258
|
+
id,
|
|
1259
|
+
role,
|
|
1260
|
+
text,
|
|
1261
|
+
html: this.renderMarkdown(text),
|
|
1262
|
+
timestamp: Date.now(),
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1131
1266
|
private async processMessage(rawValue: string) {
|
|
1132
1267
|
const content = rawValue.trim();
|
|
1133
1268
|
if (!content || this.isLoading) {
|
|
@@ -1140,18 +1275,18 @@ export class RioAssistWidget extends LitElement {
|
|
|
1140
1275
|
const contentToDisplay = content;
|
|
1141
1276
|
|
|
1142
1277
|
if (!this.currentConversationId) {
|
|
1143
|
-
this.currentConversationId =
|
|
1278
|
+
this.currentConversationId = null;
|
|
1144
1279
|
this.activeConversationTitle = null;
|
|
1145
1280
|
}
|
|
1146
|
-
|
|
1147
|
-
const wasEmptyConversation = this.messages.length === 0;
|
|
1148
|
-
|
|
1149
|
-
this.dispatchEvent(
|
|
1150
|
-
new CustomEvent('rioassist:send', {
|
|
1151
|
-
detail: {
|
|
1152
|
-
message: content,
|
|
1153
|
-
apiBaseUrl: this.apiBaseUrl,
|
|
1154
|
-
token: this.rioToken,
|
|
1281
|
+
|
|
1282
|
+
const wasEmptyConversation = this.messages.length === 0;
|
|
1283
|
+
|
|
1284
|
+
this.dispatchEvent(
|
|
1285
|
+
new CustomEvent('rioassist:send', {
|
|
1286
|
+
detail: {
|
|
1287
|
+
message: content,
|
|
1288
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
1289
|
+
token: this.rioToken,
|
|
1155
1290
|
},
|
|
1156
1291
|
bubbles: true,
|
|
1157
1292
|
composed: true,
|
|
@@ -1177,36 +1312,36 @@ export class RioAssistWidget extends LitElement {
|
|
|
1177
1312
|
this.isLoading = false;
|
|
1178
1313
|
this.errorMessage = error instanceof Error
|
|
1179
1314
|
? error.message
|
|
1180
|
-
: 'Nao foi possivel enviar a mensagem para o agente.';
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
private ensureRioClient() {
|
|
1185
|
-
const token = this.rioToken.trim();
|
|
1186
|
-
if (!token) {
|
|
1187
|
-
throw new Error(
|
|
1188
|
-
'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
if (!this.rioClient || !this.rioClient.matchesToken(token)) {
|
|
1193
|
-
this.teardownRioClient();
|
|
1194
|
-
this.rioClient = new RioWebsocketClient(token);
|
|
1195
|
-
this.rioUnsubscribe = this.rioClient.onMessage((incoming) => {
|
|
1196
|
-
this.handleIncomingMessage(incoming);
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
return this.rioClient;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
private handleIncomingMessage(message: RioIncomingMessage) {
|
|
1204
|
-
if (this.isHistoryPayload(message)) {
|
|
1205
|
-
this.logHistoryPayload(message);
|
|
1206
|
-
this.handleHistoryPayload(message.data);
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1315
|
+
: 'Nao foi possivel enviar a mensagem para o agente.';
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
private ensureRioClient() {
|
|
1320
|
+
const token = this.rioToken.trim();
|
|
1321
|
+
if (!token) {
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
if (!this.rioClient || !this.rioClient.matchesToken(token)) {
|
|
1328
|
+
this.teardownRioClient();
|
|
1329
|
+
this.rioClient = new RioWebsocketClient(token);
|
|
1330
|
+
this.rioUnsubscribe = this.rioClient.onMessage((incoming) => {
|
|
1331
|
+
this.handleIncomingMessage(incoming);
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return this.rioClient;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
private handleIncomingMessage(message: RioIncomingMessage) {
|
|
1339
|
+
if (this.isHistoryPayload(message)) {
|
|
1340
|
+
this.logHistoryPayload(message);
|
|
1341
|
+
this.handleHistoryPayload(message.data);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1210
1345
|
if (this.handleConversationSystemAction(message)) {
|
|
1211
1346
|
return;
|
|
1212
1347
|
}
|
|
@@ -1218,642 +1353,663 @@ export class RioAssistWidget extends LitElement {
|
|
|
1218
1353
|
if (this.shouldIgnoreAssistantPayload(message.action)) {
|
|
1219
1354
|
return;
|
|
1220
1355
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
this.
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
this.
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
if (
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
const candidates
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
value.
|
|
1537
|
-
value.
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
value.
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
const
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
value.
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
const
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
if (
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
this.
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
this.
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
this.
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
'
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
'
|
|
1814
|
-
'
|
|
1815
|
-
'
|
|
1816
|
-
'
|
|
1817
|
-
'
|
|
1818
|
-
'
|
|
1819
|
-
'
|
|
1820
|
-
'
|
|
1821
|
-
'
|
|
1822
|
-
'
|
|
1823
|
-
'
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1356
|
+
|
|
1357
|
+
const incomingConversationId = this.extractConversationId(message.data);
|
|
1358
|
+
if (incomingConversationId) {
|
|
1359
|
+
this.currentConversationId = incomingConversationId;
|
|
1360
|
+
this.syncActiveConversationTitle();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
console.info('[RioAssist][ws] resposta de mensagem recebida', {
|
|
1364
|
+
action: message.action ?? 'message',
|
|
1365
|
+
text: message.text,
|
|
1366
|
+
raw: message.raw,
|
|
1367
|
+
data: message.data,
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
// Handle "processing" type messages - just keep loading state, don't create message
|
|
1371
|
+
if (message.action === 'processing') {
|
|
1372
|
+
console.info('[RioAssist][ws] processando mensagem - aguardando resposta final');
|
|
1373
|
+
// Keep isLoading = true, don't create a message balloon
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const assistantMessage = this.createMessage('assistant', message.text);
|
|
1378
|
+
this.messages = [...this.messages, assistantMessage];
|
|
1379
|
+
this.clearLoadingGuard();
|
|
1380
|
+
this.isLoading = false;
|
|
1381
|
+
|
|
1382
|
+
if (this.refreshConversationsAfterResponse) {
|
|
1383
|
+
this.refreshConversationsAfterResponse = false;
|
|
1384
|
+
this.requestConversationHistory();
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
private teardownRioClient() {
|
|
1389
|
+
if (this.rioUnsubscribe) {
|
|
1390
|
+
this.rioUnsubscribe();
|
|
1391
|
+
this.rioUnsubscribe = null;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
if (this.rioClient) {
|
|
1395
|
+
this.rioClient.close();
|
|
1396
|
+
this.rioClient = null;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
async requestConversationHistory(conversationId?: string) {
|
|
1401
|
+
try {
|
|
1402
|
+
const client = this.ensureRioClient();
|
|
1403
|
+
const limit = 50;
|
|
1404
|
+
|
|
1405
|
+
console.info('[RioAssist][history] solicitando historico de conversas', {
|
|
1406
|
+
conversationId: conversationId ?? null,
|
|
1407
|
+
limit,
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
this.conversationHistoryError = '';
|
|
1411
|
+
this.conversationHistoryLoading = true;
|
|
1412
|
+
await client.requestHistory({ conversationId, limit });
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
console.error('[RioAssist][history] erro ao solicitar historico', error);
|
|
1415
|
+
this.conversationHistoryError =
|
|
1416
|
+
error instanceof Error && error.message
|
|
1417
|
+
? error.message
|
|
1418
|
+
: 'Nao foi possivel carregar as conversas.';
|
|
1419
|
+
this.conversationHistoryLoading = false;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
private handleHistoryPayload(payload: unknown) {
|
|
1424
|
+
const entries = this.extractHistoryEntries(payload);
|
|
1425
|
+
const conversationId = this.extractConversationId(payload);
|
|
1426
|
+
|
|
1427
|
+
if (conversationId !== null && conversationId !== undefined) {
|
|
1428
|
+
this.applyMessageHistory(entries, conversationId);
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (this.isMessageHistoryEntries(entries)) {
|
|
1433
|
+
this.applyMessageHistory(entries);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
this.applyConversationHistoryFromEntries(entries);
|
|
1438
|
+
|
|
1439
|
+
if (this.refreshConversationsAfterResponse) {
|
|
1440
|
+
this.refreshConversationsAfterResponse = false;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
private isHistoryPayload(message: RioIncomingMessage) {
|
|
1445
|
+
if (
|
|
1446
|
+
typeof message.action === 'string' &&
|
|
1447
|
+
message.action.toLowerCase().includes('history')
|
|
1448
|
+
) {
|
|
1449
|
+
return true;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const data = message.data;
|
|
1453
|
+
if (data && typeof data === 'object') {
|
|
1454
|
+
const action = (data as any).action;
|
|
1455
|
+
if (typeof action === 'string' && action.toLowerCase().includes('history')) {
|
|
1456
|
+
return true;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (Array.isArray((data as any).history) || Array.isArray((data as any).conversations)) {
|
|
1460
|
+
return true;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
return false;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
private logHistoryPayload(message: RioIncomingMessage) {
|
|
1468
|
+
const label = '[RioAssist][history] payload recebido do websocket';
|
|
1469
|
+
if (message.data !== null && message.data !== undefined) {
|
|
1470
|
+
console.info(label, message.data);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
console.info(label, message.raw);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
private applyConversationHistoryFromEntries(entries: unknown[]) {
|
|
1478
|
+
if (entries.length === 0) {
|
|
1479
|
+
console.info('[RioAssist][history] payload sem itens para montar lista de conversas');
|
|
1480
|
+
this.conversations = [];
|
|
1481
|
+
this.conversationHistoryLoading = false;
|
|
1482
|
+
this.conversationHistoryError = '';
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const map = new Map<string, ConversationItem>();
|
|
1487
|
+
|
|
1488
|
+
entries.forEach((entry, index) => {
|
|
1489
|
+
if (!entry || typeof entry !== 'object') {
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const normalized = this.normalizeConversationItem(
|
|
1494
|
+
entry as Record<string, unknown>,
|
|
1495
|
+
index,
|
|
1496
|
+
);
|
|
1497
|
+
|
|
1498
|
+
const rawId =
|
|
1499
|
+
(entry as Record<string, unknown>).conversationId ??
|
|
1500
|
+
(entry as Record<string, unknown>).conversationUUID ??
|
|
1501
|
+
(entry as Record<string, unknown>).conversationUuid ??
|
|
1502
|
+
(entry as Record<string, unknown>).uuid ??
|
|
1503
|
+
(entry as Record<string, unknown>).id;
|
|
1504
|
+
if (rawId) {
|
|
1505
|
+
console.info('[RioAssist][history] conversa recebida do backend', {
|
|
1506
|
+
rawId,
|
|
1507
|
+
normalizedId: normalized?.id ?? null,
|
|
1508
|
+
entry,
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
if (!normalized) {
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const current = map.get(normalized.id);
|
|
1517
|
+
if (!current) {
|
|
1518
|
+
map.set(normalized.id, normalized);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const currentTime = Date.parse(current.updatedAt);
|
|
1523
|
+
const nextTime = Date.parse(normalized.updatedAt);
|
|
1524
|
+
|
|
1525
|
+
if (Number.isFinite(nextTime) && nextTime > currentTime) {
|
|
1526
|
+
map.set(normalized.id, normalized);
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
const conversations = Array.from(map.values()).sort((a, b) => {
|
|
1531
|
+
const order = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
|
|
1532
|
+
return Number.isFinite(order) ? order : 0;
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
this.conversations = conversations;
|
|
1536
|
+
this.conversationHistoryLoading = false;
|
|
1537
|
+
this.conversationHistoryError = '';
|
|
1538
|
+
this.syncActiveConversationTitle();
|
|
1539
|
+
console.info('[RioAssist][history] conversas normalizadas', conversations);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
private applyMessageHistory(entries: unknown[], conversationId?: string | null) {
|
|
1543
|
+
if (entries.length === 0) {
|
|
1544
|
+
console.info('[RioAssist][history] lista de mensagens vazia', { conversationId });
|
|
1545
|
+
this.messages = [];
|
|
1546
|
+
this.showConversations = false;
|
|
1547
|
+
this.clearLoadingGuard();
|
|
1548
|
+
this.isLoading = false;
|
|
1549
|
+
this.conversationHistoryLoading = false;
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
const normalized = entries.flatMap((entry, index) =>
|
|
1554
|
+
this.normalizeHistoryMessages(entry as Record<string, unknown>, index),
|
|
1555
|
+
);
|
|
1556
|
+
|
|
1557
|
+
if (conversationId) {
|
|
1558
|
+
this.currentConversationId = conversationId;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
this.messages = normalized;
|
|
1562
|
+
this.showConversations = false;
|
|
1563
|
+
this.clearLoadingGuard();
|
|
1564
|
+
this.isLoading = false;
|
|
1565
|
+
this.showNewConversationShortcut = normalized.length > 0;
|
|
1566
|
+
this.conversationHistoryLoading = false;
|
|
1567
|
+
this.refreshConversationsAfterResponse = false;
|
|
1568
|
+
|
|
1569
|
+
console.info('[RioAssist][history] mensagens carregadas', {
|
|
1570
|
+
conversationId: conversationId ?? null,
|
|
1571
|
+
total: normalized.length,
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
private extractHistoryEntries(payload: unknown): unknown[] {
|
|
1576
|
+
if (Array.isArray(payload)) {
|
|
1577
|
+
return payload;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
if (payload && typeof payload === 'object') {
|
|
1581
|
+
const record = payload as Record<string, unknown>;
|
|
1582
|
+
const candidates = [
|
|
1583
|
+
record.history,
|
|
1584
|
+
record.conversations,
|
|
1585
|
+
record.data,
|
|
1586
|
+
record.items,
|
|
1587
|
+
record.messages,
|
|
1588
|
+
];
|
|
1589
|
+
|
|
1590
|
+
for (const candidate of candidates) {
|
|
1591
|
+
if (Array.isArray(candidate)) {
|
|
1592
|
+
return candidate;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
if (record.data && typeof record.data === 'object' && !Array.isArray(record.data)) {
|
|
1597
|
+
const nested = this.extractHistoryEntries(record.data);
|
|
1598
|
+
if (nested.length > 0) {
|
|
1599
|
+
return nested;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
return [];
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
private extractConversationId(payload: unknown): string | null | undefined {
|
|
1608
|
+
if (payload && typeof payload === 'object') {
|
|
1609
|
+
const record = payload as Record<string, unknown>;
|
|
1610
|
+
const candidates = [
|
|
1611
|
+
record.conversationId,
|
|
1612
|
+
record.conversationUUID,
|
|
1613
|
+
record.conversationUuid,
|
|
1614
|
+
record.uuid,
|
|
1615
|
+
record.id,
|
|
1616
|
+
];
|
|
1617
|
+
|
|
1618
|
+
for (const candidate of candidates) {
|
|
1619
|
+
if (candidate === null) {
|
|
1620
|
+
return null;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
if (candidate !== undefined) {
|
|
1624
|
+
return this.repairConversationId(String(candidate));
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
return undefined;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
private isMessageHistoryEntries(entries: unknown[]) {
|
|
1633
|
+
return entries.some((entry) => this.looksLikeMessageHistoryEntry(entry));
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
private looksLikeMessageHistoryEntry(entry: unknown) {
|
|
1637
|
+
if (!entry || typeof entry !== 'object') {
|
|
1638
|
+
return false;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
const item = entry as Record<string, unknown>;
|
|
1642
|
+
const role = item.role ?? item.sender ?? item.from ?? item.author ?? item.type;
|
|
1643
|
+
if (typeof role === 'string' && role.trim().length > 0) {
|
|
1644
|
+
return true;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
if (
|
|
1648
|
+
typeof item.content === 'string' ||
|
|
1649
|
+
typeof item.message === 'string' ||
|
|
1650
|
+
typeof item.text === 'string' ||
|
|
1651
|
+
typeof item.response === 'string'
|
|
1652
|
+
) {
|
|
1653
|
+
return true;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (Array.isArray(item.parts) && item.parts.length > 0) {
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
return false;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
private normalizeConversationItem(
|
|
1664
|
+
value: Record<string, unknown>,
|
|
1665
|
+
index: number,
|
|
1666
|
+
): ConversationItem | null {
|
|
1667
|
+
const rawId =
|
|
1668
|
+
value.conversationId ??
|
|
1669
|
+
value.conversationUUID ??
|
|
1670
|
+
value.conversationUuid ??
|
|
1671
|
+
value.uuid ??
|
|
1672
|
+
value.id;
|
|
1673
|
+
|
|
1674
|
+
const idRaw = rawId !== undefined && rawId !== null ? String(rawId) : `history-${index + 1}`;
|
|
1675
|
+
const id = this.repairConversationId(idRaw);
|
|
1676
|
+
|
|
1677
|
+
const rawTitle =
|
|
1678
|
+
value.title ??
|
|
1679
|
+
value.name ??
|
|
1680
|
+
value.topic ??
|
|
1681
|
+
value.subject ??
|
|
1682
|
+
value.question ??
|
|
1683
|
+
value.query ??
|
|
1684
|
+
value.message;
|
|
1685
|
+
|
|
1686
|
+
const title =
|
|
1687
|
+
typeof rawTitle === 'string' && rawTitle.trim().length > 0
|
|
1688
|
+
? rawTitle.trim()
|
|
1689
|
+
: `Conversa ${index + 1}`;
|
|
1690
|
+
|
|
1691
|
+
const rawUpdated =
|
|
1692
|
+
value.updatedAt ??
|
|
1693
|
+
value.updated_at ??
|
|
1694
|
+
value.lastMessageAt ??
|
|
1695
|
+
value.last_message_at ??
|
|
1696
|
+
value.createdAt ??
|
|
1697
|
+
value.created_at ??
|
|
1698
|
+
value.timestamp ??
|
|
1699
|
+
value.date;
|
|
1700
|
+
|
|
1701
|
+
const updatedAt = this.toIsoString(rawUpdated);
|
|
1702
|
+
|
|
1703
|
+
return { id, title, updatedAt };
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
private normalizeHistoryMessages(
|
|
1707
|
+
value: Record<string, unknown>,
|
|
1708
|
+
index: number,
|
|
1709
|
+
): ChatMessage[] {
|
|
1710
|
+
const messages: ChatMessage[] = [];
|
|
1711
|
+
|
|
1712
|
+
const rawUserText = value.message ?? value.question ?? value.query ?? value.text ?? value.content;
|
|
1713
|
+
const userText = typeof rawUserText === 'string' ? rawUserText.trim() : '';
|
|
1714
|
+
|
|
1715
|
+
const rawResponseText =
|
|
1716
|
+
value.response ?? value.answer ?? value.reply ?? value.completion ?? value.body ?? value.preview;
|
|
1717
|
+
const responseText = typeof rawResponseText === 'string' ? rawResponseText.trim() : '';
|
|
1718
|
+
|
|
1719
|
+
const rawId = value.id ?? value.messageId ?? value.uuid ?? value.conversationMessageId;
|
|
1720
|
+
const baseId = rawId !== undefined && rawId !== null
|
|
1721
|
+
? String(rawId)
|
|
1722
|
+
: `history-${index + 1}`;
|
|
1723
|
+
|
|
1724
|
+
const userTimestampValue =
|
|
1725
|
+
value.timestamp ??
|
|
1726
|
+
value.createdAt ??
|
|
1727
|
+
value.created_at ??
|
|
1728
|
+
value.date ??
|
|
1729
|
+
value.time;
|
|
1730
|
+
const assistantTimestampValue =
|
|
1731
|
+
value.responseTimestamp ??
|
|
1732
|
+
value.responseTime ??
|
|
1733
|
+
value.responseDate ??
|
|
1734
|
+
value.response_at ??
|
|
1735
|
+
value.updatedAt ??
|
|
1736
|
+
value.updated_at;
|
|
1737
|
+
|
|
1738
|
+
const userTimestamp = this.parseTimestamp(userTimestampValue);
|
|
1739
|
+
const assistantTimestamp = this.parseTimestamp(
|
|
1740
|
+
assistantTimestampValue,
|
|
1741
|
+
userTimestamp + 1,
|
|
1742
|
+
);
|
|
1743
|
+
|
|
1744
|
+
if (responseText) {
|
|
1745
|
+
if (userText) {
|
|
1746
|
+
messages.push({
|
|
1747
|
+
id: `${baseId}-user`,
|
|
1748
|
+
role: 'user',
|
|
1749
|
+
text: userText,
|
|
1750
|
+
html: this.renderMarkdown(userText),
|
|
1751
|
+
timestamp: userTimestamp,
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
messages.push({
|
|
1756
|
+
id: `${baseId}-assistant`,
|
|
1757
|
+
role: 'assistant',
|
|
1758
|
+
text: responseText,
|
|
1759
|
+
html: this.renderMarkdown(responseText),
|
|
1760
|
+
timestamp: assistantTimestamp,
|
|
1761
|
+
});
|
|
1762
|
+
} else if (userText) {
|
|
1763
|
+
// Se n�o tiver resposta, n�o exibimos a mensagem do usuario isolada.
|
|
1764
|
+
return [];
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (messages.length > 0) {
|
|
1768
|
+
return messages;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const fallback = this.normalizeSingleHistoryMessage(value, index);
|
|
1772
|
+
return fallback ? [fallback] : [];
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
private normalizeSingleHistoryMessage(
|
|
1776
|
+
value: Record<string, unknown>,
|
|
1777
|
+
index: number,
|
|
1778
|
+
): ChatMessage | null {
|
|
1779
|
+
const rawText =
|
|
1780
|
+
value.text ??
|
|
1781
|
+
value.message ??
|
|
1782
|
+
value.content ??
|
|
1783
|
+
value.response ??
|
|
1784
|
+
value.body ??
|
|
1785
|
+
value.preview;
|
|
1786
|
+
|
|
1787
|
+
const text = typeof rawText === 'string' && rawText.trim().length > 0
|
|
1788
|
+
? rawText
|
|
1789
|
+
: '';
|
|
1790
|
+
|
|
1791
|
+
if (!text) {
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
const role = this.normalizeRole(
|
|
1796
|
+
value.role ??
|
|
1797
|
+
value.sender ??
|
|
1798
|
+
value.from ??
|
|
1799
|
+
value.author ??
|
|
1800
|
+
value.type ??
|
|
1801
|
+
value.direction,
|
|
1802
|
+
);
|
|
1803
|
+
|
|
1804
|
+
const rawId = value.id ?? value.messageId ?? value.uuid ?? value.conversationMessageId;
|
|
1805
|
+
const id = rawId !== undefined && rawId !== null
|
|
1806
|
+
? String(rawId)
|
|
1807
|
+
: `history-message-${index + 1}`;
|
|
1808
|
+
|
|
1809
|
+
const timestampValue =
|
|
1810
|
+
value.timestamp ??
|
|
1811
|
+
value.createdAt ??
|
|
1812
|
+
value.created_at ??
|
|
1813
|
+
value.updatedAt ??
|
|
1814
|
+
value.updated_at ??
|
|
1815
|
+
value.date ??
|
|
1816
|
+
value.time;
|
|
1817
|
+
|
|
1818
|
+
const timestamp = this.parseTimestamp(timestampValue);
|
|
1819
|
+
|
|
1820
|
+
return {
|
|
1821
|
+
id,
|
|
1822
|
+
role,
|
|
1823
|
+
text,
|
|
1824
|
+
html: this.renderMarkdown(text),
|
|
1825
|
+
timestamp,
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
private normalizeRole(value: unknown): ChatRole {
|
|
1830
|
+
if (typeof value === 'string') {
|
|
1831
|
+
const normalized = value.toLowerCase();
|
|
1832
|
+
if (normalized.includes('user') || normalized.includes('client')) {
|
|
1833
|
+
return 'user';
|
|
1834
|
+
}
|
|
1835
|
+
if (normalized.includes('assistant') || normalized.includes('agent') || normalized.includes('bot')) {
|
|
1836
|
+
return 'assistant';
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
return 'assistant';
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
private parseTimestamp(value: unknown, fallback?: number) {
|
|
1844
|
+
const parsed = Date.parse(this.toIsoString(value));
|
|
1845
|
+
if (Number.isFinite(parsed)) {
|
|
1846
|
+
return parsed;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
if (Number.isFinite(fallback ?? NaN)) {
|
|
1850
|
+
return fallback as number;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
return Date.now();
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
private lookupConversationTitle(conversationId: string | null) {
|
|
1857
|
+
if (!conversationId) {
|
|
1858
|
+
return null;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
const found = this.conversations.find((item) => item.id === conversationId);
|
|
1862
|
+
return found ? found.title : null;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
private syncActiveConversationTitle() {
|
|
1866
|
+
if (!this.currentConversationId) {
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
const title = this.lookupConversationTitle(this.currentConversationId);
|
|
1871
|
+
if (title) {
|
|
1872
|
+
this.activeConversationTitle = title;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
private toIsoString(value: unknown) {
|
|
1877
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
1878
|
+
const date = new Date(value);
|
|
1879
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1880
|
+
return date.toISOString();
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
return new Date().toISOString();
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
private startLoadingGuard() {
|
|
1888
|
+
this.clearLoadingGuard();
|
|
1889
|
+
this.loadingLabelInternal = 'RIO Insight está respondendo';
|
|
1890
|
+
|
|
1891
|
+
// Após 20s, mensagem de processamento prolongado.
|
|
1892
|
+
this.loadingTimerSlow = window.setTimeout(() => {
|
|
1893
|
+
this.loadingLabelInternal = 'RIO Insight continua respondendo';
|
|
1894
|
+
this.requestUpdate();
|
|
1895
|
+
}, 20000);
|
|
1896
|
+
|
|
1897
|
+
// Após 60s, aviso de demora maior.
|
|
1898
|
+
this.loadingTimerTimeout = window.setTimeout(() => {
|
|
1899
|
+
this.loadingLabelInternal =
|
|
1900
|
+
'RIO Insight ainda está processando sua resposta. Peço que aguarde um pouco mais';
|
|
1901
|
+
this.requestUpdate();
|
|
1902
|
+
}, 60000);
|
|
1903
|
+
|
|
1904
|
+
// Após 120s, novo aviso de demora maior.
|
|
1905
|
+
this.loadingTimerTimeout = window.setTimeout(() => {
|
|
1906
|
+
this.loadingLabelInternal =
|
|
1907
|
+
'Essa solicitação está demorando um pouco mais que o esperado. Pode favor, aguarde mais um pouco';
|
|
1908
|
+
this.requestUpdate();
|
|
1909
|
+
}, 120000);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
private clearLoadingGuard() {
|
|
1913
|
+
if (this.loadingTimer !== null) {
|
|
1914
|
+
window.clearTimeout(this.loadingTimer);
|
|
1915
|
+
this.loadingTimer = null;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
if (this.loadingTimerSlow !== null) {
|
|
1919
|
+
window.clearTimeout(this.loadingTimerSlow);
|
|
1920
|
+
this.loadingTimerSlow = null;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
if (this.loadingTimerTimeout !== null) {
|
|
1924
|
+
window.clearTimeout(this.loadingTimerTimeout);
|
|
1925
|
+
this.loadingTimerTimeout = null;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
private scrollConversationToBottom() {
|
|
1930
|
+
const containers = Array.from(
|
|
1931
|
+
this.renderRoot.querySelectorAll('.panel-content'),
|
|
1932
|
+
) as HTMLElement[];
|
|
1933
|
+
|
|
1934
|
+
containers.forEach((container) => {
|
|
1935
|
+
requestAnimationFrame(() => {
|
|
1936
|
+
container.scrollTop = container.scrollHeight;
|
|
1937
|
+
});
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
private renderMarkdown(content: string) {
|
|
1942
|
+
const rendered = this.markdownRenderer.render(content);
|
|
1943
|
+
const clean = DOMPurify.sanitize(rendered, {
|
|
1944
|
+
ALLOWED_TAGS: [
|
|
1945
|
+
'a',
|
|
1946
|
+
'p',
|
|
1947
|
+
'ul',
|
|
1948
|
+
'ol',
|
|
1949
|
+
'li',
|
|
1950
|
+
'code',
|
|
1951
|
+
'pre',
|
|
1952
|
+
'strong',
|
|
1953
|
+
'em',
|
|
1954
|
+
'blockquote',
|
|
1955
|
+
'table',
|
|
1956
|
+
'thead',
|
|
1957
|
+
'tbody',
|
|
1958
|
+
'tr',
|
|
1959
|
+
'th',
|
|
1960
|
+
'td',
|
|
1961
|
+
'del',
|
|
1962
|
+
'hr',
|
|
1963
|
+
'br',
|
|
1964
|
+
'img',
|
|
1965
|
+
'span',
|
|
1966
|
+
'input',
|
|
1967
|
+
],
|
|
1968
|
+
ALLOWED_ATTR: [
|
|
1969
|
+
'href',
|
|
1970
|
+
'title',
|
|
1971
|
+
'target',
|
|
1972
|
+
'rel',
|
|
1973
|
+
'src',
|
|
1974
|
+
'alt',
|
|
1975
|
+
'class',
|
|
1976
|
+
'type',
|
|
1977
|
+
'checked',
|
|
1978
|
+
'disabled',
|
|
1979
|
+
'aria-label',
|
|
1980
|
+
],
|
|
1981
|
+
ALLOW_DATA_ATTR: false,
|
|
1982
|
+
FORBID_TAGS: ['style', 'script'],
|
|
1983
|
+
USE_PROFILES: { html: true },
|
|
1984
|
+
});
|
|
1985
|
+
|
|
1986
|
+
const container = document.createElement('div');
|
|
1987
|
+
container.innerHTML = clean;
|
|
1988
|
+
|
|
1989
|
+
container.querySelectorAll('a').forEach((anchor) => {
|
|
1990
|
+
anchor.setAttribute('target', '_blank');
|
|
1991
|
+
anchor.setAttribute('rel', 'noopener noreferrer');
|
|
1992
|
+
});
|
|
1993
|
+
|
|
1994
|
+
container.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {
|
|
1995
|
+
checkbox.setAttribute('disabled', '');
|
|
1996
|
+
checkbox.setAttribute('tabindex', '-1');
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
return container.innerHTML;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
render() {
|
|
2003
|
+
return renderRioAssist(this);
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
}
|
|
2007
|
+
declare global {
|
|
2008
|
+
interface HTMLElementTagNameMap {
|
|
2009
|
+
'rio-assist-widget': RioAssistWidget;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
if (!customElements.get('rio-assist-widget')) {
|
|
2014
|
+
customElements.define('rio-assist-widget', RioAssistWidget);
|
|
2015
|
+
}
|