rio-assist-widget 0.1.10 → 0.1.11

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.
@@ -44,11 +44,13 @@ export class RioAssistWidget extends LitElement {
44
44
  showConversations: { type: Boolean, state: true },
45
45
  conversationSearch: { type: String, state: true },
46
46
  conversationMenuId: { state: true },
47
- conversationMenuPlacement: { state: true },
48
- isFullscreen: { type: Boolean, state: true },
49
- conversationScrollbar: { state: true },
47
+ conversationMenuPlacement: { state: true },
48
+ isFullscreen: { type: Boolean, state: true },
49
+ conversationScrollbar: { state: true },
50
50
  showNewConversationShortcut: { type: Boolean, state: true },
51
51
  conversations: { state: true },
52
+ conversationHistoryLoading: { type: Boolean, state: true },
53
+ activeConversationTitle: { state: true },
52
54
  };
53
55
 
54
56
  open = false;
@@ -83,25 +85,84 @@ export class RioAssistWidget extends LitElement {
83
85
 
84
86
  conversationMenuPlacement: 'above' | 'below' = 'below';
85
87
 
86
- isFullscreen = false;
87
-
88
- showNewConversationShortcut = false;
89
-
90
- conversationScrollbar = {
91
- height: 0,
92
- top: 0,
93
- visible: false,
94
- };
88
+ isFullscreen = false;
89
+
90
+ showNewConversationShortcut = false;
91
+
92
+ conversationScrollbar = {
93
+ height: 0,
94
+ top: 0,
95
+ visible: false,
96
+ };
97
+
98
+ conversationHistoryLoading = false;
99
+
100
+ private refreshConversationsAfterResponse = false;
101
+
102
+ activeConversationTitle: string | null = null;
103
+
104
+ private generateConversationId() {
105
+ if (!this.conversationUserId) {
106
+ this.conversationUserId = this.inferUserIdFromToken();
107
+ }
108
+
109
+ const userSegment = this.conversationUserId ?? 'user';
110
+ const id = `default-${userSegment}-${this.randomId(8)}`;
111
+ console.info('[RioAssist][conversation] gerando conversationId', id);
112
+ return id;
113
+ }
114
+
115
+ private inferUserIdFromToken(): string | null {
116
+ const token = this.rioToken.trim();
117
+ if (!token || !token.includes('.')) {
118
+ return null;
119
+ }
120
+
121
+ const [, payload] = token.split('.');
122
+ try {
123
+ const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
124
+ const candidate =
125
+ decoded?.userId ??
126
+ decoded?.user_id ??
127
+ decoded?.sub ??
128
+ decoded?.id ??
129
+ decoded?.email ??
130
+ decoded?.username;
131
+
132
+ if (candidate && typeof candidate === 'string') {
133
+ return candidate.replace(/[^a-zA-Z0-9_-]/g, '');
134
+ }
135
+ } catch {
136
+ return null;
137
+ }
138
+
139
+ return null;
140
+ }
141
+
142
+ private randomId(length: number) {
143
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
144
+ let result = '';
145
+ for (let i = 0; i < length; i += 1) {
146
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
147
+ }
148
+ return result;
149
+ }
95
150
 
96
151
  private conversationScrollbarRaf: number | null = null;
97
152
 
98
- private rioClient: RioWebsocketClient | null = null;
99
-
100
- private rioUnsubscribe: (() => void) | null = null;
101
-
102
- private loadingTimer: number | null = null;
103
-
104
- private conversationScrollbarDraggingId: number | null = null;
153
+ private rioClient: RioWebsocketClient | null = null;
154
+
155
+ private rioUnsubscribe: (() => void) | null = null;
156
+
157
+ private loadingTimer: number | null = null;
158
+
159
+ private currentConversationId: string | null = null;
160
+
161
+ private conversationCounter = 0;
162
+
163
+ private conversationUserId: string | null = null;
164
+
165
+ private conversationScrollbarDraggingId: number | null = null;
105
166
 
106
167
  private conversationScrollbarDragState: {
107
168
  startY: number;
@@ -224,13 +285,28 @@ export class RioAssistWidget extends LitElement {
224
285
  this.requestConversationHistory();
225
286
  }
226
287
 
227
- toggleNewConversationShortcut() {
228
- this.showNewConversationShortcut = !this.showNewConversationShortcut;
229
- }
230
-
231
- handleConversationSearch(event: InputEvent) {
232
- this.conversationSearch = (event.target as HTMLInputElement).value;
233
- }
288
+ toggleNewConversationShortcut() {
289
+ this.showNewConversationShortcut = !this.showNewConversationShortcut;
290
+ }
291
+
292
+ handleConversationSelect(conversationId: string) {
293
+ if (!conversationId) {
294
+ return;
295
+ }
296
+
297
+ this.showConversations = false;
298
+ this.conversationMenuId = null;
299
+ this.errorMessage = '';
300
+ this.currentConversationId = conversationId;
301
+ this.activeConversationTitle = this.lookupConversationTitle(conversationId);
302
+
303
+ console.info('[RioAssist][history] carregando conversa', conversationId);
304
+ this.requestConversationHistory(conversationId);
305
+ }
306
+
307
+ handleConversationSearch(event: InputEvent) {
308
+ this.conversationSearch = (event.target as HTMLInputElement).value;
309
+ }
234
310
 
235
311
  handleConversationMenuToggle(event: Event, id: string) {
236
312
  event.stopPropagation();
@@ -316,23 +392,25 @@ export class RioAssistWidget extends LitElement {
316
392
  }
317
393
  }
318
394
 
319
- handleCreateConversation() {
320
- if (!this.hasActiveConversation) {
321
- return;
322
- }
323
-
324
- this.clearLoadingGuard();
325
- this.isLoading = false;
326
- this.messages = [];
327
- this.message = '';
328
- this.errorMessage = '';
329
- this.showConversations = false;
330
- this.teardownRioClient();
331
- this.showNewConversationShortcut = false;
332
- this.dispatchEvent(
333
- new CustomEvent('rioassist:new-conversation', {
334
- bubbles: true,
335
- composed: true,
395
+ handleCreateConversation() {
396
+ if (!this.hasActiveConversation) {
397
+ return;
398
+ }
399
+
400
+ this.clearLoadingGuard();
401
+ this.isLoading = false;
402
+ this.messages = [];
403
+ this.message = '';
404
+ this.errorMessage = '';
405
+ this.showConversations = false;
406
+ this.teardownRioClient();
407
+ this.currentConversationId = this.generateConversationId();
408
+ this.activeConversationTitle = null;
409
+ this.showNewConversationShortcut = false;
410
+ this.dispatchEvent(
411
+ new CustomEvent('rioassist:new-conversation', {
412
+ bubbles: true,
413
+ composed: true,
336
414
  }),
337
415
  );
338
416
  }
@@ -494,13 +572,18 @@ export class RioAssistWidget extends LitElement {
494
572
  timestamp: Date.now(),
495
573
  };
496
574
  }
497
-
575
+
498
576
  private async processMessage(rawValue: string) {
499
577
  const content = rawValue.trim();
500
578
  if (!content || this.isLoading) {
501
579
  return;
502
580
  }
503
581
 
582
+ if (!this.currentConversationId) {
583
+ this.currentConversationId = this.generateConversationId();
584
+ this.activeConversationTitle = null;
585
+ }
586
+
504
587
  const wasEmptyConversation = this.messages.length === 0;
505
588
 
506
589
  this.dispatchEvent(
@@ -519,20 +602,21 @@ export class RioAssistWidget extends LitElement {
519
602
  this.messages = [...this.messages, userMessage];
520
603
  if (wasEmptyConversation) {
521
604
  this.showNewConversationShortcut = true;
605
+ this.refreshConversationsAfterResponse = true;
522
606
  }
523
607
  this.message = '';
524
608
  this.errorMessage = '';
525
609
  this.isLoading = true;
526
610
  this.startLoadingGuard();
527
611
 
528
- try {
529
- const client = this.ensureRioClient();
530
- await client.sendMessage(content);
531
- } catch (error) {
532
- this.clearLoadingGuard();
533
- this.isLoading = false;
534
- this.errorMessage = error instanceof Error
535
- ? error.message
612
+ try {
613
+ const client = this.ensureRioClient();
614
+ await client.sendMessage(content, this.currentConversationId);
615
+ } catch (error) {
616
+ this.clearLoadingGuard();
617
+ this.isLoading = false;
618
+ this.errorMessage = error instanceof Error
619
+ ? error.message
536
620
  : 'Nao foi possivel enviar a mensagem para o agente.';
537
621
  }
538
622
  }
@@ -559,14 +643,26 @@ export class RioAssistWidget extends LitElement {
559
643
  private handleIncomingMessage(message: RioIncomingMessage) {
560
644
  if (this.isHistoryPayload(message)) {
561
645
  this.logHistoryPayload(message);
562
- this.applyConversationHistory(message.data);
646
+ this.handleHistoryPayload(message.data);
563
647
  return;
564
648
  }
565
649
 
650
+ console.info('[RioAssist][ws] resposta de mensagem recebida', {
651
+ action: message.action ?? 'message',
652
+ text: message.text,
653
+ raw: message.raw,
654
+ data: message.data,
655
+ });
656
+
566
657
  const assistantMessage = this.createMessage('assistant', message.text);
567
658
  this.messages = [...this.messages, assistantMessage];
568
659
  this.clearLoadingGuard();
569
660
  this.isLoading = false;
661
+
662
+ if (this.refreshConversationsAfterResponse) {
663
+ this.refreshConversationsAfterResponse = false;
664
+ this.requestConversationHistory();
665
+ }
570
666
  }
571
667
 
572
668
  private teardownRioClient() {
@@ -591,9 +687,32 @@ export class RioAssistWidget extends LitElement {
591
687
  limit,
592
688
  });
593
689
 
690
+ this.conversationHistoryLoading = true;
594
691
  await client.requestHistory({ conversationId, limit });
595
692
  } catch (error) {
596
693
  console.error('[RioAssist][history] erro ao solicitar historico', error);
694
+ this.conversationHistoryLoading = false;
695
+ }
696
+ }
697
+
698
+ private handleHistoryPayload(payload: unknown) {
699
+ const entries = this.extractHistoryEntries(payload);
700
+ const conversationId = this.extractConversationId(payload);
701
+
702
+ if (conversationId !== null && conversationId !== undefined) {
703
+ this.applyMessageHistory(entries, conversationId);
704
+ return;
705
+ }
706
+
707
+ if (this.isMessageHistoryEntries(entries)) {
708
+ this.applyMessageHistory(entries);
709
+ return;
710
+ }
711
+
712
+ this.applyConversationHistoryFromEntries(entries);
713
+
714
+ if (this.refreshConversationsAfterResponse) {
715
+ this.refreshConversationsAfterResponse = false;
597
716
  }
598
717
  }
599
718
 
@@ -630,11 +749,11 @@ export class RioAssistWidget extends LitElement {
630
749
  console.info(label, message.raw);
631
750
  }
632
751
 
633
- private applyConversationHistory(payload: unknown) {
634
- const entries = this.extractHistoryEntries(payload);
752
+ private applyConversationHistoryFromEntries(entries: unknown[]) {
635
753
  if (entries.length === 0) {
636
754
  console.info('[RioAssist][history] payload sem itens para montar lista de conversas');
637
755
  this.conversations = [];
756
+ this.conversationHistoryLoading = false;
638
757
  return;
639
758
  }
640
759
 
@@ -674,9 +793,44 @@ export class RioAssistWidget extends LitElement {
674
793
  });
675
794
 
676
795
  this.conversations = conversations;
796
+ this.conversationHistoryLoading = false;
797
+ this.syncActiveConversationTitle();
677
798
  console.info('[RioAssist][history] conversas normalizadas', conversations);
678
799
  }
679
800
 
801
+ private applyMessageHistory(entries: unknown[], conversationId?: string | null) {
802
+ if (entries.length === 0) {
803
+ console.info('[RioAssist][history] lista de mensagens vazia', { conversationId });
804
+ this.messages = [];
805
+ this.showConversations = false;
806
+ this.clearLoadingGuard();
807
+ this.isLoading = false;
808
+ this.conversationHistoryLoading = false;
809
+ return;
810
+ }
811
+
812
+ const normalized = entries.flatMap((entry, index) =>
813
+ this.normalizeHistoryMessages(entry as Record<string, unknown>, index),
814
+ );
815
+
816
+ if (conversationId) {
817
+ this.currentConversationId = conversationId;
818
+ }
819
+
820
+ this.messages = normalized;
821
+ this.showConversations = false;
822
+ this.clearLoadingGuard();
823
+ this.isLoading = false;
824
+ this.showNewConversationShortcut = normalized.length > 0;
825
+ this.conversationHistoryLoading = false;
826
+ this.refreshConversationsAfterResponse = false;
827
+
828
+ console.info('[RioAssist][history] mensagens carregadas', {
829
+ conversationId: conversationId ?? null,
830
+ total: normalized.length,
831
+ });
832
+ }
833
+
680
834
  private extractHistoryEntries(payload: unknown): unknown[] {
681
835
  if (Array.isArray(payload)) {
682
836
  return payload;
@@ -709,6 +863,62 @@ export class RioAssistWidget extends LitElement {
709
863
  return [];
710
864
  }
711
865
 
866
+ private extractConversationId(payload: unknown): string | null | undefined {
867
+ if (payload && typeof payload === 'object') {
868
+ const record = payload as Record<string, unknown>;
869
+ const candidates = [
870
+ record.conversationId,
871
+ record.conversationUUID,
872
+ record.conversationUuid,
873
+ record.uuid,
874
+ record.id,
875
+ ];
876
+
877
+ for (const candidate of candidates) {
878
+ if (candidate === null) {
879
+ return null;
880
+ }
881
+
882
+ if (candidate !== undefined) {
883
+ return String(candidate);
884
+ }
885
+ }
886
+ }
887
+
888
+ return undefined;
889
+ }
890
+
891
+ private isMessageHistoryEntries(entries: unknown[]) {
892
+ return entries.some((entry) => this.looksLikeMessageHistoryEntry(entry));
893
+ }
894
+
895
+ private looksLikeMessageHistoryEntry(entry: unknown) {
896
+ if (!entry || typeof entry !== 'object') {
897
+ return false;
898
+ }
899
+
900
+ const item = entry as Record<string, unknown>;
901
+ const role = item.role ?? item.sender ?? item.from ?? item.author ?? item.type;
902
+ if (typeof role === 'string' && role.trim().length > 0) {
903
+ return true;
904
+ }
905
+
906
+ if (
907
+ typeof item.content === 'string' ||
908
+ typeof item.message === 'string' ||
909
+ typeof item.text === 'string' ||
910
+ typeof item.response === 'string'
911
+ ) {
912
+ return true;
913
+ }
914
+
915
+ if (Array.isArray(item.parts) && item.parts.length > 0) {
916
+ return true;
917
+ }
918
+
919
+ return false;
920
+ }
921
+
712
922
  private normalizeConversationItem(
713
923
  value: Record<string, unknown>,
714
924
  index: number,
@@ -751,6 +961,176 @@ export class RioAssistWidget extends LitElement {
751
961
  return { id, title, updatedAt };
752
962
  }
753
963
 
964
+ private normalizeHistoryMessages(
965
+ value: Record<string, unknown>,
966
+ index: number,
967
+ ): ChatMessage[] {
968
+ const messages: ChatMessage[] = [];
969
+
970
+ const rawUserText = value.message ?? value.question ?? value.query ?? value.text ?? value.content;
971
+ const userText = typeof rawUserText === 'string' ? rawUserText.trim() : '';
972
+
973
+ const rawResponseText =
974
+ value.response ?? value.answer ?? value.reply ?? value.completion ?? value.body ?? value.preview;
975
+ const responseText = typeof rawResponseText === 'string' ? rawResponseText.trim() : '';
976
+
977
+ const rawId = value.id ?? value.messageId ?? value.uuid ?? value.conversationMessageId;
978
+ const baseId = rawId !== undefined && rawId !== null
979
+ ? String(rawId)
980
+ : `history-${index + 1}`;
981
+
982
+ const userTimestampValue =
983
+ value.timestamp ??
984
+ value.createdAt ??
985
+ value.created_at ??
986
+ value.date ??
987
+ value.time;
988
+ const assistantTimestampValue =
989
+ value.responseTimestamp ??
990
+ value.responseTime ??
991
+ value.responseDate ??
992
+ value.response_at ??
993
+ value.updatedAt ??
994
+ value.updated_at;
995
+
996
+ const userTimestamp = this.parseTimestamp(userTimestampValue);
997
+ const assistantTimestamp = this.parseTimestamp(
998
+ assistantTimestampValue,
999
+ userTimestamp + 1,
1000
+ );
1001
+
1002
+ if (responseText) {
1003
+ if (userText) {
1004
+ messages.push({
1005
+ id: `${baseId}-user`,
1006
+ role: 'user',
1007
+ text: userText,
1008
+ html: this.renderMarkdown(userText),
1009
+ timestamp: userTimestamp,
1010
+ });
1011
+ }
1012
+
1013
+ messages.push({
1014
+ id: `${baseId}-assistant`,
1015
+ role: 'assistant',
1016
+ text: responseText,
1017
+ html: this.renderMarkdown(responseText),
1018
+ timestamp: assistantTimestamp,
1019
+ });
1020
+ } else if (userText) {
1021
+ // Se n�o tiver resposta, n�o exibimos a mensagem do usuario isolada.
1022
+ return [];
1023
+ }
1024
+
1025
+ if (messages.length > 0) {
1026
+ return messages;
1027
+ }
1028
+
1029
+ const fallback = this.normalizeSingleHistoryMessage(value, index);
1030
+ return fallback ? [fallback] : [];
1031
+ }
1032
+
1033
+ private normalizeSingleHistoryMessage(
1034
+ value: Record<string, unknown>,
1035
+ index: number,
1036
+ ): ChatMessage | null {
1037
+ const rawText =
1038
+ value.text ??
1039
+ value.message ??
1040
+ value.content ??
1041
+ value.response ??
1042
+ value.body ??
1043
+ value.preview;
1044
+
1045
+ const text = typeof rawText === 'string' && rawText.trim().length > 0
1046
+ ? rawText
1047
+ : '';
1048
+
1049
+ if (!text) {
1050
+ return null;
1051
+ }
1052
+
1053
+ const role = this.normalizeRole(
1054
+ value.role ??
1055
+ value.sender ??
1056
+ value.from ??
1057
+ value.author ??
1058
+ value.type ??
1059
+ value.direction,
1060
+ );
1061
+
1062
+ const rawId = value.id ?? value.messageId ?? value.uuid ?? value.conversationMessageId;
1063
+ const id = rawId !== undefined && rawId !== null
1064
+ ? String(rawId)
1065
+ : `history-message-${index + 1}`;
1066
+
1067
+ const timestampValue =
1068
+ value.timestamp ??
1069
+ value.createdAt ??
1070
+ value.created_at ??
1071
+ value.updatedAt ??
1072
+ value.updated_at ??
1073
+ value.date ??
1074
+ value.time;
1075
+
1076
+ const timestamp = this.parseTimestamp(timestampValue);
1077
+
1078
+ return {
1079
+ id,
1080
+ role,
1081
+ text,
1082
+ html: this.renderMarkdown(text),
1083
+ timestamp,
1084
+ };
1085
+ }
1086
+
1087
+ private normalizeRole(value: unknown): ChatRole {
1088
+ if (typeof value === 'string') {
1089
+ const normalized = value.toLowerCase();
1090
+ if (normalized.includes('user') || normalized.includes('client')) {
1091
+ return 'user';
1092
+ }
1093
+ if (normalized.includes('assistant') || normalized.includes('agent') || normalized.includes('bot')) {
1094
+ return 'assistant';
1095
+ }
1096
+ }
1097
+
1098
+ return 'assistant';
1099
+ }
1100
+
1101
+ private parseTimestamp(value: unknown, fallback?: number) {
1102
+ const parsed = Date.parse(this.toIsoString(value));
1103
+ if (Number.isFinite(parsed)) {
1104
+ return parsed;
1105
+ }
1106
+
1107
+ if (Number.isFinite(fallback ?? NaN)) {
1108
+ return fallback as number;
1109
+ }
1110
+
1111
+ return Date.now();
1112
+ }
1113
+
1114
+ private lookupConversationTitle(conversationId: string | null) {
1115
+ if (!conversationId) {
1116
+ return null;
1117
+ }
1118
+
1119
+ const found = this.conversations.find((item) => item.id === conversationId);
1120
+ return found ? found.title : null;
1121
+ }
1122
+
1123
+ private syncActiveConversationTitle() {
1124
+ if (!this.currentConversationId) {
1125
+ return;
1126
+ }
1127
+
1128
+ const title = this.lookupConversationTitle(this.currentConversationId);
1129
+ if (title) {
1130
+ this.activeConversationTitle = title;
1131
+ }
1132
+ }
1133
+
754
1134
  private toIsoString(value: unknown) {
755
1135
  if (typeof value === 'string' || typeof value === 'number') {
756
1136
  const date = new Date(value);
@@ -27,28 +27,27 @@ export class RioWebsocketClient {
27
27
  return this.token === value;
28
28
  }
29
29
 
30
- async sendMessage(message: string) {
30
+ async sendMessage(message: string, conversationId?: string | null) {
31
31
  const socket = await this.ensureConnection();
32
32
 
33
33
  const payload = {
34
34
  action: 'sendMessage',
35
35
  message,
36
+ conversationId: conversationId ?? null,
36
37
  };
37
38
 
39
+ console.info('[RioAssist][ws] enviando payload de mensagem', payload);
38
40
  socket.send(JSON.stringify(payload));
39
41
  }
40
42
 
41
- async requestHistory(options: { conversationId?: string; limit?: number } = {}) {
43
+ async requestHistory(options: { conversationId?: string | null; limit?: number } = {}) {
42
44
  const socket = await this.ensureConnection();
43
45
  const payload: Record<string, unknown> = {
44
46
  action: 'getHistory',
45
47
  limit: options.limit ?? 50,
48
+ conversationId: options.conversationId ?? null,
46
49
  };
47
50
 
48
- if (options.conversationId) {
49
- payload.conversationId = options.conversationId;
50
- }
51
-
52
51
  socket.send(JSON.stringify(payload));
53
52
  }
54
53