rio-assist-widget 0.1.6 → 0.1.7

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.
@@ -39,12 +39,12 @@ export class RioAssistWidget extends LitElement {
39
39
  errorMessage: { type: String, state: true },
40
40
  showConversations: { type: Boolean, state: true },
41
41
  conversationSearch: { type: String, state: true },
42
- conversationMenuId: { state: true },
43
- conversationMenuPlacement: { state: true },
44
- isFullscreen: { type: Boolean, state: true },
45
- conversationScrollbar: { state: true },
46
- showNewConversationShortcut: { type: Boolean, state: true },
47
- };
42
+ conversationMenuId: { state: true },
43
+ conversationMenuPlacement: { state: true },
44
+ isFullscreen: { type: Boolean, state: true },
45
+ conversationScrollbar: { state: true },
46
+ showNewConversationShortcut: { type: Boolean, state: true },
47
+ };
48
48
 
49
49
  open = false;
50
50
 
@@ -74,37 +74,37 @@ export class RioAssistWidget extends LitElement {
74
74
 
75
75
  conversationSearch = '';
76
76
 
77
- conversationMenuId: string | null = null;
78
-
79
- conversationMenuPlacement: 'above' | 'below' = 'below';
80
-
81
- isFullscreen = false;
82
-
83
- showNewConversationShortcut = false;
84
-
85
- conversationScrollbar = {
86
- height: 0,
87
- top: 0,
88
- visible: false,
89
- };
90
-
91
- private conversationScrollbarRaf: number | null = null;
92
-
93
- private rioClient: RioWebsocketClient | null = null;
94
-
95
- private rioUnsubscribe: (() => void) | null = null;
96
-
97
- private loadingTimer: number | null = null;
98
-
99
- private conversationScrollbarDraggingId: number | null = null;
100
-
101
- private conversationScrollbarDragState: {
102
- startY: number;
103
- startThumbTop: number;
104
- trackHeight: number;
105
- thumbHeight: number;
106
- list: HTMLElement;
107
- } | null = null;
77
+ conversationMenuId: string | null = null;
78
+
79
+ conversationMenuPlacement: 'above' | 'below' = 'below';
80
+
81
+ isFullscreen = false;
82
+
83
+ showNewConversationShortcut = false;
84
+
85
+ conversationScrollbar = {
86
+ height: 0,
87
+ top: 0,
88
+ visible: false,
89
+ };
90
+
91
+ private conversationScrollbarRaf: number | null = null;
92
+
93
+ private rioClient: RioWebsocketClient | null = null;
94
+
95
+ private rioUnsubscribe: (() => void) | null = null;
96
+
97
+ private loadingTimer: number | null = null;
98
+
99
+ private conversationScrollbarDraggingId: number | null = null;
100
+
101
+ private conversationScrollbarDragState: {
102
+ startY: number;
103
+ startThumbTop: number;
104
+ trackHeight: number;
105
+ thumbHeight: number;
106
+ list: HTMLElement;
107
+ } | null = null;
108
108
 
109
109
  conversations: ConversationItem[] = Array.from({ length: 20 }).map(
110
110
  (_, index) => ({
@@ -141,14 +141,23 @@ export class RioAssistWidget extends LitElement {
141
141
  super.updated(changedProperties);
142
142
  this.style.setProperty('--accent-color', this.accentColor);
143
143
 
144
- if (
145
- changedProperties.has('isFullscreen') ||
146
- changedProperties.has('showConversations') ||
147
- changedProperties.has('conversations')
148
- ) {
149
- this.enqueueConversationScrollbarMeasure();
150
- }
151
- }
144
+ if (
145
+ changedProperties.has('isFullscreen') ||
146
+ changedProperties.has('showConversations') ||
147
+ changedProperties.has('conversations')
148
+ ) {
149
+ this.enqueueConversationScrollbarMeasure();
150
+ }
151
+
152
+ if (
153
+ changedProperties.has('messages') ||
154
+ (changedProperties.has('isLoading') && this.isLoading) ||
155
+ (changedProperties.has('open') && this.open) ||
156
+ (changedProperties.has('isFullscreen') && this.isFullscreen)
157
+ ) {
158
+ this.scrollConversationToBottom();
159
+ }
160
+ }
152
161
 
153
162
  protected firstUpdated(): void {
154
163
  this.enqueueConversationScrollbarMeasure();
@@ -165,21 +174,21 @@ export class RioAssistWidget extends LitElement {
165
174
  this.clearLoadingGuard();
166
175
  }
167
176
 
168
- get filteredConversations() {
169
- const query = this.conversationSearch.trim().toLowerCase();
170
- if (!query) {
171
- return this.conversations;
172
- }
177
+ get filteredConversations() {
178
+ const query = this.conversationSearch.trim().toLowerCase();
179
+ if (!query) {
180
+ return this.conversations;
181
+ }
182
+
183
+ return this.conversations.filter((conversation) =>
184
+ conversation.title.toLowerCase().includes(query),
185
+ );
186
+ }
187
+
188
+ get hasActiveConversation() {
189
+ return this.messages.length > 0;
190
+ }
173
191
 
174
- return this.conversations.filter((conversation) =>
175
- conversation.title.toLowerCase().includes(query),
176
- );
177
- }
178
-
179
- get hasActiveConversation() {
180
- return this.messages.length > 0;
181
- }
182
-
183
192
  togglePanel() {
184
193
  if (this.isFullscreen) {
185
194
  this.exitFullscreen(false);
@@ -211,20 +220,20 @@ export class RioAssistWidget extends LitElement {
211
220
  this.conversationMenuId = null;
212
221
  }
213
222
 
214
- toggleConversationsPanel() {
215
- this.showConversations = !this.showConversations;
216
- if (!this.showConversations) {
217
- this.conversationMenuId = null;
218
- }
219
- }
220
-
221
- toggleNewConversationShortcut() {
222
- this.showNewConversationShortcut = !this.showNewConversationShortcut;
223
- }
224
-
225
- handleConversationSearch(event: InputEvent) {
226
- this.conversationSearch = (event.target as HTMLInputElement).value;
227
- }
223
+ toggleConversationsPanel() {
224
+ this.showConversations = !this.showConversations;
225
+ if (!this.showConversations) {
226
+ this.conversationMenuId = null;
227
+ }
228
+ }
229
+
230
+ toggleNewConversationShortcut() {
231
+ this.showNewConversationShortcut = !this.showNewConversationShortcut;
232
+ }
233
+
234
+ handleConversationSearch(event: InputEvent) {
235
+ this.conversationSearch = (event.target as HTMLInputElement).value;
236
+ }
228
237
 
229
238
  handleConversationMenuToggle(event: Event, id: string) {
230
239
  event.stopPropagation();
@@ -297,134 +306,134 @@ export class RioAssistWidget extends LitElement {
297
306
  this.showConversations = false;
298
307
  }
299
308
 
300
- exitFullscreen(restorePanel: boolean) {
301
- if (!this.isFullscreen) {
302
- return;
303
- }
304
-
305
- this.isFullscreen = false;
306
- this.conversationMenuId = null;
307
- this.showNewConversationShortcut = false;
308
- if (restorePanel) {
309
- this.open = true;
310
- }
311
- }
309
+ exitFullscreen(restorePanel: boolean) {
310
+ if (!this.isFullscreen) {
311
+ return;
312
+ }
312
313
 
313
- handleCreateConversation() {
314
- if (!this.hasActiveConversation) {
315
- return;
316
- }
317
-
318
- this.clearLoadingGuard();
319
- this.isLoading = false;
320
- this.messages = [];
321
- this.message = '';
322
- this.errorMessage = '';
323
- this.showConversations = false;
324
- this.teardownRioClient();
325
- this.showNewConversationShortcut = false;
326
- this.dispatchEvent(
327
- new CustomEvent('rioassist:new-conversation', {
328
- bubbles: true,
329
- composed: true,
330
- }),
331
- );
332
- }
314
+ this.isFullscreen = false;
315
+ this.conversationMenuId = null;
316
+ this.showNewConversationShortcut = false;
317
+ if (restorePanel) {
318
+ this.open = true;
319
+ }
320
+ }
321
+
322
+ handleCreateConversation() {
323
+ if (!this.hasActiveConversation) {
324
+ return;
325
+ }
326
+
327
+ this.clearLoadingGuard();
328
+ this.isLoading = false;
329
+ this.messages = [];
330
+ this.message = '';
331
+ this.errorMessage = '';
332
+ this.showConversations = false;
333
+ this.teardownRioClient();
334
+ this.showNewConversationShortcut = false;
335
+ this.dispatchEvent(
336
+ new CustomEvent('rioassist:new-conversation', {
337
+ bubbles: true,
338
+ composed: true,
339
+ }),
340
+ );
341
+ }
342
+
343
+ handleConversationListScroll(event: Event) {
344
+ const target = event.currentTarget as HTMLElement | null;
345
+ if (!target) {
346
+ return;
347
+ }
348
+ this.updateConversationScrollbar(target);
349
+ }
350
+
351
+ handleConversationScrollbarPointerDown(event: PointerEvent) {
352
+ const track = event.currentTarget as HTMLElement | null;
353
+ const list = this.renderRoot.querySelector(
354
+ '.conversation-list--sidebar',
355
+ ) as HTMLElement | null;
356
+
357
+ if (!track || !list) {
358
+ return;
359
+ }
360
+
361
+ const trackRect = track.getBoundingClientRect();
362
+ const thumbHeight = trackRect.height * (this.conversationScrollbar.height / 100);
363
+ const maxThumbTop = Math.max(trackRect.height - thumbHeight, 0);
364
+ const scrollRange = Math.max(list.scrollHeight - list.clientHeight, 1);
365
+ const currentThumbTop = (list.scrollTop / scrollRange) * maxThumbTop;
366
+ const offsetY = event.clientY - trackRect.top;
367
+ const isOnThumb = offsetY >= currentThumbTop && offsetY <= currentThumbTop + thumbHeight;
368
+
369
+ const nextThumbTop = isOnThumb
370
+ ? currentThumbTop
371
+ : Math.min(Math.max(offsetY - thumbHeight / 2, 0), maxThumbTop);
372
+
373
+ if (!isOnThumb) {
374
+ list.scrollTop = (nextThumbTop / Math.max(maxThumbTop, 1)) * (list.scrollHeight - list.clientHeight);
375
+ this.updateConversationScrollbar(list);
376
+ }
377
+
378
+ track.setPointerCapture(event.pointerId);
379
+ this.conversationScrollbarDraggingId = event.pointerId;
380
+ this.conversationScrollbarDragState = {
381
+ startY: event.clientY,
382
+ startThumbTop: nextThumbTop,
383
+ trackHeight: trackRect.height,
384
+ thumbHeight,
385
+ list,
386
+ };
387
+ event.preventDefault();
388
+ }
389
+
390
+ handleConversationScrollbarPointerMove(event: PointerEvent) {
391
+ if (
392
+ this.conversationScrollbarDraggingId === null ||
393
+ this.conversationScrollbarDraggingId !== event.pointerId ||
394
+ !this.conversationScrollbarDragState
395
+ ) {
396
+ return;
397
+ }
398
+
399
+ const {
400
+ startY,
401
+ startThumbTop,
402
+ trackHeight,
403
+ thumbHeight,
404
+ list,
405
+ } = this.conversationScrollbarDragState;
406
+
407
+ const maxThumbTop = Math.max(trackHeight - thumbHeight, 0);
408
+ const deltaY = event.clientY - startY;
409
+ const thumbTop = Math.min(Math.max(startThumbTop + deltaY, 0), maxThumbTop);
410
+ const scrollRange = list.scrollHeight - list.clientHeight;
411
+
412
+ if (scrollRange > 0) {
413
+ list.scrollTop = (thumbTop / Math.max(maxThumbTop, 1)) * scrollRange;
414
+ this.updateConversationScrollbar(list);
415
+ }
416
+
417
+ event.preventDefault();
418
+ }
419
+
420
+ handleConversationScrollbarPointerUp(event: PointerEvent) {
421
+ if (this.conversationScrollbarDraggingId !== event.pointerId) {
422
+ return;
423
+ }
424
+
425
+ const track = event.currentTarget as HTMLElement | null;
426
+ track?.releasePointerCapture(event.pointerId);
427
+
428
+ this.conversationScrollbarDraggingId = null;
429
+ this.conversationScrollbarDragState = null;
430
+ }
431
+
432
+ private enqueueConversationScrollbarMeasure() {
433
+ if (this.conversationScrollbarRaf !== null) {
434
+ return;
435
+ }
333
436
 
334
- handleConversationListScroll(event: Event) {
335
- const target = event.currentTarget as HTMLElement | null;
336
- if (!target) {
337
- return;
338
- }
339
- this.updateConversationScrollbar(target);
340
- }
341
-
342
- handleConversationScrollbarPointerDown(event: PointerEvent) {
343
- const track = event.currentTarget as HTMLElement | null;
344
- const list = this.renderRoot.querySelector(
345
- '.conversation-list--sidebar',
346
- ) as HTMLElement | null;
347
-
348
- if (!track || !list) {
349
- return;
350
- }
351
-
352
- const trackRect = track.getBoundingClientRect();
353
- const thumbHeight = trackRect.height * (this.conversationScrollbar.height / 100);
354
- const maxThumbTop = Math.max(trackRect.height - thumbHeight, 0);
355
- const scrollRange = Math.max(list.scrollHeight - list.clientHeight, 1);
356
- const currentThumbTop = (list.scrollTop / scrollRange) * maxThumbTop;
357
- const offsetY = event.clientY - trackRect.top;
358
- const isOnThumb = offsetY >= currentThumbTop && offsetY <= currentThumbTop + thumbHeight;
359
-
360
- const nextThumbTop = isOnThumb
361
- ? currentThumbTop
362
- : Math.min(Math.max(offsetY - thumbHeight / 2, 0), maxThumbTop);
363
-
364
- if (!isOnThumb) {
365
- list.scrollTop = (nextThumbTop / Math.max(maxThumbTop, 1)) * (list.scrollHeight - list.clientHeight);
366
- this.updateConversationScrollbar(list);
367
- }
368
-
369
- track.setPointerCapture(event.pointerId);
370
- this.conversationScrollbarDraggingId = event.pointerId;
371
- this.conversationScrollbarDragState = {
372
- startY: event.clientY,
373
- startThumbTop: nextThumbTop,
374
- trackHeight: trackRect.height,
375
- thumbHeight,
376
- list,
377
- };
378
- event.preventDefault();
379
- }
380
-
381
- handleConversationScrollbarPointerMove(event: PointerEvent) {
382
- if (
383
- this.conversationScrollbarDraggingId === null ||
384
- this.conversationScrollbarDraggingId !== event.pointerId ||
385
- !this.conversationScrollbarDragState
386
- ) {
387
- return;
388
- }
389
-
390
- const {
391
- startY,
392
- startThumbTop,
393
- trackHeight,
394
- thumbHeight,
395
- list,
396
- } = this.conversationScrollbarDragState;
397
-
398
- const maxThumbTop = Math.max(trackHeight - thumbHeight, 0);
399
- const deltaY = event.clientY - startY;
400
- const thumbTop = Math.min(Math.max(startThumbTop + deltaY, 0), maxThumbTop);
401
- const scrollRange = list.scrollHeight - list.clientHeight;
402
-
403
- if (scrollRange > 0) {
404
- list.scrollTop = (thumbTop / Math.max(maxThumbTop, 1)) * scrollRange;
405
- this.updateConversationScrollbar(list);
406
- }
407
-
408
- event.preventDefault();
409
- }
410
-
411
- handleConversationScrollbarPointerUp(event: PointerEvent) {
412
- if (this.conversationScrollbarDraggingId !== event.pointerId) {
413
- return;
414
- }
415
-
416
- const track = event.currentTarget as HTMLElement | null;
417
- track?.releasePointerCapture(event.pointerId);
418
-
419
- this.conversationScrollbarDraggingId = null;
420
- this.conversationScrollbarDragState = null;
421
- }
422
-
423
- private enqueueConversationScrollbarMeasure() {
424
- if (this.conversationScrollbarRaf !== null) {
425
- return;
426
- }
427
-
428
437
  this.conversationScrollbarRaf = requestAnimationFrame(() => {
429
438
  this.conversationScrollbarRaf = null;
430
439
  this.updateConversationScrollbar();
@@ -453,22 +462,22 @@ export class RioAssistWidget extends LitElement {
453
462
  return;
454
463
  }
455
464
 
456
- const ratio = clientHeight / scrollHeight;
457
- const height = Math.max(ratio * 100, 8);
458
- const maxTop = 100 - height;
459
- const top =
460
- scrollTop / (scrollHeight - clientHeight) * (maxTop > 0 ? maxTop : 0);
461
-
462
- this.conversationScrollbar = {
463
- height,
464
- top,
465
- visible: true,
466
- };
467
- }
468
-
469
- async onSuggestionClick(suggestion: string) {
470
- await this.processMessage(suggestion);
471
- }
465
+ const ratio = clientHeight / scrollHeight;
466
+ const height = Math.max(ratio * 100, 8);
467
+ const maxTop = 100 - height;
468
+ const top =
469
+ scrollTop / (scrollHeight - clientHeight) * (maxTop > 0 ? maxTop : 0);
470
+
471
+ this.conversationScrollbar = {
472
+ height,
473
+ top,
474
+ visible: true,
475
+ };
476
+ }
477
+
478
+ async onSuggestionClick(suggestion: string) {
479
+ await this.processMessage(suggestion);
480
+ }
472
481
 
473
482
  async handleSubmit(event: SubmitEvent) {
474
483
  event.preventDefault();
@@ -488,30 +497,35 @@ export class RioAssistWidget extends LitElement {
488
497
  };
489
498
  }
490
499
 
491
- private async processMessage(rawValue: string) {
492
- const content = rawValue.trim();
493
- if (!content || this.isLoading) {
494
- return;
495
- }
496
-
497
- this.dispatchEvent(
498
- new CustomEvent('rioassist:send', {
499
- detail: {
500
- message: content,
501
- apiBaseUrl: this.apiBaseUrl,
500
+ private async processMessage(rawValue: string) {
501
+ const content = rawValue.trim();
502
+ if (!content || this.isLoading) {
503
+ return;
504
+ }
505
+
506
+ const wasEmptyConversation = this.messages.length === 0;
507
+
508
+ this.dispatchEvent(
509
+ new CustomEvent('rioassist:send', {
510
+ detail: {
511
+ message: content,
512
+ apiBaseUrl: this.apiBaseUrl,
502
513
  token: this.rioToken,
503
514
  },
504
515
  bubbles: true,
505
516
  composed: true,
506
517
  }),
507
518
  );
508
-
509
- const userMessage = this.createMessage('user', content);
510
- this.messages = [...this.messages, userMessage];
511
- this.message = '';
512
- this.errorMessage = '';
513
- this.isLoading = true;
514
- this.startLoadingGuard();
519
+
520
+ const userMessage = this.createMessage('user', content);
521
+ this.messages = [...this.messages, userMessage];
522
+ if (wasEmptyConversation) {
523
+ this.showNewConversationShortcut = true;
524
+ }
525
+ this.message = '';
526
+ this.errorMessage = '';
527
+ this.isLoading = true;
528
+ this.startLoadingGuard();
515
529
 
516
530
  try {
517
531
  const client = this.ensureRioClient();
@@ -525,11 +539,11 @@ export class RioAssistWidget extends LitElement {
525
539
  }
526
540
  }
527
541
 
528
- private ensureRioClient() {
529
- const token = this.rioToken.trim();
530
- if (!token) {
531
- throw new Error(
532
- 'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
542
+ private ensureRioClient() {
543
+ const token = this.rioToken.trim();
544
+ if (!token) {
545
+ throw new Error(
546
+ 'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
533
547
  );
534
548
  }
535
549
 
@@ -566,23 +580,35 @@ export class RioAssistWidget extends LitElement {
566
580
  private startLoadingGuard() {
567
581
  this.clearLoadingGuard();
568
582
  this.loadingTimer = window.setTimeout(() => {
569
- this.loadingTimer = null;
570
- this.isLoading = false;
571
- }, 15000);
572
- }
573
-
574
- private clearLoadingGuard() {
575
- if (this.loadingTimer !== null) {
576
- window.clearTimeout(this.loadingTimer);
577
- this.loadingTimer = null;
578
- }
579
- }
580
-
581
- render() {
582
- return renderRioAssist(this);
583
- }
584
-
585
- }
583
+ this.loadingTimer = null;
584
+ this.isLoading = false;
585
+ }, 15000);
586
+ }
587
+
588
+ private clearLoadingGuard() {
589
+ if (this.loadingTimer !== null) {
590
+ window.clearTimeout(this.loadingTimer);
591
+ this.loadingTimer = null;
592
+ }
593
+ }
594
+
595
+ private scrollConversationToBottom() {
596
+ const containers = Array.from(
597
+ this.renderRoot.querySelectorAll('.panel-content'),
598
+ ) as HTMLElement[];
599
+
600
+ containers.forEach((container) => {
601
+ requestAnimationFrame(() => {
602
+ container.scrollTop = container.scrollHeight;
603
+ });
604
+ });
605
+ }
606
+
607
+ render() {
608
+ return renderRioAssist(this);
609
+ }
610
+
611
+ }
586
612
  declare global {
587
613
  interface HTMLElementTagNameMap {
588
614
  'rio-assist-widget': RioAssistWidget;