rio-assist-widget 0.1.18 → 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.
@@ -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,7 +18,7 @@ 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;
@@ -31,6 +31,13 @@ type ConversationDeleteTarget = {
31
31
  index: number;
32
32
  };
33
33
 
34
+ type ConversationRenameTarget = {
35
+ id: string;
36
+ title: string;
37
+ index: number;
38
+ draft: string;
39
+ };
40
+
34
41
  export type HeaderActionConfig = {
35
42
  id?: string;
36
43
  iconUrl: string;
@@ -39,24 +46,24 @@ export type HeaderActionConfig = {
39
46
  };
40
47
 
41
48
  export class RioAssistWidget extends LitElement {
42
- static styles = widgetStyles;
43
-
44
- static properties = {
45
- open: { type: Boolean, state: true },
46
- message: { type: String, state: true },
47
- titleText: { type: String, attribute: 'data-title' },
48
- buttonLabel: { type: String, attribute: 'data-button-label' },
49
- placeholder: { type: String, attribute: 'data-placeholder' },
50
- accentColor: { type: String, attribute: 'data-accent-color' },
51
- apiBaseUrl: { type: String, attribute: 'data-api-base-url' },
52
- rioToken: { type: String, attribute: 'data-rio-token' },
53
- suggestionsSource: { type: String, attribute: 'data-suggestions' },
54
- messages: { state: true },
55
- isLoading: { type: Boolean, state: true },
56
- errorMessage: { type: String, state: true },
57
- showConversations: { type: Boolean, state: true },
58
- conversationSearch: { type: String, state: true },
59
- 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 },
60
67
  conversationMenuPlacement: { state: true },
61
68
  isFullscreen: { type: Boolean, state: true },
62
69
  conversationScrollbar: { state: true },
@@ -66,42 +73,49 @@ export class RioAssistWidget extends LitElement {
66
73
  activeConversationTitle: { state: true },
67
74
  conversationHistoryError: { type: String, state: true },
68
75
  deleteConversationTarget: { attribute: false },
76
+ renameConversationTarget: { attribute: false },
69
77
  headerActions: { attribute: false },
70
78
  homeUrl: { type: String, attribute: 'data-home-url' },
71
79
  };
72
-
73
- open = false;
74
-
75
- message = '';
76
-
77
- titleText = 'Rio Insight';
78
-
79
- buttonLabel = 'Rio Insight';
80
-
81
- placeholder = 'Pergunte alguma coisa';
82
-
83
- accentColor = '#008B9A';
84
-
85
- apiBaseUrl = '';
86
-
87
- rioToken = '';
88
-
89
- suggestionsSource = '';
90
-
91
- messages: ChatMessage[] = [];
92
-
93
- isLoading = false;
94
-
95
- errorMessage = '';
96
-
97
- showConversations = false;
98
-
99
- conversationSearch = '';
100
-
101
- conversationMenuId: string | null = null;
102
-
103
- conversationMenuPlacement: 'above' | 'below' = 'below';
104
-
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
+
105
119
  isFullscreen = false;
106
120
 
107
121
  showNewConversationShortcut = false;
@@ -118,6 +132,12 @@ export class RioAssistWidget extends LitElement {
118
132
 
119
133
  deleteConversationTarget: ConversationDeleteTarget | null = null;
120
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
+
121
141
  private refreshConversationsAfterResponse = false;
122
142
 
123
143
  activeConversationTitle: string | null = null;
@@ -172,9 +192,9 @@ export class RioAssistWidget extends LitElement {
172
192
  }
173
193
  return result;
174
194
  }
175
-
176
- private conversationScrollbarRaf: number | null = null;
177
-
195
+
196
+ private conversationScrollbarRaf: number | null = null;
197
+
178
198
  private rioClient: RioWebsocketClient | null = null;
179
199
 
180
200
  private rioUnsubscribe: (() => void) | null = null;
@@ -188,7 +208,7 @@ export class RioAssistWidget extends LitElement {
188
208
  private conversationUserId: string | null = null;
189
209
 
190
210
  private conversationScrollbarDraggingId: number | null = null;
191
-
211
+
192
212
  private conversationScrollbarDragState: {
193
213
  startY: number;
194
214
  startThumbTop: number;
@@ -202,24 +222,53 @@ export class RioAssistWidget extends LitElement {
202
222
  linkify: true,
203
223
  breaks: true,
204
224
  }).use(markdownItTaskLists);
205
-
206
- conversations: ConversationItem[] = [];
207
-
208
- get suggestions(): string[] {
209
- if (!this.suggestionsSource) {
210
- return [];
211
- }
212
-
213
- return this.suggestionsSource
214
- .split('|')
215
- .map((item) => item.trim())
216
- .filter(Boolean);
217
- }
218
-
219
- protected updated(changedProperties: PropertyValues): void {
220
- super.updated(changedProperties);
221
- this.style.setProperty('--accent-color', this.accentColor);
222
-
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
+
223
272
  if (
224
273
  changedProperties.has('isFullscreen') ||
225
274
  changedProperties.has('showConversations') ||
@@ -237,59 +286,59 @@ export class RioAssistWidget extends LitElement {
237
286
  this.scrollConversationToBottom();
238
287
  }
239
288
  }
240
-
241
- protected firstUpdated(): void {
242
- this.enqueueConversationScrollbarMeasure();
243
- }
244
-
245
- disconnectedCallback(): void {
246
- super.disconnectedCallback();
247
- if (this.conversationScrollbarRaf !== null) {
248
- cancelAnimationFrame(this.conversationScrollbarRaf);
249
- this.conversationScrollbarRaf = null;
250
- }
251
-
252
- this.teardownRioClient();
253
- this.clearLoadingGuard();
254
- }
255
-
256
- get filteredConversations() {
257
- const query = this.conversationSearch.trim().toLowerCase();
258
- if (!query) {
259
- return this.conversations;
260
- }
261
-
262
- return this.conversations.filter((conversation) =>
263
- conversation.title.toLowerCase().includes(query),
264
- );
265
- }
266
-
267
- get hasActiveConversation() {
268
- return this.messages.length > 0;
269
- }
270
-
271
- togglePanel() {
272
- if (this.isFullscreen) {
273
- this.exitFullscreen(false);
274
- return;
275
- }
276
-
277
- this.open = !this.open;
278
- this.dispatchEvent(
279
- new CustomEvent(this.open ? 'rioassist:open' : 'rioassist:close', {
280
- bubbles: true,
281
- composed: true,
282
- }),
283
- );
284
- }
285
-
286
- closePanel() {
287
- this.isFullscreen = false;
288
- if (this.open) {
289
- this.togglePanel();
290
- }
291
- }
292
-
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
+
293
342
  openConversationsPanel() {
294
343
  this.showConversations = true;
295
344
  this.requestConversationHistory();
@@ -309,7 +358,7 @@ export class RioAssistWidget extends LitElement {
309
358
 
310
359
  this.requestConversationHistory();
311
360
  }
312
-
361
+
313
362
  toggleNewConversationShortcut() {
314
363
  this.showNewConversationShortcut = !this.showNewConversationShortcut;
315
364
  }
@@ -332,41 +381,41 @@ export class RioAssistWidget extends LitElement {
332
381
  handleConversationSearch(event: InputEvent) {
333
382
  this.conversationSearch = (event.target as HTMLInputElement).value;
334
383
  }
335
-
336
- handleConversationMenuToggle(event: Event, id: string) {
337
- event.stopPropagation();
338
-
339
- if (this.conversationMenuId === id) {
340
- this.conversationMenuId = null;
341
- return;
342
- }
343
-
344
- const button = event.currentTarget as HTMLElement;
345
- const container = this.renderRoot.querySelector(
346
- '.conversations-panel__surface',
347
- ) as HTMLElement | null;
348
-
349
- if (button && container) {
350
- const buttonRect = button.getBoundingClientRect();
351
- const containerRect = container.getBoundingClientRect();
352
- const spaceBelow = containerRect.bottom - buttonRect.bottom;
353
- this.conversationMenuPlacement = spaceBelow < 140 ? 'above' : 'below';
354
- } else {
355
- this.conversationMenuPlacement = 'below';
356
- }
357
-
358
- this.conversationMenuId = id;
359
- }
360
-
361
- handleConversationsPanelPointer(event: PointerEvent) {
362
- const target = event.target as HTMLElement;
363
- if (
364
- !target.closest('.conversation-menu') &&
365
- !target.closest('.conversation-menu-button')
366
- ) {
367
- this.conversationMenuId = null;
368
- }
369
- }
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
+ }
370
419
 
371
420
  handleConversationAction(action: 'rename' | 'delete', id: string) {
372
421
  this.conversationMenuId = null;
@@ -385,7 +434,12 @@ export class RioAssistWidget extends LitElement {
385
434
  return;
386
435
  }
387
436
 
388
- this.dispatchConversationAction('rename', conversation, conversationIndex);
437
+ this.renameConversationTarget = {
438
+ id: conversation.id,
439
+ title: conversation.title,
440
+ index: conversationIndex,
441
+ draft: conversation.title,
442
+ };
389
443
  }
390
444
 
391
445
  handleHomeNavigation() {
@@ -452,24 +506,68 @@ export class RioAssistWidget extends LitElement {
452
506
  }
453
507
  }
454
508
 
455
- confirmDeleteConversation() {
509
+ async confirmDeleteConversation() {
456
510
  const target = this.deleteConversationTarget;
457
511
  if (!target) {
458
512
  return;
459
513
  }
460
514
 
461
- this.dispatchConversationAction('delete', { id: target.id, title: target.title }, target.index);
462
- this.deleteConversationTarget = null;
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
+ }
463
523
  }
464
524
 
465
525
  cancelDeleteConversation() {
466
526
  this.deleteConversationTarget = null;
467
527
  }
468
528
 
469
- private dispatchConversationAction(
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(
470
567
  action: 'rename' | 'delete',
471
568
  conversation: Pick<ConversationItem, 'id' | 'title'>,
472
569
  index: number,
570
+ newTitle?: string,
473
571
  ) {
474
572
  const eventName =
475
573
  action === 'rename' ? 'rioassist:conversation-rename' : 'rioassist:conversation-delete';
@@ -490,12 +588,112 @@ export class RioAssistWidget extends LitElement {
490
588
  );
491
589
 
492
590
  if (!allowed) {
493
- return;
591
+ return false;
494
592
  }
495
593
 
496
594
  if (action === 'delete') {
497
- this.applyConversationDeletion(conversation.id);
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;
498
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;
499
697
  }
500
698
 
501
699
  handleHeaderActionClick(action: HeaderActionConfig, index: number) {
@@ -529,14 +727,14 @@ export class RioAssistWidget extends LitElement {
529
727
  this.exitFullscreen(true);
530
728
  return;
531
729
  }
532
-
533
- if (this.showConversations) {
534
- this.closeConversationsPanel();
535
- } else {
536
- this.closePanel();
537
- }
538
- }
539
-
730
+
731
+ if (this.showConversations) {
732
+ this.closeConversationsPanel();
733
+ } else {
734
+ this.closePanel();
735
+ }
736
+ }
737
+
540
738
  enterFullscreen() {
541
739
  if (this.isFullscreen) {
542
740
  return;
@@ -547,20 +745,20 @@ export class RioAssistWidget extends LitElement {
547
745
  this.showConversations = false;
548
746
  this.requestConversationHistory();
549
747
  }
550
-
551
- exitFullscreen(restorePanel: boolean) {
552
- if (!this.isFullscreen) {
553
- return;
554
- }
555
-
556
- this.isFullscreen = false;
557
- this.conversationMenuId = null;
558
- this.showNewConversationShortcut = false;
559
- if (restorePanel) {
560
- this.open = true;
561
- }
562
- }
563
-
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
+
564
762
  handleCreateConversation() {
565
763
  if (!this.hasActiveConversation) {
566
764
  return;
@@ -580,132 +778,132 @@ export class RioAssistWidget extends LitElement {
580
778
  new CustomEvent('rioassist:new-conversation', {
581
779
  bubbles: true,
582
780
  composed: true,
583
- }),
584
- );
585
- }
586
-
587
- handleConversationListScroll(event: Event) {
588
- const target = event.currentTarget as HTMLElement | null;
589
- if (!target) {
590
- return;
591
- }
592
- this.updateConversationScrollbar(target);
593
- }
594
-
595
- handleConversationScrollbarPointerDown(event: PointerEvent) {
596
- const track = event.currentTarget as HTMLElement | null;
597
- const list = this.renderRoot.querySelector(
598
- '.conversation-list--sidebar',
599
- ) as HTMLElement | null;
600
-
601
- if (!track || !list) {
602
- return;
603
- }
604
-
605
- const trackRect = track.getBoundingClientRect();
606
- const thumbHeight = trackRect.height * (this.conversationScrollbar.height / 100);
607
- const maxThumbTop = Math.max(trackRect.height - thumbHeight, 0);
608
- const scrollRange = Math.max(list.scrollHeight - list.clientHeight, 1);
609
- const currentThumbTop = (list.scrollTop / scrollRange) * maxThumbTop;
610
- const offsetY = event.clientY - trackRect.top;
611
- const isOnThumb = offsetY >= currentThumbTop && offsetY <= currentThumbTop + thumbHeight;
612
-
613
- const nextThumbTop = isOnThumb
614
- ? currentThumbTop
615
- : Math.min(Math.max(offsetY - thumbHeight / 2, 0), maxThumbTop);
616
-
617
- if (!isOnThumb) {
618
- list.scrollTop = (nextThumbTop / Math.max(maxThumbTop, 1)) * (list.scrollHeight - list.clientHeight);
619
- this.updateConversationScrollbar(list);
620
- }
621
-
622
- track.setPointerCapture(event.pointerId);
623
- this.conversationScrollbarDraggingId = event.pointerId;
624
- this.conversationScrollbarDragState = {
625
- startY: event.clientY,
626
- startThumbTop: nextThumbTop,
627
- trackHeight: trackRect.height,
628
- thumbHeight,
629
- list,
630
- };
631
- event.preventDefault();
632
- }
633
-
634
- handleConversationScrollbarPointerMove(event: PointerEvent) {
635
- if (
636
- this.conversationScrollbarDraggingId === null ||
637
- this.conversationScrollbarDraggingId !== event.pointerId ||
638
- !this.conversationScrollbarDragState
639
- ) {
640
- return;
641
- }
642
-
643
- const {
644
- startY,
645
- startThumbTop,
646
- trackHeight,
647
- thumbHeight,
648
- list,
649
- } = this.conversationScrollbarDragState;
650
-
651
- const maxThumbTop = Math.max(trackHeight - thumbHeight, 0);
652
- const deltaY = event.clientY - startY;
653
- const thumbTop = Math.min(Math.max(startThumbTop + deltaY, 0), maxThumbTop);
654
- const scrollRange = list.scrollHeight - list.clientHeight;
655
-
656
- if (scrollRange > 0) {
657
- list.scrollTop = (thumbTop / Math.max(maxThumbTop, 1)) * scrollRange;
658
- this.updateConversationScrollbar(list);
659
- }
660
-
661
- event.preventDefault();
662
- }
663
-
664
- handleConversationScrollbarPointerUp(event: PointerEvent) {
665
- if (this.conversationScrollbarDraggingId !== event.pointerId) {
666
- return;
667
- }
668
-
669
- const track = event.currentTarget as HTMLElement | null;
670
- track?.releasePointerCapture(event.pointerId);
671
-
672
- this.conversationScrollbarDraggingId = null;
673
- this.conversationScrollbarDragState = null;
674
- }
675
-
676
- private enqueueConversationScrollbarMeasure() {
677
- if (this.conversationScrollbarRaf !== null) {
678
- return;
679
- }
680
-
681
- this.conversationScrollbarRaf = requestAnimationFrame(() => {
682
- this.conversationScrollbarRaf = null;
683
- this.updateConversationScrollbar();
684
- });
685
- }
686
-
687
- private updateConversationScrollbar(target?: HTMLElement | null) {
688
- const element =
689
- target ??
690
- (this.renderRoot.querySelector(
691
- '.conversation-list--sidebar',
692
- ) as HTMLElement | null);
693
-
694
- if (!element) {
695
- if (this.conversationScrollbar.visible) {
696
- this.conversationScrollbar = { height: 0, top: 0, visible: false };
697
- }
698
- return;
699
- }
700
-
701
- const { scrollHeight, clientHeight, scrollTop } = element;
702
- if (scrollHeight <= clientHeight + 1) {
703
- if (this.conversationScrollbar.visible) {
704
- this.conversationScrollbar = { height: 0, top: 0, visible: false };
705
- }
706
- return;
707
- }
708
-
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
+
709
907
  const ratio = clientHeight / scrollHeight;
710
908
  const height = Math.max(ratio * 100, 8);
711
909
  const maxTop = 100 - height;
@@ -722,7 +920,7 @@ export class RioAssistWidget extends LitElement {
722
920
  async onSuggestionClick(suggestion: string) {
723
921
  await this.processMessage(suggestion);
724
922
  }
725
-
923
+
726
924
  async handleSubmit(event: SubmitEvent) {
727
925
  event.preventDefault();
728
926
  await this.processMessage(this.message);
@@ -760,12 +958,12 @@ export class RioAssistWidget extends LitElement {
760
958
  detail: {
761
959
  message: content,
762
960
  apiBaseUrl: this.apiBaseUrl,
763
- token: this.rioToken,
764
- },
765
- bubbles: true,
766
- composed: true,
767
- }),
768
- );
961
+ token: this.rioToken,
962
+ },
963
+ bubbles: true,
964
+ composed: true,
965
+ }),
966
+ );
769
967
 
770
968
  const userMessage = this.createMessage('user', content);
771
969
  this.messages = [...this.messages, userMessage];
@@ -777,7 +975,7 @@ export class RioAssistWidget extends LitElement {
777
975
  this.errorMessage = '';
778
976
  this.isLoading = true;
779
977
  this.startLoadingGuard();
780
-
978
+
781
979
  try {
782
980
  const client = this.ensureRioClient();
783
981
  await client.sendMessage(content, this.currentConversationId);
@@ -786,25 +984,25 @@ export class RioAssistWidget extends LitElement {
786
984
  this.isLoading = false;
787
985
  this.errorMessage = error instanceof Error
788
986
  ? error.message
789
- : 'Nao foi possivel enviar a mensagem para o agente.';
790
- }
791
- }
792
-
987
+ : 'Nao foi possivel enviar a mensagem para o agente.';
988
+ }
989
+ }
990
+
793
991
  private ensureRioClient() {
794
992
  const token = this.rioToken.trim();
795
993
  if (!token) {
796
994
  throw new Error(
797
995
  'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
798
- );
799
- }
800
-
801
- if (!this.rioClient || !this.rioClient.matchesToken(token)) {
802
- this.teardownRioClient();
803
- this.rioClient = new RioWebsocketClient(token);
804
- this.rioUnsubscribe = this.rioClient.onMessage((incoming) => {
805
- this.handleIncomingMessage(incoming);
806
- });
807
- }
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
+ }
808
1006
 
809
1007
  return this.rioClient;
810
1008
  }
@@ -816,6 +1014,14 @@ export class RioAssistWidget extends LitElement {
816
1014
  return;
817
1015
  }
818
1016
 
1017
+ if (this.handleConversationSystemAction(message)) {
1018
+ return;
1019
+ }
1020
+
1021
+ if (this.shouldIgnoreAssistantPayload(message.action)) {
1022
+ return;
1023
+ }
1024
+
819
1025
  console.info('[RioAssist][ws] resposta de mensagem recebida', {
820
1026
  action: message.action ?? 'message',
821
1027
  text: message.text,
@@ -833,13 +1039,13 @@ export class RioAssistWidget extends LitElement {
833
1039
  this.requestConversationHistory();
834
1040
  }
835
1041
  }
836
-
837
- private teardownRioClient() {
838
- if (this.rioUnsubscribe) {
839
- this.rioUnsubscribe();
840
- this.rioUnsubscribe = null;
841
- }
842
-
1042
+
1043
+ private teardownRioClient() {
1044
+ if (this.rioUnsubscribe) {
1045
+ this.rioUnsubscribe();
1046
+ this.rioUnsubscribe = null;
1047
+ }
1048
+
843
1049
  if (this.rioClient) {
844
1050
  this.rioClient.close();
845
1051
  this.rioClient = null;
@@ -1320,10 +1526,20 @@ export class RioAssistWidget extends LitElement {
1320
1526
 
1321
1527
  private startLoadingGuard() {
1322
1528
  this.clearLoadingGuard();
1323
- this.loadingTimer = window.setTimeout(() => {
1324
- this.loadingTimer = null;
1325
- this.isLoading = false;
1326
- }, 15000);
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);
1327
1543
  }
1328
1544
 
1329
1545
  private clearLoadingGuard() {
@@ -1331,6 +1547,16 @@ export class RioAssistWidget extends LitElement {
1331
1547
  window.clearTimeout(this.loadingTimer);
1332
1548
  this.loadingTimer = null;
1333
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
+ }
1334
1560
  }
1335
1561
 
1336
1562
  private scrollConversationToBottom() {
@@ -1411,11 +1637,11 @@ export class RioAssistWidget extends LitElement {
1411
1637
  }
1412
1638
 
1413
1639
  }
1414
- declare global {
1415
- interface HTMLElementTagNameMap {
1416
- 'rio-assist-widget': RioAssistWidget;
1417
- }
1418
- }
1640
+ declare global {
1641
+ interface HTMLElementTagNameMap {
1642
+ 'rio-assist-widget': RioAssistWidget;
1643
+ }
1644
+ }
1419
1645
 
1420
1646
  if (!customElements.get('rio-assist-widget')) {
1421
1647
  customElements.define('rio-assist-widget', RioAssistWidget);