rio-assist-widget 0.1.14 → 0.1.23
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 +9 -3
- package/dist/rio-assist.js +196 -38
- package/index.html +1 -2
- package/package.json +1 -1
- package/src/components/conversations-panel/conversations-panel.styles.ts +11 -5
- package/src/components/conversations-panel/conversations-panel.template.ts +15 -12
- package/src/components/mini-panel/mini-panel.styles.ts +46 -13
- package/src/components/mini-panel/mini-panel.template.ts +4 -1
- package/src/components/rio-assist/rio-assist.styles.ts +74 -0
- package/src/components/rio-assist/rio-assist.template.ts +48 -0
- package/src/components/rio-assist/rio-assist.ts +714 -371
- package/src/main.ts +15 -9
- package/src/playground.ts +9 -3
- package/src/services/rioWebsocket.ts +23 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LitElement, type PropertyValues } from 'lit';
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit';
|
|
2
2
|
import { widgetStyles } from './rio-assist.styles';
|
|
3
3
|
import { renderRioAssist } from './rio-assist.template';
|
|
4
4
|
import {
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
import MarkdownIt from 'markdown-it';
|
|
9
9
|
import markdownItTaskLists from 'markdown-it-task-lists';
|
|
10
10
|
import DOMPurify from 'dompurify';
|
|
11
|
-
|
|
12
|
-
type ChatRole = 'user' | 'assistant';
|
|
13
|
-
|
|
11
|
+
|
|
12
|
+
type ChatRole = 'user' | 'assistant';
|
|
13
|
+
|
|
14
14
|
export type ChatMessage = {
|
|
15
15
|
id: string;
|
|
16
16
|
role: ChatRole;
|
|
@@ -18,13 +18,26 @@ export type ChatMessage = {
|
|
|
18
18
|
html?: string;
|
|
19
19
|
timestamp: number;
|
|
20
20
|
};
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
type ConversationItem = {
|
|
23
23
|
id: string;
|
|
24
24
|
title: string;
|
|
25
25
|
updatedAt: string;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
type ConversationDeleteTarget = {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
index: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type ConversationRenameTarget = {
|
|
35
|
+
id: string;
|
|
36
|
+
title: string;
|
|
37
|
+
index: number;
|
|
38
|
+
draft: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
28
41
|
export type HeaderActionConfig = {
|
|
29
42
|
id?: string;
|
|
30
43
|
iconUrl: string;
|
|
@@ -33,24 +46,24 @@ export type HeaderActionConfig = {
|
|
|
33
46
|
};
|
|
34
47
|
|
|
35
48
|
export class RioAssistWidget extends LitElement {
|
|
36
|
-
static styles = widgetStyles;
|
|
37
|
-
|
|
38
|
-
static properties = {
|
|
39
|
-
open: { type: Boolean, state: true },
|
|
40
|
-
message: { type: String, state: true },
|
|
41
|
-
titleText: { type: String, attribute: 'data-title' },
|
|
42
|
-
buttonLabel: { type: String, attribute: 'data-button-label' },
|
|
43
|
-
placeholder: { type: String, attribute: 'data-placeholder' },
|
|
44
|
-
accentColor: { type: String, attribute: 'data-accent-color' },
|
|
45
|
-
apiBaseUrl: { type: String, attribute: 'data-api-base-url' },
|
|
46
|
-
rioToken: { type: String, attribute: 'data-rio-token' },
|
|
47
|
-
suggestionsSource: { type: String, attribute: 'data-suggestions' },
|
|
48
|
-
messages: { state: true },
|
|
49
|
-
isLoading: { type: Boolean, state: true },
|
|
50
|
-
errorMessage: { type: String, state: true },
|
|
51
|
-
showConversations: { type: Boolean, state: true },
|
|
52
|
-
conversationSearch: { type: String, state: true },
|
|
53
|
-
conversationMenuId: { state: true },
|
|
49
|
+
static styles = widgetStyles;
|
|
50
|
+
|
|
51
|
+
static properties = {
|
|
52
|
+
open: { type: Boolean, state: true },
|
|
53
|
+
message: { type: String, state: true },
|
|
54
|
+
titleText: { type: String, attribute: 'data-title' },
|
|
55
|
+
buttonLabel: { type: String, attribute: 'data-button-label' },
|
|
56
|
+
placeholder: { type: String, attribute: 'data-placeholder' },
|
|
57
|
+
accentColor: { type: String, attribute: 'data-accent-color' },
|
|
58
|
+
apiBaseUrl: { type: String, attribute: 'data-api-base-url' },
|
|
59
|
+
rioToken: { type: String, attribute: 'data-rio-token' },
|
|
60
|
+
suggestionsSource: { type: String, attribute: 'data-suggestions' },
|
|
61
|
+
messages: { state: true },
|
|
62
|
+
isLoading: { type: Boolean, state: true },
|
|
63
|
+
errorMessage: { type: String, state: true },
|
|
64
|
+
showConversations: { type: Boolean, state: true },
|
|
65
|
+
conversationSearch: { type: String, state: true },
|
|
66
|
+
conversationMenuId: { state: true },
|
|
54
67
|
conversationMenuPlacement: { state: true },
|
|
55
68
|
isFullscreen: { type: Boolean, state: true },
|
|
56
69
|
conversationScrollbar: { state: true },
|
|
@@ -58,42 +71,51 @@ export class RioAssistWidget extends LitElement {
|
|
|
58
71
|
conversations: { state: true },
|
|
59
72
|
conversationHistoryLoading: { type: Boolean, state: true },
|
|
60
73
|
activeConversationTitle: { state: true },
|
|
74
|
+
conversationHistoryError: { type: String, state: true },
|
|
75
|
+
deleteConversationTarget: { attribute: false },
|
|
76
|
+
renameConversationTarget: { attribute: false },
|
|
61
77
|
headerActions: { attribute: false },
|
|
62
78
|
homeUrl: { type: String, attribute: 'data-home-url' },
|
|
63
79
|
};
|
|
64
|
-
|
|
65
|
-
open = false;
|
|
66
|
-
|
|
67
|
-
message = '';
|
|
68
|
-
|
|
69
|
-
titleText = 'Rio Insight';
|
|
70
|
-
|
|
71
|
-
buttonLabel = 'Rio Insight';
|
|
72
|
-
|
|
73
|
-
placeholder = 'Pergunte alguma coisa';
|
|
74
|
-
|
|
75
|
-
accentColor = '#008B9A';
|
|
76
|
-
|
|
77
|
-
apiBaseUrl = '';
|
|
78
|
-
|
|
79
|
-
rioToken = '';
|
|
80
|
-
|
|
81
|
-
suggestionsSource = '';
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
80
|
+
|
|
81
|
+
open = false;
|
|
82
|
+
|
|
83
|
+
message = '';
|
|
84
|
+
|
|
85
|
+
titleText = 'Rio Insight';
|
|
86
|
+
|
|
87
|
+
buttonLabel = 'Rio Insight';
|
|
88
|
+
|
|
89
|
+
placeholder = 'Pergunte alguma coisa';
|
|
90
|
+
|
|
91
|
+
accentColor = '#008B9A';
|
|
92
|
+
|
|
93
|
+
apiBaseUrl = '';
|
|
94
|
+
|
|
95
|
+
rioToken = '';
|
|
96
|
+
|
|
97
|
+
suggestionsSource = '';
|
|
98
|
+
|
|
99
|
+
private randomizedSuggestions: string[] = [];
|
|
100
|
+
|
|
101
|
+
messages: ChatMessage[] = [];
|
|
102
|
+
|
|
103
|
+
isLoading = false;
|
|
104
|
+
|
|
105
|
+
errorMessage = '';
|
|
106
|
+
|
|
107
|
+
get loadingLabel() {
|
|
108
|
+
return this.loadingLabelInternal;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
showConversations = false;
|
|
112
|
+
|
|
113
|
+
conversationSearch = '';
|
|
114
|
+
|
|
115
|
+
conversationMenuId: string | null = null;
|
|
116
|
+
|
|
117
|
+
conversationMenuPlacement: 'above' | 'below' = 'below';
|
|
118
|
+
|
|
97
119
|
isFullscreen = false;
|
|
98
120
|
|
|
99
121
|
showNewConversationShortcut = false;
|
|
@@ -106,6 +128,16 @@ export class RioAssistWidget extends LitElement {
|
|
|
106
128
|
|
|
107
129
|
conversationHistoryLoading = false;
|
|
108
130
|
|
|
131
|
+
conversationHistoryError = '';
|
|
132
|
+
|
|
133
|
+
deleteConversationTarget: ConversationDeleteTarget | null = null;
|
|
134
|
+
|
|
135
|
+
renameConversationTarget: ConversationRenameTarget | null = null;
|
|
136
|
+
|
|
137
|
+
private loadingLabelInternal = 'Rio Insight está respondendo...';
|
|
138
|
+
private loadingTimerSlow: number | null = null;
|
|
139
|
+
private loadingTimerTimeout: number | null = null;
|
|
140
|
+
|
|
109
141
|
private refreshConversationsAfterResponse = false;
|
|
110
142
|
|
|
111
143
|
activeConversationTitle: string | null = null;
|
|
@@ -160,9 +192,9 @@ export class RioAssistWidget extends LitElement {
|
|
|
160
192
|
}
|
|
161
193
|
return result;
|
|
162
194
|
}
|
|
163
|
-
|
|
164
|
-
private conversationScrollbarRaf: number | null = null;
|
|
165
|
-
|
|
195
|
+
|
|
196
|
+
private conversationScrollbarRaf: number | null = null;
|
|
197
|
+
|
|
166
198
|
private rioClient: RioWebsocketClient | null = null;
|
|
167
199
|
|
|
168
200
|
private rioUnsubscribe: (() => void) | null = null;
|
|
@@ -176,7 +208,7 @@ export class RioAssistWidget extends LitElement {
|
|
|
176
208
|
private conversationUserId: string | null = null;
|
|
177
209
|
|
|
178
210
|
private conversationScrollbarDraggingId: number | null = null;
|
|
179
|
-
|
|
211
|
+
|
|
180
212
|
private conversationScrollbarDragState: {
|
|
181
213
|
startY: number;
|
|
182
214
|
startThumbTop: number;
|
|
@@ -190,24 +222,53 @@ export class RioAssistWidget extends LitElement {
|
|
|
190
222
|
linkify: true,
|
|
191
223
|
breaks: true,
|
|
192
224
|
}).use(markdownItTaskLists);
|
|
193
|
-
|
|
194
|
-
conversations: ConversationItem[] = [];
|
|
195
|
-
|
|
196
|
-
get suggestions(): string[] {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
225
|
+
|
|
226
|
+
conversations: ConversationItem[] = [];
|
|
227
|
+
|
|
228
|
+
get suggestions(): string[] {
|
|
229
|
+
return this.randomizedSuggestions;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private parseSuggestions(source: string): string[] {
|
|
233
|
+
if (!source) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return source
|
|
238
|
+
.split('|')
|
|
239
|
+
.map((item) => item.trim())
|
|
240
|
+
.filter(Boolean);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private pickRandomSuggestions(options: string[], count: number): string[] {
|
|
244
|
+
if (options.length <= count) {
|
|
245
|
+
return [...options];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const pool = [...options];
|
|
249
|
+
for (let index = pool.length - 1; index > 0; index -= 1) {
|
|
250
|
+
const swapIndex = Math.floor(Math.random() * (index + 1));
|
|
251
|
+
[pool[index], pool[swapIndex]] = [pool[swapIndex], pool[index]];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return pool.slice(0, count);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
protected willUpdate(changedProperties: PropertyValues): void {
|
|
258
|
+
super.willUpdate(changedProperties);
|
|
259
|
+
|
|
260
|
+
if (changedProperties.has('suggestionsSource')) {
|
|
261
|
+
this.randomizedSuggestions = this.pickRandomSuggestions(
|
|
262
|
+
this.parseSuggestions(this.suggestionsSource),
|
|
263
|
+
3,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
protected updated(changedProperties: PropertyValues): void {
|
|
269
|
+
super.updated(changedProperties);
|
|
270
|
+
this.style.setProperty('--accent-color', this.accentColor);
|
|
271
|
+
|
|
211
272
|
if (
|
|
212
273
|
changedProperties.has('isFullscreen') ||
|
|
213
274
|
changedProperties.has('showConversations') ||
|
|
@@ -225,59 +286,59 @@ export class RioAssistWidget extends LitElement {
|
|
|
225
286
|
this.scrollConversationToBottom();
|
|
226
287
|
}
|
|
227
288
|
}
|
|
228
|
-
|
|
229
|
-
protected firstUpdated(): void {
|
|
230
|
-
this.enqueueConversationScrollbarMeasure();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
disconnectedCallback(): void {
|
|
234
|
-
super.disconnectedCallback();
|
|
235
|
-
if (this.conversationScrollbarRaf !== null) {
|
|
236
|
-
cancelAnimationFrame(this.conversationScrollbarRaf);
|
|
237
|
-
this.conversationScrollbarRaf = null;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
this.teardownRioClient();
|
|
241
|
-
this.clearLoadingGuard();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
get filteredConversations() {
|
|
245
|
-
const query = this.conversationSearch.trim().toLowerCase();
|
|
246
|
-
if (!query) {
|
|
247
|
-
return this.conversations;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return this.conversations.filter((conversation) =>
|
|
251
|
-
conversation.title.toLowerCase().includes(query),
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
get hasActiveConversation() {
|
|
256
|
-
return this.messages.length > 0;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
togglePanel() {
|
|
260
|
-
if (this.isFullscreen) {
|
|
261
|
-
this.exitFullscreen(false);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
this.open = !this.open;
|
|
266
|
-
this.dispatchEvent(
|
|
267
|
-
new CustomEvent(this.open ? 'rioassist:open' : 'rioassist:close', {
|
|
268
|
-
bubbles: true,
|
|
269
|
-
composed: true,
|
|
270
|
-
}),
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
closePanel() {
|
|
275
|
-
this.isFullscreen = false;
|
|
276
|
-
if (this.open) {
|
|
277
|
-
this.togglePanel();
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
289
|
+
|
|
290
|
+
protected firstUpdated(): void {
|
|
291
|
+
this.enqueueConversationScrollbarMeasure();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
disconnectedCallback(): void {
|
|
295
|
+
super.disconnectedCallback();
|
|
296
|
+
if (this.conversationScrollbarRaf !== null) {
|
|
297
|
+
cancelAnimationFrame(this.conversationScrollbarRaf);
|
|
298
|
+
this.conversationScrollbarRaf = null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
this.teardownRioClient();
|
|
302
|
+
this.clearLoadingGuard();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
get filteredConversations() {
|
|
306
|
+
const query = this.conversationSearch.trim().toLowerCase();
|
|
307
|
+
if (!query) {
|
|
308
|
+
return this.conversations;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return this.conversations.filter((conversation) =>
|
|
312
|
+
conversation.title.toLowerCase().includes(query),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
get hasActiveConversation() {
|
|
317
|
+
return this.messages.length > 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
togglePanel() {
|
|
321
|
+
if (this.isFullscreen) {
|
|
322
|
+
this.exitFullscreen(false);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.open = !this.open;
|
|
327
|
+
this.dispatchEvent(
|
|
328
|
+
new CustomEvent(this.open ? 'rioassist:open' : 'rioassist:close', {
|
|
329
|
+
bubbles: true,
|
|
330
|
+
composed: true,
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
closePanel() {
|
|
336
|
+
this.isFullscreen = false;
|
|
337
|
+
if (this.open) {
|
|
338
|
+
this.togglePanel();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
281
342
|
openConversationsPanel() {
|
|
282
343
|
this.showConversations = true;
|
|
283
344
|
this.requestConversationHistory();
|
|
@@ -297,7 +358,7 @@ export class RioAssistWidget extends LitElement {
|
|
|
297
358
|
|
|
298
359
|
this.requestConversationHistory();
|
|
299
360
|
}
|
|
300
|
-
|
|
361
|
+
|
|
301
362
|
toggleNewConversationShortcut() {
|
|
302
363
|
this.showNewConversationShortcut = !this.showNewConversationShortcut;
|
|
303
364
|
}
|
|
@@ -320,53 +381,65 @@ export class RioAssistWidget extends LitElement {
|
|
|
320
381
|
handleConversationSearch(event: InputEvent) {
|
|
321
382
|
this.conversationSearch = (event.target as HTMLInputElement).value;
|
|
322
383
|
}
|
|
323
|
-
|
|
324
|
-
handleConversationMenuToggle(event: Event, id: string) {
|
|
325
|
-
event.stopPropagation();
|
|
326
|
-
|
|
327
|
-
if (this.conversationMenuId === id) {
|
|
328
|
-
this.conversationMenuId = null;
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const button = event.currentTarget as HTMLElement;
|
|
333
|
-
const container = this.renderRoot.querySelector(
|
|
334
|
-
'.conversations-panel__surface',
|
|
335
|
-
) as HTMLElement | null;
|
|
336
|
-
|
|
337
|
-
if (button && container) {
|
|
338
|
-
const buttonRect = button.getBoundingClientRect();
|
|
339
|
-
const containerRect = container.getBoundingClientRect();
|
|
340
|
-
const spaceBelow = containerRect.bottom - buttonRect.bottom;
|
|
341
|
-
this.conversationMenuPlacement = spaceBelow < 140 ? 'above' : 'below';
|
|
342
|
-
} else {
|
|
343
|
-
this.conversationMenuPlacement = 'below';
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
this.conversationMenuId = id;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
handleConversationsPanelPointer(event: PointerEvent) {
|
|
350
|
-
const target = event.target as HTMLElement;
|
|
351
|
-
if (
|
|
352
|
-
!target.closest('.conversation-menu') &&
|
|
353
|
-
!target.closest('.conversation-menu-button')
|
|
354
|
-
) {
|
|
355
|
-
this.conversationMenuId = null;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
384
|
+
|
|
385
|
+
handleConversationMenuToggle(event: Event, id: string) {
|
|
386
|
+
event.stopPropagation();
|
|
387
|
+
|
|
388
|
+
if (this.conversationMenuId === id) {
|
|
389
|
+
this.conversationMenuId = null;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const button = event.currentTarget as HTMLElement;
|
|
394
|
+
const container = this.renderRoot.querySelector(
|
|
395
|
+
'.conversations-panel__surface',
|
|
396
|
+
) as HTMLElement | null;
|
|
397
|
+
|
|
398
|
+
if (button && container) {
|
|
399
|
+
const buttonRect = button.getBoundingClientRect();
|
|
400
|
+
const containerRect = container.getBoundingClientRect();
|
|
401
|
+
const spaceBelow = containerRect.bottom - buttonRect.bottom;
|
|
402
|
+
this.conversationMenuPlacement = spaceBelow < 140 ? 'above' : 'below';
|
|
403
|
+
} else {
|
|
404
|
+
this.conversationMenuPlacement = 'below';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
this.conversationMenuId = id;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
handleConversationsPanelPointer(event: PointerEvent) {
|
|
411
|
+
const target = event.target as HTMLElement;
|
|
412
|
+
if (
|
|
413
|
+
!target.closest('.conversation-menu') &&
|
|
414
|
+
!target.closest('.conversation-menu-button')
|
|
415
|
+
) {
|
|
416
|
+
this.conversationMenuId = null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
359
420
|
handleConversationAction(action: 'rename' | 'delete', id: string) {
|
|
360
421
|
this.conversationMenuId = null;
|
|
361
|
-
const
|
|
362
|
-
if (
|
|
422
|
+
const conversationIndex = this.conversations.findIndex((item) => item.id === id);
|
|
423
|
+
if (conversationIndex === -1) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const conversation = this.conversations[conversationIndex];
|
|
428
|
+
if (action === 'delete') {
|
|
429
|
+
this.deleteConversationTarget = {
|
|
430
|
+
id: conversation.id,
|
|
431
|
+
title: conversation.title,
|
|
432
|
+
index: conversationIndex,
|
|
433
|
+
};
|
|
363
434
|
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
this.renameConversationTarget = {
|
|
438
|
+
id: conversation.id,
|
|
439
|
+
title: conversation.title,
|
|
440
|
+
index: conversationIndex,
|
|
441
|
+
draft: conversation.title,
|
|
442
|
+
};
|
|
370
443
|
}
|
|
371
444
|
|
|
372
445
|
handleHomeNavigation() {
|
|
@@ -389,6 +462,240 @@ export class RioAssistWidget extends LitElement {
|
|
|
389
462
|
}
|
|
390
463
|
}
|
|
391
464
|
|
|
465
|
+
applyConversationRename(id: string, newTitle: string) {
|
|
466
|
+
if (!id || !newTitle) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let changed = false;
|
|
471
|
+
this.conversations = this.conversations.map((conversation) => {
|
|
472
|
+
if (conversation.id === id) {
|
|
473
|
+
changed = true;
|
|
474
|
+
return { ...conversation, title: newTitle };
|
|
475
|
+
}
|
|
476
|
+
return conversation;
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if (!changed) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (this.currentConversationId === id) {
|
|
484
|
+
this.activeConversationTitle = newTitle;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
applyConversationDeletion(id: string) {
|
|
489
|
+
if (!id) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const wasActive = this.currentConversationId === id;
|
|
494
|
+
const next = this.conversations.filter((conversation) => conversation.id !== id);
|
|
495
|
+
|
|
496
|
+
if (next.length === this.conversations.length) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
this.conversations = next;
|
|
501
|
+
|
|
502
|
+
if (wasActive) {
|
|
503
|
+
this.currentConversationId = null;
|
|
504
|
+
this.activeConversationTitle = null;
|
|
505
|
+
this.messages = [];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async confirmDeleteConversation() {
|
|
510
|
+
const target = this.deleteConversationTarget;
|
|
511
|
+
if (!target) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const success = await this.dispatchConversationAction(
|
|
516
|
+
'delete',
|
|
517
|
+
{ id: target.id, title: target.title },
|
|
518
|
+
target.index,
|
|
519
|
+
);
|
|
520
|
+
if (success) {
|
|
521
|
+
this.deleteConversationTarget = null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
cancelDeleteConversation() {
|
|
526
|
+
this.deleteConversationTarget = null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
handleRenameDraft(event: InputEvent) {
|
|
530
|
+
if (!this.renameConversationTarget) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
this.renameConversationTarget = {
|
|
535
|
+
...this.renameConversationTarget,
|
|
536
|
+
draft: (event.target as HTMLInputElement).value,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async confirmRenameConversation() {
|
|
541
|
+
const target = this.renameConversationTarget;
|
|
542
|
+
if (!target) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const newTitle = target.draft.trim();
|
|
547
|
+
if (!newTitle) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const success = await this.dispatchConversationAction(
|
|
552
|
+
'rename',
|
|
553
|
+
{ id: target.id, title: newTitle },
|
|
554
|
+
target.index,
|
|
555
|
+
newTitle,
|
|
556
|
+
);
|
|
557
|
+
if (success) {
|
|
558
|
+
this.renameConversationTarget = null;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
cancelRenameConversation() {
|
|
563
|
+
this.renameConversationTarget = null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private async dispatchConversationAction(
|
|
567
|
+
action: 'rename' | 'delete',
|
|
568
|
+
conversation: Pick<ConversationItem, 'id' | 'title'>,
|
|
569
|
+
index: number,
|
|
570
|
+
newTitle?: string,
|
|
571
|
+
) {
|
|
572
|
+
const eventName =
|
|
573
|
+
action === 'rename' ? 'rioassist:conversation-rename' : 'rioassist:conversation-delete';
|
|
574
|
+
const detail = {
|
|
575
|
+
id: conversation.id,
|
|
576
|
+
title: conversation.title,
|
|
577
|
+
index,
|
|
578
|
+
action,
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const allowed = this.dispatchEvent(
|
|
582
|
+
new CustomEvent(eventName, {
|
|
583
|
+
detail,
|
|
584
|
+
bubbles: true,
|
|
585
|
+
composed: true,
|
|
586
|
+
cancelable: true,
|
|
587
|
+
}),
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
if (!allowed) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (action === 'delete') {
|
|
595
|
+
const ok = await this.syncConversationDeleteBackend(conversation.id);
|
|
596
|
+
return ok;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (action === 'rename' && newTitle) {
|
|
600
|
+
const ok = await this.syncConversationRenameBackend(conversation.id, newTitle);
|
|
601
|
+
return ok;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private async syncConversationRenameBackend(conversationId: string, newTitle: string) {
|
|
608
|
+
try {
|
|
609
|
+
const client = this.ensureRioClient();
|
|
610
|
+
await client.renameConversation(conversationId, newTitle);
|
|
611
|
+
this.applyConversationRename(conversationId, newTitle);
|
|
612
|
+
this.conversationHistoryError = '';
|
|
613
|
+
return true;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
console.error('[RioAssist][history] erro ao renomear conversa', error);
|
|
616
|
+
this.conversationHistoryError =
|
|
617
|
+
error instanceof Error && error.message
|
|
618
|
+
? error.message
|
|
619
|
+
: 'Nao foi possivel renomear a conversa.';
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private async syncConversationDeleteBackend(conversationId: string) {
|
|
625
|
+
try {
|
|
626
|
+
const client = this.ensureRioClient();
|
|
627
|
+
await client.deleteConversation(conversationId);
|
|
628
|
+
this.applyConversationDeletion(conversationId);
|
|
629
|
+
this.conversationHistoryError = '';
|
|
630
|
+
return true;
|
|
631
|
+
} catch (error) {
|
|
632
|
+
console.error('[RioAssist][history] erro ao excluir conversa', error);
|
|
633
|
+
this.conversationHistoryError =
|
|
634
|
+
error instanceof Error && error.message
|
|
635
|
+
? error.message
|
|
636
|
+
: 'Nao foi possivel excluir a conversa.';
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private handleConversationSystemAction(message: RioIncomingMessage) {
|
|
642
|
+
const action = (message.action ?? '').toLowerCase();
|
|
643
|
+
if (action === 'conversationrenamed') {
|
|
644
|
+
const data = message.data as Record<string, unknown>;
|
|
645
|
+
const id = this.extractString(data, ['conversationId', 'id']);
|
|
646
|
+
const newTitle = this.extractString(data, ['newTitle', 'title']);
|
|
647
|
+
if (id && newTitle) {
|
|
648
|
+
this.applyConversationRename(id, newTitle);
|
|
649
|
+
this.conversationHistoryError = '';
|
|
650
|
+
}
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (action === 'conversationdeleted') {
|
|
655
|
+
const data = message.data as Record<string, unknown>;
|
|
656
|
+
const id = this.extractString(data, ['conversationId', 'id']);
|
|
657
|
+
if (id) {
|
|
658
|
+
this.applyConversationDeletion(id);
|
|
659
|
+
this.conversationHistoryError = '';
|
|
660
|
+
}
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (action === 'processing') {
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private shouldIgnoreAssistantPayload(action?: string) {
|
|
672
|
+
if (!action) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
const normalized = action.toLowerCase();
|
|
676
|
+
return (
|
|
677
|
+
normalized === 'processing' ||
|
|
678
|
+
normalized === 'conversationrenamed' ||
|
|
679
|
+
normalized === 'conversationdeleted'
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private extractString(
|
|
684
|
+
data: Record<string, unknown> | undefined,
|
|
685
|
+
keys: string[],
|
|
686
|
+
): string | null {
|
|
687
|
+
if (!data || typeof data !== 'object') {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
for (const key of keys) {
|
|
691
|
+
const value = data[key];
|
|
692
|
+
if (typeof value === 'string' && value.trim()) {
|
|
693
|
+
return value;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
|
|
392
699
|
handleHeaderActionClick(action: HeaderActionConfig, index: number) {
|
|
393
700
|
const detail = {
|
|
394
701
|
index,
|
|
@@ -420,37 +727,38 @@ export class RioAssistWidget extends LitElement {
|
|
|
420
727
|
this.exitFullscreen(true);
|
|
421
728
|
return;
|
|
422
729
|
}
|
|
423
|
-
|
|
424
|
-
if (this.showConversations) {
|
|
425
|
-
this.closeConversationsPanel();
|
|
426
|
-
} else {
|
|
427
|
-
this.closePanel();
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
enterFullscreen() {
|
|
432
|
-
if (this.isFullscreen) {
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
this.isFullscreen = true;
|
|
437
|
-
this.open = false;
|
|
438
|
-
this.showConversations = false;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
this.
|
|
448
|
-
this.
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
730
|
+
|
|
731
|
+
if (this.showConversations) {
|
|
732
|
+
this.closeConversationsPanel();
|
|
733
|
+
} else {
|
|
734
|
+
this.closePanel();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
enterFullscreen() {
|
|
739
|
+
if (this.isFullscreen) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
this.isFullscreen = true;
|
|
744
|
+
this.open = false;
|
|
745
|
+
this.showConversations = false;
|
|
746
|
+
this.requestConversationHistory();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
exitFullscreen(restorePanel: boolean) {
|
|
750
|
+
if (!this.isFullscreen) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
this.isFullscreen = false;
|
|
755
|
+
this.conversationMenuId = null;
|
|
756
|
+
this.showNewConversationShortcut = false;
|
|
757
|
+
if (restorePanel) {
|
|
758
|
+
this.open = true;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
454
762
|
handleCreateConversation() {
|
|
455
763
|
if (!this.hasActiveConversation) {
|
|
456
764
|
return;
|
|
@@ -470,132 +778,132 @@ export class RioAssistWidget extends LitElement {
|
|
|
470
778
|
new CustomEvent('rioassist:new-conversation', {
|
|
471
779
|
bubbles: true,
|
|
472
780
|
composed: true,
|
|
473
|
-
}),
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
handleConversationListScroll(event: Event) {
|
|
478
|
-
const target = event.currentTarget as HTMLElement | null;
|
|
479
|
-
if (!target) {
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
this.updateConversationScrollbar(target);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
handleConversationScrollbarPointerDown(event: PointerEvent) {
|
|
486
|
-
const track = event.currentTarget as HTMLElement | null;
|
|
487
|
-
const list = this.renderRoot.querySelector(
|
|
488
|
-
'.conversation-list--sidebar',
|
|
489
|
-
) as HTMLElement | null;
|
|
490
|
-
|
|
491
|
-
if (!track || !list) {
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const trackRect = track.getBoundingClientRect();
|
|
496
|
-
const thumbHeight = trackRect.height * (this.conversationScrollbar.height / 100);
|
|
497
|
-
const maxThumbTop = Math.max(trackRect.height - thumbHeight, 0);
|
|
498
|
-
const scrollRange = Math.max(list.scrollHeight - list.clientHeight, 1);
|
|
499
|
-
const currentThumbTop = (list.scrollTop / scrollRange) * maxThumbTop;
|
|
500
|
-
const offsetY = event.clientY - trackRect.top;
|
|
501
|
-
const isOnThumb = offsetY >= currentThumbTop && offsetY <= currentThumbTop + thumbHeight;
|
|
502
|
-
|
|
503
|
-
const nextThumbTop = isOnThumb
|
|
504
|
-
? currentThumbTop
|
|
505
|
-
: Math.min(Math.max(offsetY - thumbHeight / 2, 0), maxThumbTop);
|
|
506
|
-
|
|
507
|
-
if (!isOnThumb) {
|
|
508
|
-
list.scrollTop = (nextThumbTop / Math.max(maxThumbTop, 1)) * (list.scrollHeight - list.clientHeight);
|
|
509
|
-
this.updateConversationScrollbar(list);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
track.setPointerCapture(event.pointerId);
|
|
513
|
-
this.conversationScrollbarDraggingId = event.pointerId;
|
|
514
|
-
this.conversationScrollbarDragState = {
|
|
515
|
-
startY: event.clientY,
|
|
516
|
-
startThumbTop: nextThumbTop,
|
|
517
|
-
trackHeight: trackRect.height,
|
|
518
|
-
thumbHeight,
|
|
519
|
-
list,
|
|
520
|
-
};
|
|
521
|
-
event.preventDefault();
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
handleConversationScrollbarPointerMove(event: PointerEvent) {
|
|
525
|
-
if (
|
|
526
|
-
this.conversationScrollbarDraggingId === null ||
|
|
527
|
-
this.conversationScrollbarDraggingId !== event.pointerId ||
|
|
528
|
-
!this.conversationScrollbarDragState
|
|
529
|
-
) {
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const {
|
|
534
|
-
startY,
|
|
535
|
-
startThumbTop,
|
|
536
|
-
trackHeight,
|
|
537
|
-
thumbHeight,
|
|
538
|
-
list,
|
|
539
|
-
} = this.conversationScrollbarDragState;
|
|
540
|
-
|
|
541
|
-
const maxThumbTop = Math.max(trackHeight - thumbHeight, 0);
|
|
542
|
-
const deltaY = event.clientY - startY;
|
|
543
|
-
const thumbTop = Math.min(Math.max(startThumbTop + deltaY, 0), maxThumbTop);
|
|
544
|
-
const scrollRange = list.scrollHeight - list.clientHeight;
|
|
545
|
-
|
|
546
|
-
if (scrollRange > 0) {
|
|
547
|
-
list.scrollTop = (thumbTop / Math.max(maxThumbTop, 1)) * scrollRange;
|
|
548
|
-
this.updateConversationScrollbar(list);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
event.preventDefault();
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
handleConversationScrollbarPointerUp(event: PointerEvent) {
|
|
555
|
-
if (this.conversationScrollbarDraggingId !== event.pointerId) {
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const track = event.currentTarget as HTMLElement | null;
|
|
560
|
-
track?.releasePointerCapture(event.pointerId);
|
|
561
|
-
|
|
562
|
-
this.conversationScrollbarDraggingId = null;
|
|
563
|
-
this.conversationScrollbarDragState = null;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
private enqueueConversationScrollbarMeasure() {
|
|
567
|
-
if (this.conversationScrollbarRaf !== null) {
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
this.conversationScrollbarRaf = requestAnimationFrame(() => {
|
|
572
|
-
this.conversationScrollbarRaf = null;
|
|
573
|
-
this.updateConversationScrollbar();
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
private updateConversationScrollbar(target?: HTMLElement | null) {
|
|
578
|
-
const element =
|
|
579
|
-
target ??
|
|
580
|
-
(this.renderRoot.querySelector(
|
|
581
|
-
'.conversation-list--sidebar',
|
|
582
|
-
) as HTMLElement | null);
|
|
583
|
-
|
|
584
|
-
if (!element) {
|
|
585
|
-
if (this.conversationScrollbar.visible) {
|
|
586
|
-
this.conversationScrollbar = { height: 0, top: 0, visible: false };
|
|
587
|
-
}
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const { scrollHeight, clientHeight, scrollTop } = element;
|
|
592
|
-
if (scrollHeight <= clientHeight + 1) {
|
|
593
|
-
if (this.conversationScrollbar.visible) {
|
|
594
|
-
this.conversationScrollbar = { height: 0, top: 0, visible: false };
|
|
595
|
-
}
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
|
|
781
|
+
}),
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
handleConversationListScroll(event: Event) {
|
|
786
|
+
const target = event.currentTarget as HTMLElement | null;
|
|
787
|
+
if (!target) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
this.updateConversationScrollbar(target);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
handleConversationScrollbarPointerDown(event: PointerEvent) {
|
|
794
|
+
const track = event.currentTarget as HTMLElement | null;
|
|
795
|
+
const list = this.renderRoot.querySelector(
|
|
796
|
+
'.conversation-list--sidebar',
|
|
797
|
+
) as HTMLElement | null;
|
|
798
|
+
|
|
799
|
+
if (!track || !list) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const trackRect = track.getBoundingClientRect();
|
|
804
|
+
const thumbHeight = trackRect.height * (this.conversationScrollbar.height / 100);
|
|
805
|
+
const maxThumbTop = Math.max(trackRect.height - thumbHeight, 0);
|
|
806
|
+
const scrollRange = Math.max(list.scrollHeight - list.clientHeight, 1);
|
|
807
|
+
const currentThumbTop = (list.scrollTop / scrollRange) * maxThumbTop;
|
|
808
|
+
const offsetY = event.clientY - trackRect.top;
|
|
809
|
+
const isOnThumb = offsetY >= currentThumbTop && offsetY <= currentThumbTop + thumbHeight;
|
|
810
|
+
|
|
811
|
+
const nextThumbTop = isOnThumb
|
|
812
|
+
? currentThumbTop
|
|
813
|
+
: Math.min(Math.max(offsetY - thumbHeight / 2, 0), maxThumbTop);
|
|
814
|
+
|
|
815
|
+
if (!isOnThumb) {
|
|
816
|
+
list.scrollTop = (nextThumbTop / Math.max(maxThumbTop, 1)) * (list.scrollHeight - list.clientHeight);
|
|
817
|
+
this.updateConversationScrollbar(list);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
track.setPointerCapture(event.pointerId);
|
|
821
|
+
this.conversationScrollbarDraggingId = event.pointerId;
|
|
822
|
+
this.conversationScrollbarDragState = {
|
|
823
|
+
startY: event.clientY,
|
|
824
|
+
startThumbTop: nextThumbTop,
|
|
825
|
+
trackHeight: trackRect.height,
|
|
826
|
+
thumbHeight,
|
|
827
|
+
list,
|
|
828
|
+
};
|
|
829
|
+
event.preventDefault();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
handleConversationScrollbarPointerMove(event: PointerEvent) {
|
|
833
|
+
if (
|
|
834
|
+
this.conversationScrollbarDraggingId === null ||
|
|
835
|
+
this.conversationScrollbarDraggingId !== event.pointerId ||
|
|
836
|
+
!this.conversationScrollbarDragState
|
|
837
|
+
) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const {
|
|
842
|
+
startY,
|
|
843
|
+
startThumbTop,
|
|
844
|
+
trackHeight,
|
|
845
|
+
thumbHeight,
|
|
846
|
+
list,
|
|
847
|
+
} = this.conversationScrollbarDragState;
|
|
848
|
+
|
|
849
|
+
const maxThumbTop = Math.max(trackHeight - thumbHeight, 0);
|
|
850
|
+
const deltaY = event.clientY - startY;
|
|
851
|
+
const thumbTop = Math.min(Math.max(startThumbTop + deltaY, 0), maxThumbTop);
|
|
852
|
+
const scrollRange = list.scrollHeight - list.clientHeight;
|
|
853
|
+
|
|
854
|
+
if (scrollRange > 0) {
|
|
855
|
+
list.scrollTop = (thumbTop / Math.max(maxThumbTop, 1)) * scrollRange;
|
|
856
|
+
this.updateConversationScrollbar(list);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
event.preventDefault();
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
handleConversationScrollbarPointerUp(event: PointerEvent) {
|
|
863
|
+
if (this.conversationScrollbarDraggingId !== event.pointerId) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const track = event.currentTarget as HTMLElement | null;
|
|
868
|
+
track?.releasePointerCapture(event.pointerId);
|
|
869
|
+
|
|
870
|
+
this.conversationScrollbarDraggingId = null;
|
|
871
|
+
this.conversationScrollbarDragState = null;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
private enqueueConversationScrollbarMeasure() {
|
|
875
|
+
if (this.conversationScrollbarRaf !== null) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
this.conversationScrollbarRaf = requestAnimationFrame(() => {
|
|
880
|
+
this.conversationScrollbarRaf = null;
|
|
881
|
+
this.updateConversationScrollbar();
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
private updateConversationScrollbar(target?: HTMLElement | null) {
|
|
886
|
+
const element =
|
|
887
|
+
target ??
|
|
888
|
+
(this.renderRoot.querySelector(
|
|
889
|
+
'.conversation-list--sidebar',
|
|
890
|
+
) as HTMLElement | null);
|
|
891
|
+
|
|
892
|
+
if (!element) {
|
|
893
|
+
if (this.conversationScrollbar.visible) {
|
|
894
|
+
this.conversationScrollbar = { height: 0, top: 0, visible: false };
|
|
895
|
+
}
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const { scrollHeight, clientHeight, scrollTop } = element;
|
|
900
|
+
if (scrollHeight <= clientHeight + 1) {
|
|
901
|
+
if (this.conversationScrollbar.visible) {
|
|
902
|
+
this.conversationScrollbar = { height: 0, top: 0, visible: false };
|
|
903
|
+
}
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
599
907
|
const ratio = clientHeight / scrollHeight;
|
|
600
908
|
const height = Math.max(ratio * 100, 8);
|
|
601
909
|
const maxTop = 100 - height;
|
|
@@ -612,7 +920,7 @@ export class RioAssistWidget extends LitElement {
|
|
|
612
920
|
async onSuggestionClick(suggestion: string) {
|
|
613
921
|
await this.processMessage(suggestion);
|
|
614
922
|
}
|
|
615
|
-
|
|
923
|
+
|
|
616
924
|
async handleSubmit(event: SubmitEvent) {
|
|
617
925
|
event.preventDefault();
|
|
618
926
|
await this.processMessage(this.message);
|
|
@@ -650,12 +958,12 @@ export class RioAssistWidget extends LitElement {
|
|
|
650
958
|
detail: {
|
|
651
959
|
message: content,
|
|
652
960
|
apiBaseUrl: this.apiBaseUrl,
|
|
653
|
-
token: this.rioToken,
|
|
654
|
-
},
|
|
655
|
-
bubbles: true,
|
|
656
|
-
composed: true,
|
|
657
|
-
}),
|
|
658
|
-
);
|
|
961
|
+
token: this.rioToken,
|
|
962
|
+
},
|
|
963
|
+
bubbles: true,
|
|
964
|
+
composed: true,
|
|
965
|
+
}),
|
|
966
|
+
);
|
|
659
967
|
|
|
660
968
|
const userMessage = this.createMessage('user', content);
|
|
661
969
|
this.messages = [...this.messages, userMessage];
|
|
@@ -667,7 +975,7 @@ export class RioAssistWidget extends LitElement {
|
|
|
667
975
|
this.errorMessage = '';
|
|
668
976
|
this.isLoading = true;
|
|
669
977
|
this.startLoadingGuard();
|
|
670
|
-
|
|
978
|
+
|
|
671
979
|
try {
|
|
672
980
|
const client = this.ensureRioClient();
|
|
673
981
|
await client.sendMessage(content, this.currentConversationId);
|
|
@@ -676,25 +984,25 @@ export class RioAssistWidget extends LitElement {
|
|
|
676
984
|
this.isLoading = false;
|
|
677
985
|
this.errorMessage = error instanceof Error
|
|
678
986
|
? error.message
|
|
679
|
-
: 'Nao foi possivel enviar a mensagem para o agente.';
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
987
|
+
: 'Nao foi possivel enviar a mensagem para o agente.';
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
683
991
|
private ensureRioClient() {
|
|
684
992
|
const token = this.rioToken.trim();
|
|
685
993
|
if (!token) {
|
|
686
994
|
throw new Error(
|
|
687
995
|
'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
if (!this.rioClient || !this.rioClient.matchesToken(token)) {
|
|
692
|
-
this.teardownRioClient();
|
|
693
|
-
this.rioClient = new RioWebsocketClient(token);
|
|
694
|
-
this.rioUnsubscribe = this.rioClient.onMessage((incoming) => {
|
|
695
|
-
this.handleIncomingMessage(incoming);
|
|
696
|
-
});
|
|
697
|
-
}
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (!this.rioClient || !this.rioClient.matchesToken(token)) {
|
|
1000
|
+
this.teardownRioClient();
|
|
1001
|
+
this.rioClient = new RioWebsocketClient(token);
|
|
1002
|
+
this.rioUnsubscribe = this.rioClient.onMessage((incoming) => {
|
|
1003
|
+
this.handleIncomingMessage(incoming);
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
698
1006
|
|
|
699
1007
|
return this.rioClient;
|
|
700
1008
|
}
|
|
@@ -706,6 +1014,14 @@ export class RioAssistWidget extends LitElement {
|
|
|
706
1014
|
return;
|
|
707
1015
|
}
|
|
708
1016
|
|
|
1017
|
+
if (this.handleConversationSystemAction(message)) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (this.shouldIgnoreAssistantPayload(message.action)) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
709
1025
|
console.info('[RioAssist][ws] resposta de mensagem recebida', {
|
|
710
1026
|
action: message.action ?? 'message',
|
|
711
1027
|
text: message.text,
|
|
@@ -723,13 +1039,13 @@ export class RioAssistWidget extends LitElement {
|
|
|
723
1039
|
this.requestConversationHistory();
|
|
724
1040
|
}
|
|
725
1041
|
}
|
|
726
|
-
|
|
727
|
-
private teardownRioClient() {
|
|
728
|
-
if (this.rioUnsubscribe) {
|
|
729
|
-
this.rioUnsubscribe();
|
|
730
|
-
this.rioUnsubscribe = null;
|
|
731
|
-
}
|
|
732
|
-
|
|
1042
|
+
|
|
1043
|
+
private teardownRioClient() {
|
|
1044
|
+
if (this.rioUnsubscribe) {
|
|
1045
|
+
this.rioUnsubscribe();
|
|
1046
|
+
this.rioUnsubscribe = null;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
733
1049
|
if (this.rioClient) {
|
|
734
1050
|
this.rioClient.close();
|
|
735
1051
|
this.rioClient = null;
|
|
@@ -746,10 +1062,15 @@ export class RioAssistWidget extends LitElement {
|
|
|
746
1062
|
limit,
|
|
747
1063
|
});
|
|
748
1064
|
|
|
1065
|
+
this.conversationHistoryError = '';
|
|
749
1066
|
this.conversationHistoryLoading = true;
|
|
750
1067
|
await client.requestHistory({ conversationId, limit });
|
|
751
1068
|
} catch (error) {
|
|
752
1069
|
console.error('[RioAssist][history] erro ao solicitar historico', error);
|
|
1070
|
+
this.conversationHistoryError =
|
|
1071
|
+
error instanceof Error && error.message
|
|
1072
|
+
? error.message
|
|
1073
|
+
: 'Nao foi possivel carregar as conversas.';
|
|
753
1074
|
this.conversationHistoryLoading = false;
|
|
754
1075
|
}
|
|
755
1076
|
}
|
|
@@ -813,6 +1134,7 @@ export class RioAssistWidget extends LitElement {
|
|
|
813
1134
|
console.info('[RioAssist][history] payload sem itens para montar lista de conversas');
|
|
814
1135
|
this.conversations = [];
|
|
815
1136
|
this.conversationHistoryLoading = false;
|
|
1137
|
+
this.conversationHistoryError = '';
|
|
816
1138
|
return;
|
|
817
1139
|
}
|
|
818
1140
|
|
|
@@ -853,6 +1175,7 @@ export class RioAssistWidget extends LitElement {
|
|
|
853
1175
|
|
|
854
1176
|
this.conversations = conversations;
|
|
855
1177
|
this.conversationHistoryLoading = false;
|
|
1178
|
+
this.conversationHistoryError = '';
|
|
856
1179
|
this.syncActiveConversationTitle();
|
|
857
1180
|
console.info('[RioAssist][history] conversas normalizadas', conversations);
|
|
858
1181
|
}
|
|
@@ -1203,10 +1526,20 @@ export class RioAssistWidget extends LitElement {
|
|
|
1203
1526
|
|
|
1204
1527
|
private startLoadingGuard() {
|
|
1205
1528
|
this.clearLoadingGuard();
|
|
1206
|
-
this.
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1529
|
+
this.loadingLabelInternal = 'Rio Insight está respondendo';
|
|
1530
|
+
|
|
1531
|
+
// Após 20s, mensagem de processamento prolongado.
|
|
1532
|
+
this.loadingTimerSlow = window.setTimeout(() => {
|
|
1533
|
+
this.loadingLabelInternal = 'Rio Insight continua respondendo';
|
|
1534
|
+
this.requestUpdate();
|
|
1535
|
+
}, 20000);
|
|
1536
|
+
|
|
1537
|
+
// Após 60s, aviso de demora maior.
|
|
1538
|
+
this.loadingTimerTimeout = window.setTimeout(() => {
|
|
1539
|
+
this.loadingLabelInternal =
|
|
1540
|
+
'Essa solicitação está demorando um pouco mais que o esperado. Pode favor, aguarde mais um pouco';
|
|
1541
|
+
this.requestUpdate();
|
|
1542
|
+
}, 60000);
|
|
1210
1543
|
}
|
|
1211
1544
|
|
|
1212
1545
|
private clearLoadingGuard() {
|
|
@@ -1214,6 +1547,16 @@ export class RioAssistWidget extends LitElement {
|
|
|
1214
1547
|
window.clearTimeout(this.loadingTimer);
|
|
1215
1548
|
this.loadingTimer = null;
|
|
1216
1549
|
}
|
|
1550
|
+
|
|
1551
|
+
if (this.loadingTimerSlow !== null) {
|
|
1552
|
+
window.clearTimeout(this.loadingTimerSlow);
|
|
1553
|
+
this.loadingTimerSlow = null;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
if (this.loadingTimerTimeout !== null) {
|
|
1557
|
+
window.clearTimeout(this.loadingTimerTimeout);
|
|
1558
|
+
this.loadingTimerTimeout = null;
|
|
1559
|
+
}
|
|
1217
1560
|
}
|
|
1218
1561
|
|
|
1219
1562
|
private scrollConversationToBottom() {
|
|
@@ -1294,11 +1637,11 @@ export class RioAssistWidget extends LitElement {
|
|
|
1294
1637
|
}
|
|
1295
1638
|
|
|
1296
1639
|
}
|
|
1297
|
-
declare global {
|
|
1298
|
-
interface HTMLElementTagNameMap {
|
|
1299
|
-
'rio-assist-widget': RioAssistWidget;
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1640
|
+
declare global {
|
|
1641
|
+
interface HTMLElementTagNameMap {
|
|
1642
|
+
'rio-assist-widget': RioAssistWidget;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1302
1645
|
|
|
1303
1646
|
if (!customElements.get('rio-assist-widget')) {
|
|
1304
1647
|
customElements.define('rio-assist-widget', RioAssistWidget);
|