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.
@@ -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
- messages: ChatMessage[] = [];
84
-
85
- isLoading = false;
86
-
87
- errorMessage = '';
88
-
89
- showConversations = false;
90
-
91
- conversationSearch = '';
92
-
93
- conversationMenuId: string | null = null;
94
-
95
- conversationMenuPlacement: 'above' | 'below' = 'below';
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
- if (!this.suggestionsSource) {
198
- return [];
199
- }
200
-
201
- return this.suggestionsSource
202
- .split('|')
203
- .map((item) => item.trim())
204
- .filter(Boolean);
205
- }
206
-
207
- protected updated(changedProperties: PropertyValues): void {
208
- super.updated(changedProperties);
209
- this.style.setProperty('--accent-color', this.accentColor);
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 conversation = this.conversations.find((item) => item.id === id);
362
- if (!conversation) {
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
- const message = `${
367
- action === 'rename' ? 'Renomear' : 'Excluir'
368
- } "${conversation.title}"`;
369
- console.info(`[Mock] ${message}`);
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
- exitFullscreen(restorePanel: boolean) {
442
- if (!this.isFullscreen) {
443
- return;
444
- }
445
-
446
- this.isFullscreen = false;
447
- this.conversationMenuId = null;
448
- this.showNewConversationShortcut = false;
449
- if (restorePanel) {
450
- this.open = true;
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.loadingTimer = window.setTimeout(() => {
1207
- this.loadingTimer = null;
1208
- this.isLoading = false;
1209
- }, 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);
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);