rio-assist-widget 0.1.23 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/rio-assist.js +119 -65
- package/package.json +1 -1
- package/src/components/conversations-panel/conversations-panel.template.ts +12 -8
- package/src/components/mini-panel/mini-panel.styles.ts +78 -58
- package/src/components/rio-assist/rio-assist.styles.ts +9 -0
- package/src/components/rio-assist/rio-assist.template.ts +36 -0
- package/src/components/rio-assist/rio-assist.ts +1710 -1511
|
@@ -1,41 +1,58 @@
|
|
|
1
|
-
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
-
import { widgetStyles } from './rio-assist.styles';
|
|
3
|
-
import { renderRioAssist } from './rio-assist.template';
|
|
4
|
-
import {
|
|
5
|
-
RioWebsocketClient,
|
|
6
|
-
type RioIncomingMessage,
|
|
7
|
-
} from '../../services/rioWebsocket';
|
|
8
|
-
import MarkdownIt from 'markdown-it';
|
|
9
|
-
import markdownItTaskLists from 'markdown-it-task-lists';
|
|
10
|
-
import DOMPurify from 'dompurify';
|
|
11
|
-
|
|
12
|
-
type ChatRole = 'user' | 'assistant';
|
|
13
|
-
|
|
14
|
-
export type ChatMessage = {
|
|
15
|
-
id: string;
|
|
16
|
-
role: ChatRole;
|
|
17
|
-
text: string;
|
|
18
|
-
html?: string;
|
|
19
|
-
timestamp: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
type ConversationItem = {
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
+
import { widgetStyles } from './rio-assist.styles';
|
|
3
|
+
import { renderRioAssist } from './rio-assist.template';
|
|
4
|
+
import {
|
|
5
|
+
RioWebsocketClient,
|
|
6
|
+
type RioIncomingMessage,
|
|
7
|
+
} from '../../services/rioWebsocket';
|
|
8
|
+
import MarkdownIt from 'markdown-it';
|
|
9
|
+
import markdownItTaskLists from 'markdown-it-task-lists';
|
|
10
|
+
import DOMPurify from 'dompurify';
|
|
11
|
+
|
|
12
|
+
type ChatRole = 'user' | 'assistant';
|
|
13
|
+
|
|
14
|
+
export type ChatMessage = {
|
|
15
|
+
id: string;
|
|
16
|
+
role: ChatRole;
|
|
17
|
+
text: string;
|
|
18
|
+
html?: string;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ConversationItem = {
|
|
23
|
+
id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
updatedAt: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type ConversationDeleteTarget = {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
index: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type ConversationRenameTarget = {
|
|
23
35
|
id: string;
|
|
24
36
|
title: string;
|
|
25
|
-
|
|
37
|
+
index: number;
|
|
38
|
+
draft: string;
|
|
26
39
|
};
|
|
27
40
|
|
|
28
|
-
type
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
type ConversationActionKind = 'rename' | 'delete';
|
|
42
|
+
|
|
43
|
+
type ConversationActionAttempt = {
|
|
44
|
+
action: ConversationActionKind;
|
|
45
|
+
conversationId: string;
|
|
46
|
+
originalTitle: string;
|
|
31
47
|
index: number;
|
|
48
|
+
newTitle?: string;
|
|
49
|
+
snapshot?: ConversationItem;
|
|
50
|
+
messagesSnapshot?: ChatMessage[];
|
|
51
|
+
wasActive?: boolean;
|
|
32
52
|
};
|
|
33
53
|
|
|
34
|
-
type
|
|
35
|
-
|
|
36
|
-
title: string;
|
|
37
|
-
index: number;
|
|
38
|
-
draft: string;
|
|
54
|
+
type ConversationActionErrorState = ConversationActionAttempt & {
|
|
55
|
+
message: string;
|
|
39
56
|
};
|
|
40
57
|
|
|
41
58
|
export type HeaderActionConfig = {
|
|
@@ -43,475 +60,514 @@ export type HeaderActionConfig = {
|
|
|
43
60
|
iconUrl: string;
|
|
44
61
|
ariaLabel?: string;
|
|
45
62
|
onClick?: () => void;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export class RioAssistWidget extends LitElement {
|
|
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 },
|
|
67
|
-
conversationMenuPlacement: { state: true },
|
|
68
|
-
isFullscreen: { type: Boolean, state: true },
|
|
69
|
-
conversationScrollbar: { state: true },
|
|
70
|
-
showNewConversationShortcut: { type: Boolean, state: true },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export class RioAssistWidget extends LitElement {
|
|
66
|
+
static styles = widgetStyles;
|
|
67
|
+
|
|
68
|
+
static properties = {
|
|
69
|
+
open: { type: Boolean, state: true },
|
|
70
|
+
message: { type: String, state: true },
|
|
71
|
+
titleText: { type: String, attribute: 'data-title' },
|
|
72
|
+
buttonLabel: { type: String, attribute: 'data-button-label' },
|
|
73
|
+
placeholder: { type: String, attribute: 'data-placeholder' },
|
|
74
|
+
accentColor: { type: String, attribute: 'data-accent-color' },
|
|
75
|
+
apiBaseUrl: { type: String, attribute: 'data-api-base-url' },
|
|
76
|
+
rioToken: { type: String, attribute: 'data-rio-token' },
|
|
77
|
+
suggestionsSource: { type: String, attribute: 'data-suggestions' },
|
|
78
|
+
messages: { state: true },
|
|
79
|
+
isLoading: { type: Boolean, state: true },
|
|
80
|
+
errorMessage: { type: String, state: true },
|
|
81
|
+
showConversations: { type: Boolean, state: true },
|
|
82
|
+
conversationSearch: { type: String, state: true },
|
|
83
|
+
conversationMenuId: { state: true },
|
|
84
|
+
conversationMenuPlacement: { state: true },
|
|
85
|
+
isFullscreen: { type: Boolean, state: true },
|
|
86
|
+
conversationScrollbar: { state: true },
|
|
87
|
+
showNewConversationShortcut: { type: Boolean, state: true },
|
|
71
88
|
conversations: { state: true },
|
|
72
89
|
conversationHistoryLoading: { type: Boolean, state: true },
|
|
73
90
|
activeConversationTitle: { state: true },
|
|
74
91
|
conversationHistoryError: { type: String, state: true },
|
|
75
92
|
deleteConversationTarget: { attribute: false },
|
|
76
93
|
renameConversationTarget: { attribute: false },
|
|
94
|
+
conversationActionError: { attribute: false },
|
|
77
95
|
headerActions: { attribute: false },
|
|
78
96
|
homeUrl: { type: String, attribute: 'data-home-url' },
|
|
79
97
|
};
|
|
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
|
-
|
|
119
|
-
isFullscreen = false;
|
|
120
|
-
|
|
121
|
-
showNewConversationShortcut = false;
|
|
122
|
-
|
|
123
|
-
conversationScrollbar = {
|
|
124
|
-
height: 0,
|
|
125
|
-
top: 0,
|
|
126
|
-
visible: false,
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
conversationHistoryLoading = false;
|
|
130
|
-
|
|
131
|
-
conversationHistoryError = '';
|
|
132
|
-
|
|
133
|
-
deleteConversationTarget: ConversationDeleteTarget | null = null;
|
|
134
|
-
|
|
98
|
+
|
|
99
|
+
open = false;
|
|
100
|
+
|
|
101
|
+
message = '';
|
|
102
|
+
|
|
103
|
+
titleText = 'Rio Insight';
|
|
104
|
+
|
|
105
|
+
buttonLabel = 'Rio Insight';
|
|
106
|
+
|
|
107
|
+
placeholder = 'Pergunte alguma coisa';
|
|
108
|
+
|
|
109
|
+
accentColor = '#008B9A';
|
|
110
|
+
|
|
111
|
+
apiBaseUrl = '';
|
|
112
|
+
|
|
113
|
+
rioToken = '';
|
|
114
|
+
|
|
115
|
+
suggestionsSource = '';
|
|
116
|
+
|
|
117
|
+
private randomizedSuggestions: string[] = [];
|
|
118
|
+
|
|
119
|
+
messages: ChatMessage[] = [];
|
|
120
|
+
|
|
121
|
+
isLoading = false;
|
|
122
|
+
|
|
123
|
+
errorMessage = '';
|
|
124
|
+
|
|
125
|
+
get loadingLabel() {
|
|
126
|
+
return this.loadingLabelInternal;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
showConversations = false;
|
|
130
|
+
|
|
131
|
+
conversationSearch = '';
|
|
132
|
+
|
|
133
|
+
conversationMenuId: string | null = null;
|
|
134
|
+
|
|
135
|
+
conversationMenuPlacement: 'above' | 'below' = 'below';
|
|
136
|
+
|
|
137
|
+
isFullscreen = false;
|
|
138
|
+
|
|
139
|
+
showNewConversationShortcut = false;
|
|
140
|
+
|
|
141
|
+
conversationScrollbar = {
|
|
142
|
+
height: 0,
|
|
143
|
+
top: 0,
|
|
144
|
+
visible: false,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
conversationHistoryLoading = false;
|
|
148
|
+
|
|
149
|
+
conversationHistoryError = '';
|
|
150
|
+
|
|
151
|
+
deleteConversationTarget: ConversationDeleteTarget | null = null;
|
|
152
|
+
|
|
135
153
|
renameConversationTarget: ConversationRenameTarget | null = null;
|
|
136
154
|
|
|
137
|
-
|
|
138
|
-
private loadingTimerSlow: number | null = null;
|
|
139
|
-
private loadingTimerTimeout: number | null = null;
|
|
140
|
-
|
|
141
|
-
private refreshConversationsAfterResponse = false;
|
|
155
|
+
conversationActionError: ConversationActionErrorState | null = null;
|
|
142
156
|
|
|
143
|
-
|
|
157
|
+
private loadingLabelInternal = 'Rio Insight está respondendo...';
|
|
158
|
+
private loadingTimerSlow: number | null = null;
|
|
159
|
+
private loadingTimerTimeout: number | null = null;
|
|
160
|
+
|
|
161
|
+
private refreshConversationsAfterResponse = false;
|
|
162
|
+
|
|
163
|
+
activeConversationTitle: string | null = null;
|
|
144
164
|
|
|
145
165
|
headerActions: HeaderActionConfig[] = [];
|
|
146
166
|
|
|
147
167
|
homeUrl = '';
|
|
148
168
|
|
|
149
|
-
private
|
|
150
|
-
if (!this.conversationUserId) {
|
|
151
|
-
this.conversationUserId = this.inferUserIdFromToken();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const userSegment = this.conversationUserId ?? 'user';
|
|
155
|
-
const id = `default-${userSegment}-${this.randomId(8)}`;
|
|
156
|
-
console.info('[RioAssist][conversation] gerando conversationId', id);
|
|
157
|
-
return id;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
private inferUserIdFromToken(): string | null {
|
|
161
|
-
const token = this.rioToken.trim();
|
|
162
|
-
if (!token || !token.includes('.')) {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const [, payload] = token.split('.');
|
|
167
|
-
try {
|
|
168
|
-
const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
|
|
169
|
-
const candidate =
|
|
170
|
-
decoded?.userId ??
|
|
171
|
-
decoded?.user_id ??
|
|
172
|
-
decoded?.sub ??
|
|
173
|
-
decoded?.id ??
|
|
174
|
-
decoded?.email ??
|
|
175
|
-
decoded?.username;
|
|
176
|
-
|
|
177
|
-
if (candidate && typeof candidate === 'string') {
|
|
178
|
-
return candidate.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
179
|
-
}
|
|
180
|
-
} catch {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private randomId(length: number) {
|
|
188
|
-
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
189
|
-
let result = '';
|
|
190
|
-
for (let i = 0; i < length; i += 1) {
|
|
191
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
192
|
-
}
|
|
193
|
-
return result;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
private conversationScrollbarRaf: number | null = null;
|
|
197
|
-
|
|
198
|
-
private rioClient: RioWebsocketClient | null = null;
|
|
199
|
-
|
|
200
|
-
private rioUnsubscribe: (() => void) | null = null;
|
|
201
|
-
|
|
202
|
-
private loadingTimer: number | null = null;
|
|
203
|
-
|
|
204
|
-
private currentConversationId: string | null = null;
|
|
205
|
-
|
|
206
|
-
private conversationCounter = 0;
|
|
207
|
-
|
|
208
|
-
private conversationUserId: string | null = null;
|
|
209
|
-
|
|
210
|
-
private conversationScrollbarDraggingId: number | null = null;
|
|
211
|
-
|
|
212
|
-
private conversationScrollbarDragState: {
|
|
213
|
-
startY: number;
|
|
214
|
-
startThumbTop: number;
|
|
215
|
-
trackHeight: number;
|
|
216
|
-
thumbHeight: number;
|
|
217
|
-
list: HTMLElement;
|
|
218
|
-
} | null = null;
|
|
219
|
-
|
|
220
|
-
private markdownRenderer = new MarkdownIt({
|
|
221
|
-
html: false,
|
|
222
|
-
linkify: true,
|
|
223
|
-
breaks: true,
|
|
224
|
-
}).use(markdownItTaskLists);
|
|
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
|
-
|
|
272
|
-
if (
|
|
273
|
-
changedProperties.has('isFullscreen') ||
|
|
274
|
-
changedProperties.has('showConversations') ||
|
|
275
|
-
changedProperties.has('conversations')
|
|
276
|
-
) {
|
|
277
|
-
this.enqueueConversationScrollbarMeasure();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
changedProperties.has('messages') ||
|
|
282
|
-
(changedProperties.has('isLoading') && this.isLoading) ||
|
|
283
|
-
(changedProperties.has('open') && this.open) ||
|
|
284
|
-
(changedProperties.has('isFullscreen') && this.isFullscreen)
|
|
285
|
-
) {
|
|
286
|
-
this.scrollConversationToBottom();
|
|
287
|
-
}
|
|
288
|
-
}
|
|
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
|
-
|
|
342
|
-
openConversationsPanel() {
|
|
343
|
-
this.showConversations = true;
|
|
344
|
-
this.requestConversationHistory();
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
closeConversationsPanel() {
|
|
348
|
-
this.showConversations = false;
|
|
349
|
-
this.conversationMenuId = null;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
toggleConversationsPanel() {
|
|
353
|
-
this.showConversations = !this.showConversations;
|
|
354
|
-
if (!this.showConversations) {
|
|
355
|
-
this.conversationMenuId = null;
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
this.requestConversationHistory();
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
toggleNewConversationShortcut() {
|
|
363
|
-
this.showNewConversationShortcut = !this.showNewConversationShortcut;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
handleConversationSelect(conversationId: string) {
|
|
367
|
-
if (!conversationId) {
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
this.showConversations = false;
|
|
372
|
-
this.conversationMenuId = null;
|
|
373
|
-
this.errorMessage = '';
|
|
374
|
-
this.currentConversationId = conversationId;
|
|
375
|
-
this.activeConversationTitle = this.lookupConversationTitle(conversationId);
|
|
376
|
-
|
|
377
|
-
console.info('[RioAssist][history] carregando conversa', conversationId);
|
|
378
|
-
this.requestConversationHistory(conversationId);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
handleConversationSearch(event: InputEvent) {
|
|
382
|
-
this.conversationSearch = (event.target as HTMLInputElement).value;
|
|
383
|
-
}
|
|
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
|
-
|
|
420
|
-
handleConversationAction(action: 'rename' | 'delete', id: string) {
|
|
421
|
-
this.conversationMenuId = null;
|
|
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
|
-
};
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
this.renameConversationTarget = {
|
|
438
|
-
id: conversation.id,
|
|
439
|
-
title: conversation.title,
|
|
440
|
-
index: conversationIndex,
|
|
441
|
-
draft: conversation.title,
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
handleHomeNavigation() {
|
|
446
|
-
const detail = { url: this.homeUrl || null };
|
|
447
|
-
const allowed = this.dispatchEvent(
|
|
448
|
-
new CustomEvent('rioassist:home', {
|
|
449
|
-
detail,
|
|
450
|
-
bubbles: true,
|
|
451
|
-
composed: true,
|
|
452
|
-
cancelable: true,
|
|
453
|
-
}),
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
if (!allowed) {
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (this.homeUrl) {
|
|
461
|
-
window.location.assign(this.homeUrl);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
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
|
-
});
|
|
169
|
+
private pendingConversationAction: ConversationActionAttempt | null = null;
|
|
478
170
|
|
|
479
|
-
|
|
171
|
+
private generateConversationId() {
|
|
172
|
+
if (!this.conversationUserId) {
|
|
173
|
+
this.conversationUserId = this.inferUserIdFromToken();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const userSegment = this.conversationUserId ?? 'user';
|
|
177
|
+
const id = `default-${userSegment}-${this.randomId(8)}`;
|
|
178
|
+
console.info('[RioAssist][conversation] gerando conversationId', id);
|
|
179
|
+
return id;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private inferUserIdFromToken(): string | null {
|
|
183
|
+
const token = this.rioToken.trim();
|
|
184
|
+
if (!token || !token.includes('.')) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const [, payload] = token.split('.');
|
|
189
|
+
try {
|
|
190
|
+
const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
|
|
191
|
+
const candidate =
|
|
192
|
+
decoded?.userId ??
|
|
193
|
+
decoded?.user_id ??
|
|
194
|
+
decoded?.sub ??
|
|
195
|
+
decoded?.id ??
|
|
196
|
+
decoded?.email ??
|
|
197
|
+
decoded?.username;
|
|
198
|
+
|
|
199
|
+
if (candidate && typeof candidate === 'string') {
|
|
200
|
+
return candidate.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private randomId(length: number) {
|
|
210
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
211
|
+
let result = '';
|
|
212
|
+
for (let i = 0; i < length; i += 1) {
|
|
213
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private conversationScrollbarRaf: number | null = null;
|
|
219
|
+
|
|
220
|
+
private rioClient: RioWebsocketClient | null = null;
|
|
221
|
+
|
|
222
|
+
private rioUnsubscribe: (() => void) | null = null;
|
|
223
|
+
|
|
224
|
+
private loadingTimer: number | null = null;
|
|
225
|
+
|
|
226
|
+
private currentConversationId: string | null = null;
|
|
227
|
+
|
|
228
|
+
private conversationCounter = 0;
|
|
229
|
+
|
|
230
|
+
private conversationUserId: string | null = null;
|
|
231
|
+
|
|
232
|
+
private conversationScrollbarDraggingId: number | null = null;
|
|
233
|
+
|
|
234
|
+
private conversationScrollbarDragState: {
|
|
235
|
+
startY: number;
|
|
236
|
+
startThumbTop: number;
|
|
237
|
+
trackHeight: number;
|
|
238
|
+
thumbHeight: number;
|
|
239
|
+
list: HTMLElement;
|
|
240
|
+
} | null = null;
|
|
241
|
+
|
|
242
|
+
private markdownRenderer = new MarkdownIt({
|
|
243
|
+
html: false,
|
|
244
|
+
linkify: true,
|
|
245
|
+
breaks: true,
|
|
246
|
+
}).use(markdownItTaskLists);
|
|
247
|
+
|
|
248
|
+
conversations: ConversationItem[] = [];
|
|
249
|
+
|
|
250
|
+
get suggestions(): string[] {
|
|
251
|
+
return this.randomizedSuggestions;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private parseSuggestions(source: string): string[] {
|
|
255
|
+
if (!source) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return source
|
|
260
|
+
.split('|')
|
|
261
|
+
.map((item) => item.trim())
|
|
262
|
+
.filter(Boolean);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private pickRandomSuggestions(options: string[], count: number): string[] {
|
|
266
|
+
if (options.length <= count) {
|
|
267
|
+
return [...options];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const pool = [...options];
|
|
271
|
+
for (let index = pool.length - 1; index > 0; index -= 1) {
|
|
272
|
+
const swapIndex = Math.floor(Math.random() * (index + 1));
|
|
273
|
+
[pool[index], pool[swapIndex]] = [pool[swapIndex], pool[index]];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return pool.slice(0, count);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
protected willUpdate(changedProperties: PropertyValues): void {
|
|
280
|
+
super.willUpdate(changedProperties);
|
|
281
|
+
|
|
282
|
+
if (changedProperties.has('suggestionsSource')) {
|
|
283
|
+
this.randomizedSuggestions = this.pickRandomSuggestions(
|
|
284
|
+
this.parseSuggestions(this.suggestionsSource),
|
|
285
|
+
3,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
protected updated(changedProperties: PropertyValues): void {
|
|
291
|
+
super.updated(changedProperties);
|
|
292
|
+
this.style.setProperty('--accent-color', this.accentColor);
|
|
293
|
+
|
|
294
|
+
if (
|
|
295
|
+
changedProperties.has('isFullscreen') ||
|
|
296
|
+
changedProperties.has('showConversations') ||
|
|
297
|
+
changedProperties.has('conversations')
|
|
298
|
+
) {
|
|
299
|
+
this.enqueueConversationScrollbarMeasure();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (
|
|
303
|
+
changedProperties.has('messages') ||
|
|
304
|
+
(changedProperties.has('isLoading') && this.isLoading) ||
|
|
305
|
+
(changedProperties.has('open') && this.open) ||
|
|
306
|
+
(changedProperties.has('isFullscreen') && this.isFullscreen)
|
|
307
|
+
) {
|
|
308
|
+
this.scrollConversationToBottom();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
protected firstUpdated(): void {
|
|
313
|
+
this.enqueueConversationScrollbarMeasure();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
disconnectedCallback(): void {
|
|
317
|
+
super.disconnectedCallback();
|
|
318
|
+
if (this.conversationScrollbarRaf !== null) {
|
|
319
|
+
cancelAnimationFrame(this.conversationScrollbarRaf);
|
|
320
|
+
this.conversationScrollbarRaf = null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.teardownRioClient();
|
|
324
|
+
this.clearLoadingGuard();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
get filteredConversations() {
|
|
328
|
+
const query = this.conversationSearch.trim().toLowerCase();
|
|
329
|
+
if (!query) {
|
|
330
|
+
return this.conversations;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return this.conversations.filter((conversation) =>
|
|
334
|
+
conversation.title.toLowerCase().includes(query),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
get hasActiveConversation() {
|
|
339
|
+
return this.messages.length > 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
togglePanel() {
|
|
343
|
+
if (this.isFullscreen) {
|
|
344
|
+
this.exitFullscreen(false);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
this.open = !this.open;
|
|
349
|
+
this.dispatchEvent(
|
|
350
|
+
new CustomEvent(this.open ? 'rioassist:open' : 'rioassist:close', {
|
|
351
|
+
bubbles: true,
|
|
352
|
+
composed: true,
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
closePanel() {
|
|
358
|
+
this.isFullscreen = false;
|
|
359
|
+
if (this.open) {
|
|
360
|
+
this.togglePanel();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
openConversationsPanel() {
|
|
365
|
+
this.showConversations = true;
|
|
366
|
+
this.requestConversationHistory();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
closeConversationsPanel() {
|
|
370
|
+
this.showConversations = false;
|
|
371
|
+
this.conversationMenuId = null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
toggleConversationsPanel() {
|
|
375
|
+
this.showConversations = !this.showConversations;
|
|
376
|
+
if (!this.showConversations) {
|
|
377
|
+
this.conversationMenuId = null;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.requestConversationHistory();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
toggleNewConversationShortcut() {
|
|
385
|
+
this.showNewConversationShortcut = !this.showNewConversationShortcut;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
handleConversationSelect(conversationId: string) {
|
|
389
|
+
if (!conversationId) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
this.showConversations = false;
|
|
394
|
+
this.conversationMenuId = null;
|
|
395
|
+
this.errorMessage = '';
|
|
396
|
+
this.currentConversationId = conversationId;
|
|
397
|
+
this.activeConversationTitle = this.lookupConversationTitle(conversationId);
|
|
398
|
+
|
|
399
|
+
console.info('[RioAssist][history] carregando conversa', conversationId);
|
|
400
|
+
this.requestConversationHistory(conversationId);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
handleConversationSearch(event: InputEvent) {
|
|
404
|
+
this.conversationSearch = (event.target as HTMLInputElement).value;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
handleConversationMenuToggle(event: Event, id: string) {
|
|
408
|
+
event.stopPropagation();
|
|
409
|
+
|
|
410
|
+
if (this.conversationMenuId === id) {
|
|
411
|
+
this.conversationMenuId = null;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const button = event.currentTarget as HTMLElement;
|
|
416
|
+
const container = this.renderRoot.querySelector(
|
|
417
|
+
'.conversations-panel__surface',
|
|
418
|
+
) as HTMLElement | null;
|
|
419
|
+
|
|
420
|
+
if (button && container) {
|
|
421
|
+
const buttonRect = button.getBoundingClientRect();
|
|
422
|
+
const containerRect = container.getBoundingClientRect();
|
|
423
|
+
const spaceBelow = containerRect.bottom - buttonRect.bottom;
|
|
424
|
+
this.conversationMenuPlacement = spaceBelow < 140 ? 'above' : 'below';
|
|
425
|
+
} else {
|
|
426
|
+
this.conversationMenuPlacement = 'below';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
this.conversationMenuId = id;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
handleConversationsPanelPointer(event: PointerEvent) {
|
|
433
|
+
const target = event.target as HTMLElement;
|
|
434
|
+
if (
|
|
435
|
+
!target.closest('.conversation-menu') &&
|
|
436
|
+
!target.closest('.conversation-menu-button')
|
|
437
|
+
) {
|
|
438
|
+
this.conversationMenuId = null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
handleConversationAction(action: 'rename' | 'delete', id: string) {
|
|
443
|
+
this.conversationMenuId = null;
|
|
444
|
+
const conversationIndex = this.conversations.findIndex((item) => item.id === id);
|
|
445
|
+
if (conversationIndex === -1) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const conversation = this.conversations[conversationIndex];
|
|
450
|
+
if (action === 'delete') {
|
|
451
|
+
this.deleteConversationTarget = {
|
|
452
|
+
id: conversation.id,
|
|
453
|
+
title: conversation.title,
|
|
454
|
+
index: conversationIndex,
|
|
455
|
+
};
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
this.renameConversationTarget = {
|
|
460
|
+
id: conversation.id,
|
|
461
|
+
title: conversation.title,
|
|
462
|
+
index: conversationIndex,
|
|
463
|
+
draft: conversation.title,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
handleHomeNavigation() {
|
|
468
|
+
const detail = { url: this.homeUrl || null };
|
|
469
|
+
const allowed = this.dispatchEvent(
|
|
470
|
+
new CustomEvent('rioassist:home', {
|
|
471
|
+
detail,
|
|
472
|
+
bubbles: true,
|
|
473
|
+
composed: true,
|
|
474
|
+
cancelable: true,
|
|
475
|
+
}),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
if (!allowed) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (this.homeUrl) {
|
|
483
|
+
window.location.assign(this.homeUrl);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
applyConversationRename(id: string, newTitle: string) {
|
|
488
|
+
if (!id || !newTitle) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let changed = false;
|
|
493
|
+
this.conversations = this.conversations.map((conversation) => {
|
|
494
|
+
if (conversation.id === id) {
|
|
495
|
+
changed = true;
|
|
496
|
+
return { ...conversation, title: newTitle };
|
|
497
|
+
}
|
|
498
|
+
return conversation;
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
if (!changed) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (this.currentConversationId === id) {
|
|
506
|
+
this.activeConversationTitle = newTitle;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
applyConversationDeletion(id: string) {
|
|
511
|
+
if (!id) {
|
|
480
512
|
return;
|
|
481
513
|
}
|
|
482
514
|
|
|
483
|
-
|
|
484
|
-
|
|
515
|
+
const wasActive = this.currentConversationId === id;
|
|
516
|
+
const next = this.conversations.filter((conversation) => conversation.id !== id);
|
|
517
|
+
|
|
518
|
+
if (next.length === this.conversations.length) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
this.conversations = next;
|
|
523
|
+
|
|
524
|
+
if (wasActive) {
|
|
525
|
+
this.currentConversationId = null;
|
|
526
|
+
this.activeConversationTitle = null;
|
|
527
|
+
this.messages = [];
|
|
485
528
|
}
|
|
486
529
|
}
|
|
487
530
|
|
|
488
|
-
|
|
489
|
-
if (!
|
|
531
|
+
private restoreConversationSnapshot(snapshot: ConversationItem | undefined, index: number) {
|
|
532
|
+
if (!snapshot) {
|
|
490
533
|
return;
|
|
491
534
|
}
|
|
492
535
|
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (next.length === this.conversations.length) {
|
|
536
|
+
const exists = this.conversations.some((conversation) => conversation.id === snapshot.id);
|
|
537
|
+
if (exists) {
|
|
497
538
|
return;
|
|
498
539
|
}
|
|
499
540
|
|
|
541
|
+
const next = [...this.conversations];
|
|
542
|
+
const position = index >= 0 && index <= next.length ? index : next.length;
|
|
543
|
+
next.splice(position, 0, snapshot);
|
|
500
544
|
this.conversations = next;
|
|
501
|
-
|
|
502
|
-
if (wasActive) {
|
|
503
|
-
this.currentConversationId = null;
|
|
504
|
-
this.activeConversationTitle = null;
|
|
505
|
-
this.messages = [];
|
|
506
|
-
}
|
|
507
545
|
}
|
|
508
|
-
|
|
546
|
+
|
|
509
547
|
async confirmDeleteConversation() {
|
|
510
548
|
const target = this.deleteConversationTarget;
|
|
511
549
|
if (!target) {
|
|
512
550
|
return;
|
|
513
551
|
}
|
|
514
552
|
|
|
553
|
+
const snapshot =
|
|
554
|
+
this.conversations[target.index] ??
|
|
555
|
+
this.conversations.find((item) => item.id === target.id) ?? {
|
|
556
|
+
id: target.id,
|
|
557
|
+
title: target.title,
|
|
558
|
+
updatedAt: new Date().toISOString(),
|
|
559
|
+
};
|
|
560
|
+
const isActive = this.currentConversationId === target.id;
|
|
561
|
+
this.pendingConversationAction = {
|
|
562
|
+
action: 'delete',
|
|
563
|
+
conversationId: target.id,
|
|
564
|
+
originalTitle: target.title,
|
|
565
|
+
index: target.index,
|
|
566
|
+
snapshot,
|
|
567
|
+
messagesSnapshot: isActive ? [...this.messages] : undefined,
|
|
568
|
+
wasActive: isActive,
|
|
569
|
+
};
|
|
570
|
+
|
|
515
571
|
const success = await this.dispatchConversationAction(
|
|
516
572
|
'delete',
|
|
517
573
|
{ id: target.id, title: target.title },
|
|
@@ -519,35 +575,46 @@ export class RioAssistWidget extends LitElement {
|
|
|
519
575
|
);
|
|
520
576
|
if (success) {
|
|
521
577
|
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
578
|
return;
|
|
544
579
|
}
|
|
545
580
|
|
|
581
|
+
this.pendingConversationAction = null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
cancelDeleteConversation() {
|
|
585
|
+
this.deleteConversationTarget = null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
handleRenameDraft(event: InputEvent) {
|
|
589
|
+
if (!this.renameConversationTarget) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
this.renameConversationTarget = {
|
|
594
|
+
...this.renameConversationTarget,
|
|
595
|
+
draft: (event.target as HTMLInputElement).value,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async confirmRenameConversation() {
|
|
600
|
+
const target = this.renameConversationTarget;
|
|
601
|
+
if (!target) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
546
605
|
const newTitle = target.draft.trim();
|
|
547
606
|
if (!newTitle) {
|
|
548
607
|
return;
|
|
549
608
|
}
|
|
550
609
|
|
|
610
|
+
this.pendingConversationAction = {
|
|
611
|
+
action: 'rename',
|
|
612
|
+
conversationId: target.id,
|
|
613
|
+
originalTitle: target.title,
|
|
614
|
+
index: target.index,
|
|
615
|
+
newTitle,
|
|
616
|
+
};
|
|
617
|
+
|
|
551
618
|
const success = await this.dispatchConversationAction(
|
|
552
619
|
'rename',
|
|
553
620
|
{ id: target.id, title: newTitle },
|
|
@@ -556,111 +623,181 @@ export class RioAssistWidget extends LitElement {
|
|
|
556
623
|
);
|
|
557
624
|
if (success) {
|
|
558
625
|
this.renameConversationTarget = null;
|
|
626
|
+
return;
|
|
559
627
|
}
|
|
560
|
-
}
|
|
561
628
|
|
|
629
|
+
this.pendingConversationAction = null;
|
|
630
|
+
}
|
|
631
|
+
|
|
562
632
|
cancelRenameConversation() {
|
|
563
633
|
this.renameConversationTarget = null;
|
|
564
634
|
}
|
|
565
635
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
}
|
|
636
|
+
cancelConversationActionError() {
|
|
637
|
+
this.conversationActionError = null;
|
|
638
|
+
this.pendingConversationAction = null;
|
|
639
|
+
}
|
|
593
640
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
641
|
+
async retryConversationAction() {
|
|
642
|
+
const errorState = this.conversationActionError;
|
|
643
|
+
if (!errorState) {
|
|
644
|
+
return;
|
|
597
645
|
}
|
|
598
646
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
647
|
+
const indexFromState =
|
|
648
|
+
typeof errorState.index === 'number' ? errorState.index : this.conversations.findIndex(
|
|
649
|
+
(item) => item.id === errorState.conversationId,
|
|
650
|
+
);
|
|
651
|
+
const safeIndex =
|
|
652
|
+
indexFromState >= 0
|
|
653
|
+
? indexFromState
|
|
654
|
+
: this.conversations.length > 0
|
|
655
|
+
? this.conversations.length - 1
|
|
656
|
+
: 0;
|
|
657
|
+
|
|
658
|
+
const snapshot =
|
|
659
|
+
errorState.snapshot ??
|
|
660
|
+
this.conversations.find((item) => item.id === errorState.conversationId) ?? {
|
|
661
|
+
id: errorState.conversationId,
|
|
662
|
+
title: errorState.originalTitle,
|
|
663
|
+
updatedAt: new Date().toISOString(),
|
|
664
|
+
};
|
|
603
665
|
|
|
604
|
-
|
|
605
|
-
|
|
666
|
+
this.pendingConversationAction = {
|
|
667
|
+
action: errorState.action,
|
|
668
|
+
conversationId: errorState.conversationId,
|
|
669
|
+
originalTitle: errorState.originalTitle,
|
|
670
|
+
index: safeIndex,
|
|
671
|
+
newTitle: errorState.newTitle,
|
|
672
|
+
snapshot,
|
|
673
|
+
messagesSnapshot: errorState.messagesSnapshot,
|
|
674
|
+
wasActive: errorState.wasActive,
|
|
675
|
+
};
|
|
606
676
|
|
|
607
|
-
|
|
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
|
-
}
|
|
677
|
+
this.conversationActionError = null;
|
|
623
678
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
}
|
|
679
|
+
await this.dispatchConversationAction(
|
|
680
|
+
errorState.action,
|
|
681
|
+
{ id: errorState.conversationId, title: errorState.newTitle ?? errorState.originalTitle },
|
|
682
|
+
safeIndex,
|
|
683
|
+
errorState.newTitle,
|
|
684
|
+
);
|
|
639
685
|
}
|
|
640
686
|
|
|
641
|
-
private
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
687
|
+
private async dispatchConversationAction(
|
|
688
|
+
action: 'rename' | 'delete',
|
|
689
|
+
conversation: Pick<ConversationItem, 'id' | 'title'>,
|
|
690
|
+
index: number,
|
|
691
|
+
newTitle?: string,
|
|
692
|
+
) {
|
|
693
|
+
const eventName =
|
|
694
|
+
action === 'rename' ? 'rioassist:conversation-rename' : 'rioassist:conversation-delete';
|
|
695
|
+
const detail = {
|
|
696
|
+
id: conversation.id,
|
|
697
|
+
title: conversation.title,
|
|
698
|
+
index,
|
|
699
|
+
action,
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const allowed = this.dispatchEvent(
|
|
703
|
+
new CustomEvent(eventName, {
|
|
704
|
+
detail,
|
|
705
|
+
bubbles: true,
|
|
706
|
+
composed: true,
|
|
707
|
+
cancelable: true,
|
|
708
|
+
}),
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
if (!allowed) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (action === 'delete') {
|
|
716
|
+
const ok = await this.syncConversationDeleteBackend(conversation.id);
|
|
717
|
+
return ok;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (action === 'rename' && newTitle) {
|
|
721
|
+
const ok = await this.syncConversationRenameBackend(conversation.id, newTitle);
|
|
722
|
+
return ok;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private async syncConversationRenameBackend(conversationId: string, newTitle: string) {
|
|
729
|
+
try {
|
|
730
|
+
const client = this.ensureRioClient();
|
|
731
|
+
await client.renameConversation(conversationId, newTitle);
|
|
732
|
+
this.applyConversationRename(conversationId, newTitle);
|
|
733
|
+
this.conversationHistoryError = '';
|
|
734
|
+
return true;
|
|
735
|
+
} catch (error) {
|
|
736
|
+
console.error('[RioAssist][history] erro ao renomear conversa', error);
|
|
737
|
+
this.conversationHistoryError =
|
|
738
|
+
error instanceof Error && error.message
|
|
739
|
+
? error.message
|
|
740
|
+
: 'Nao foi possivel renomear a conversa.';
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private async syncConversationDeleteBackend(conversationId: string) {
|
|
746
|
+
try {
|
|
747
|
+
const client = this.ensureRioClient();
|
|
748
|
+
await client.deleteConversation(conversationId);
|
|
749
|
+
this.applyConversationDeletion(conversationId);
|
|
750
|
+
this.conversationHistoryError = '';
|
|
751
|
+
return true;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
console.error('[RioAssist][history] erro ao excluir conversa', error);
|
|
754
|
+
this.conversationHistoryError =
|
|
755
|
+
error instanceof Error && error.message
|
|
756
|
+
? error.message
|
|
757
|
+
: 'Nao foi possivel excluir a conversa.';
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
private handleConversationSystemAction(message: RioIncomingMessage) {
|
|
763
|
+
const action = (message.action ?? '').toLowerCase();
|
|
764
|
+
if (action === 'conversationrenamed') {
|
|
765
|
+
const data = message.data as Record<string, unknown>;
|
|
645
766
|
const id = this.extractString(data, ['conversationId', 'id']);
|
|
646
767
|
const newTitle = this.extractString(data, ['newTitle', 'title']);
|
|
647
768
|
if (id && newTitle) {
|
|
648
769
|
this.applyConversationRename(id, newTitle);
|
|
649
770
|
this.conversationHistoryError = '';
|
|
771
|
+
if (
|
|
772
|
+
this.pendingConversationAction &&
|
|
773
|
+
this.pendingConversationAction.conversationId === id &&
|
|
774
|
+
this.pendingConversationAction.action === 'rename'
|
|
775
|
+
) {
|
|
776
|
+
this.pendingConversationAction = null;
|
|
777
|
+
this.conversationActionError = null;
|
|
778
|
+
}
|
|
650
779
|
}
|
|
651
780
|
return true;
|
|
652
781
|
}
|
|
653
782
|
|
|
654
783
|
if (action === 'conversationdeleted') {
|
|
655
|
-
const data = message.data as Record<string, unknown>;
|
|
784
|
+
const data = message.data as Record<string, unknown>;
|
|
656
785
|
const id = this.extractString(data, ['conversationId', 'id']);
|
|
657
786
|
if (id) {
|
|
658
787
|
this.applyConversationDeletion(id);
|
|
659
788
|
this.conversationHistoryError = '';
|
|
789
|
+
if (
|
|
790
|
+
this.pendingConversationAction &&
|
|
791
|
+
this.pendingConversationAction.conversationId === id &&
|
|
792
|
+
this.pendingConversationAction.action === 'delete'
|
|
793
|
+
) {
|
|
794
|
+
this.pendingConversationAction = null;
|
|
795
|
+
this.conversationActionError = null;
|
|
796
|
+
}
|
|
660
797
|
}
|
|
661
798
|
return true;
|
|
662
799
|
}
|
|
663
|
-
|
|
800
|
+
|
|
664
801
|
if (action === 'processing') {
|
|
665
802
|
return true;
|
|
666
803
|
}
|
|
@@ -668,981 +805,1043 @@ export class RioAssistWidget extends LitElement {
|
|
|
668
805
|
return false;
|
|
669
806
|
}
|
|
670
807
|
|
|
671
|
-
private
|
|
672
|
-
|
|
808
|
+
private handleConversationActionError(message: RioIncomingMessage) {
|
|
809
|
+
const action = (message.action ?? '').toLowerCase();
|
|
810
|
+
if (action !== 'error') {
|
|
673
811
|
return false;
|
|
674
812
|
}
|
|
675
|
-
const normalized = action.toLowerCase();
|
|
676
|
-
return (
|
|
677
|
-
normalized === 'processing' ||
|
|
678
|
-
normalized === 'conversationrenamed' ||
|
|
679
|
-
normalized === 'conversationdeleted'
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
813
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (
|
|
693
|
-
|
|
814
|
+
const data = message.data as Record<string, unknown>;
|
|
815
|
+
const errorText =
|
|
816
|
+
this.extractString(data, ['error', 'message', 'detail', 'description']) ||
|
|
817
|
+
(typeof message.text === 'string' && message.text.trim()
|
|
818
|
+
? message.text
|
|
819
|
+
: 'O agente retornou um erro ao processar a conversa.');
|
|
820
|
+
|
|
821
|
+
const pending = this.pendingConversationAction;
|
|
822
|
+
if (pending) {
|
|
823
|
+
if (pending.action === 'rename') {
|
|
824
|
+
this.applyConversationRename(pending.conversationId, pending.originalTitle);
|
|
694
825
|
}
|
|
695
|
-
}
|
|
696
|
-
return null;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
handleHeaderActionClick(action: HeaderActionConfig, index: number) {
|
|
700
|
-
const detail = {
|
|
701
|
-
index,
|
|
702
|
-
id: action.id ?? null,
|
|
703
|
-
ariaLabel: action.ariaLabel ?? null,
|
|
704
|
-
iconUrl: action.iconUrl,
|
|
705
|
-
};
|
|
706
|
-
|
|
707
|
-
const allowed = this.dispatchEvent(
|
|
708
|
-
new CustomEvent('rioassist:header-action', {
|
|
709
|
-
detail,
|
|
710
|
-
bubbles: true,
|
|
711
|
-
composed: true,
|
|
712
|
-
cancelable: true,
|
|
713
|
-
}),
|
|
714
|
-
);
|
|
715
|
-
|
|
716
|
-
if (!allowed) {
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
if (typeof action.onClick === 'function') {
|
|
721
|
-
action.onClick();
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
handleCloseAction() {
|
|
726
|
-
if (this.isFullscreen) {
|
|
727
|
-
this.exitFullscreen(true);
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
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
|
-
|
|
762
|
-
handleCreateConversation() {
|
|
763
|
-
if (!this.hasActiveConversation) {
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
this.clearLoadingGuard();
|
|
768
|
-
this.isLoading = false;
|
|
769
|
-
this.messages = [];
|
|
770
|
-
this.message = '';
|
|
771
|
-
this.errorMessage = '';
|
|
772
|
-
this.showConversations = false;
|
|
773
|
-
this.teardownRioClient();
|
|
774
|
-
this.currentConversationId = this.generateConversationId();
|
|
775
|
-
this.activeConversationTitle = null;
|
|
776
|
-
this.showNewConversationShortcut = false;
|
|
777
|
-
this.dispatchEvent(
|
|
778
|
-
new CustomEvent('rioassist:new-conversation', {
|
|
779
|
-
bubbles: true,
|
|
780
|
-
composed: true,
|
|
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
826
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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 };
|
|
827
|
+
if (pending.action === 'delete') {
|
|
828
|
+
this.restoreConversationSnapshot(pending.snapshot, pending.index);
|
|
829
|
+
if (pending.wasActive) {
|
|
830
|
+
this.currentConversationId = pending.conversationId;
|
|
831
|
+
this.activeConversationTitle = pending.originalTitle;
|
|
832
|
+
this.messages = pending.messagesSnapshot ?? this.messages;
|
|
833
|
+
}
|
|
903
834
|
}
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
const ratio = clientHeight / scrollHeight;
|
|
908
|
-
const height = Math.max(ratio * 100, 8);
|
|
909
|
-
const maxTop = 100 - height;
|
|
910
|
-
const top =
|
|
911
|
-
scrollTop / (scrollHeight - clientHeight) * (maxTop > 0 ? maxTop : 0);
|
|
912
835
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
async onSuggestionClick(suggestion: string) {
|
|
921
|
-
await this.processMessage(suggestion);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
async handleSubmit(event: SubmitEvent) {
|
|
925
|
-
event.preventDefault();
|
|
926
|
-
await this.processMessage(this.message);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
private createMessage(role: ChatRole, text: string): ChatMessage {
|
|
930
|
-
const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
931
|
-
? crypto.randomUUID()
|
|
932
|
-
: `${Date.now()}-${Math.random()}`;
|
|
933
|
-
|
|
934
|
-
return {
|
|
935
|
-
id,
|
|
936
|
-
role,
|
|
937
|
-
text,
|
|
938
|
-
html: this.renderMarkdown(text),
|
|
939
|
-
timestamp: Date.now(),
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
private async processMessage(rawValue: string) {
|
|
944
|
-
const content = rawValue.trim();
|
|
945
|
-
if (!content || this.isLoading) {
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
if (!this.currentConversationId) {
|
|
950
|
-
this.currentConversationId = this.generateConversationId();
|
|
951
|
-
this.activeConversationTitle = null;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
const wasEmptyConversation = this.messages.length === 0;
|
|
955
|
-
|
|
956
|
-
this.dispatchEvent(
|
|
957
|
-
new CustomEvent('rioassist:send', {
|
|
958
|
-
detail: {
|
|
959
|
-
message: content,
|
|
960
|
-
apiBaseUrl: this.apiBaseUrl,
|
|
961
|
-
token: this.rioToken,
|
|
962
|
-
},
|
|
963
|
-
bubbles: true,
|
|
964
|
-
composed: true,
|
|
965
|
-
}),
|
|
966
|
-
);
|
|
967
|
-
|
|
968
|
-
const userMessage = this.createMessage('user', content);
|
|
969
|
-
this.messages = [...this.messages, userMessage];
|
|
970
|
-
if (wasEmptyConversation) {
|
|
971
|
-
this.showNewConversationShortcut = true;
|
|
972
|
-
this.refreshConversationsAfterResponse = true;
|
|
973
|
-
}
|
|
974
|
-
this.message = '';
|
|
975
|
-
this.errorMessage = '';
|
|
976
|
-
this.isLoading = true;
|
|
977
|
-
this.startLoadingGuard();
|
|
978
|
-
|
|
979
|
-
try {
|
|
980
|
-
const client = this.ensureRioClient();
|
|
981
|
-
await client.sendMessage(content, this.currentConversationId);
|
|
982
|
-
} catch (error) {
|
|
836
|
+
this.conversationActionError = {
|
|
837
|
+
...pending,
|
|
838
|
+
message: errorText,
|
|
839
|
+
};
|
|
840
|
+
this.pendingConversationAction = null;
|
|
983
841
|
this.clearLoadingGuard();
|
|
984
842
|
this.isLoading = false;
|
|
985
|
-
this.errorMessage = error instanceof Error
|
|
986
|
-
? error.message
|
|
987
|
-
: 'Nao foi possivel enviar a mensagem para o agente.';
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
private ensureRioClient() {
|
|
992
|
-
const token = this.rioToken.trim();
|
|
993
|
-
if (!token) {
|
|
994
|
-
throw new Error(
|
|
995
|
-
'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
|
|
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
|
-
}
|
|
1006
|
-
|
|
1007
|
-
return this.rioClient;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
private handleIncomingMessage(message: RioIncomingMessage) {
|
|
1011
|
-
if (this.isHistoryPayload(message)) {
|
|
1012
|
-
this.logHistoryPayload(message);
|
|
1013
|
-
this.handleHistoryPayload(message.data);
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
if (this.handleConversationSystemAction(message)) {
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
if (this.shouldIgnoreAssistantPayload(message.action)) {
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
console.info('[RioAssist][ws] resposta de mensagem recebida', {
|
|
1026
|
-
action: message.action ?? 'message',
|
|
1027
|
-
text: message.text,
|
|
1028
|
-
raw: message.raw,
|
|
1029
|
-
data: message.data,
|
|
1030
|
-
});
|
|
1031
|
-
|
|
1032
|
-
const assistantMessage = this.createMessage('assistant', message.text);
|
|
1033
|
-
this.messages = [...this.messages, assistantMessage];
|
|
1034
|
-
this.clearLoadingGuard();
|
|
1035
|
-
this.isLoading = false;
|
|
1036
|
-
|
|
1037
|
-
if (this.refreshConversationsAfterResponse) {
|
|
1038
|
-
this.refreshConversationsAfterResponse = false;
|
|
1039
|
-
this.requestConversationHistory();
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
private teardownRioClient() {
|
|
1044
|
-
if (this.rioUnsubscribe) {
|
|
1045
|
-
this.rioUnsubscribe();
|
|
1046
|
-
this.rioUnsubscribe = null;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
if (this.rioClient) {
|
|
1050
|
-
this.rioClient.close();
|
|
1051
|
-
this.rioClient = null;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
async requestConversationHistory(conversationId?: string) {
|
|
1056
|
-
try {
|
|
1057
|
-
const client = this.ensureRioClient();
|
|
1058
|
-
const limit = 50;
|
|
1059
|
-
|
|
1060
|
-
console.info('[RioAssist][history] solicitando historico de conversas', {
|
|
1061
|
-
conversationId: conversationId ?? null,
|
|
1062
|
-
limit,
|
|
1063
|
-
});
|
|
1064
|
-
|
|
1065
|
-
this.conversationHistoryError = '';
|
|
1066
|
-
this.conversationHistoryLoading = true;
|
|
1067
|
-
await client.requestHistory({ conversationId, limit });
|
|
1068
|
-
} catch (error) {
|
|
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.';
|
|
1074
|
-
this.conversationHistoryLoading = false;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
private handleHistoryPayload(payload: unknown) {
|
|
1079
|
-
const entries = this.extractHistoryEntries(payload);
|
|
1080
|
-
const conversationId = this.extractConversationId(payload);
|
|
1081
|
-
|
|
1082
|
-
if (conversationId !== null && conversationId !== undefined) {
|
|
1083
|
-
this.applyMessageHistory(entries, conversationId);
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
if (this.isMessageHistoryEntries(entries)) {
|
|
1088
|
-
this.applyMessageHistory(entries);
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
this.applyConversationHistoryFromEntries(entries);
|
|
1093
|
-
|
|
1094
|
-
if (this.refreshConversationsAfterResponse) {
|
|
1095
|
-
this.refreshConversationsAfterResponse = false;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
private isHistoryPayload(message: RioIncomingMessage) {
|
|
1100
|
-
if (
|
|
1101
|
-
typeof message.action === 'string' &&
|
|
1102
|
-
message.action.toLowerCase().includes('history')
|
|
1103
|
-
) {
|
|
1104
843
|
return true;
|
|
1105
844
|
}
|
|
1106
845
|
|
|
1107
|
-
|
|
1108
|
-
if (data && typeof data === 'object') {
|
|
1109
|
-
const action = (data as any).action;
|
|
1110
|
-
if (typeof action === 'string' && action.toLowerCase().includes('history')) {
|
|
1111
|
-
return true;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
if (Array.isArray((data as any).history) || Array.isArray((data as any).conversations)) {
|
|
1115
|
-
return true;
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
return false;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
private logHistoryPayload(message: RioIncomingMessage) {
|
|
1123
|
-
const label = '[RioAssist][history] payload recebido do websocket';
|
|
1124
|
-
if (message.data !== null && message.data !== undefined) {
|
|
1125
|
-
console.info(label, message.data);
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
console.info(label, message.raw);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
private applyConversationHistoryFromEntries(entries: unknown[]) {
|
|
1133
|
-
if (entries.length === 0) {
|
|
1134
|
-
console.info('[RioAssist][history] payload sem itens para montar lista de conversas');
|
|
1135
|
-
this.conversations = [];
|
|
1136
|
-
this.conversationHistoryLoading = false;
|
|
1137
|
-
this.conversationHistoryError = '';
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
const map = new Map<string, ConversationItem>();
|
|
1142
|
-
|
|
1143
|
-
entries.forEach((entry, index) => {
|
|
1144
|
-
if (!entry || typeof entry !== 'object') {
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
const normalized = this.normalizeConversationItem(
|
|
1149
|
-
entry as Record<string, unknown>,
|
|
1150
|
-
index,
|
|
1151
|
-
);
|
|
1152
|
-
|
|
1153
|
-
if (!normalized) {
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
const current = map.get(normalized.id);
|
|
1158
|
-
if (!current) {
|
|
1159
|
-
map.set(normalized.id, normalized);
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
const currentTime = Date.parse(current.updatedAt);
|
|
1164
|
-
const nextTime = Date.parse(normalized.updatedAt);
|
|
1165
|
-
|
|
1166
|
-
if (Number.isFinite(nextTime) && nextTime > currentTime) {
|
|
1167
|
-
map.set(normalized.id, normalized);
|
|
1168
|
-
}
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
const conversations = Array.from(map.values()).sort((a, b) => {
|
|
1172
|
-
const order = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
|
|
1173
|
-
return Number.isFinite(order) ? order : 0;
|
|
1174
|
-
});
|
|
1175
|
-
|
|
1176
|
-
this.conversations = conversations;
|
|
1177
|
-
this.conversationHistoryLoading = false;
|
|
1178
|
-
this.conversationHistoryError = '';
|
|
1179
|
-
this.syncActiveConversationTitle();
|
|
1180
|
-
console.info('[RioAssist][history] conversas normalizadas', conversations);
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
private applyMessageHistory(entries: unknown[], conversationId?: string | null) {
|
|
1184
|
-
if (entries.length === 0) {
|
|
1185
|
-
console.info('[RioAssist][history] lista de mensagens vazia', { conversationId });
|
|
1186
|
-
this.messages = [];
|
|
1187
|
-
this.showConversations = false;
|
|
1188
|
-
this.clearLoadingGuard();
|
|
1189
|
-
this.isLoading = false;
|
|
1190
|
-
this.conversationHistoryLoading = false;
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
const normalized = entries.flatMap((entry, index) =>
|
|
1195
|
-
this.normalizeHistoryMessages(entry as Record<string, unknown>, index),
|
|
1196
|
-
);
|
|
1197
|
-
|
|
1198
|
-
if (conversationId) {
|
|
1199
|
-
this.currentConversationId = conversationId;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
this.messages = normalized;
|
|
1203
|
-
this.showConversations = false;
|
|
846
|
+
this.errorMessage = errorText;
|
|
1204
847
|
this.clearLoadingGuard();
|
|
1205
848
|
this.isLoading = false;
|
|
1206
|
-
|
|
1207
|
-
this.conversationHistoryLoading = false;
|
|
1208
|
-
this.refreshConversationsAfterResponse = false;
|
|
1209
|
-
|
|
1210
|
-
console.info('[RioAssist][history] mensagens carregadas', {
|
|
1211
|
-
conversationId: conversationId ?? null,
|
|
1212
|
-
total: normalized.length,
|
|
1213
|
-
});
|
|
849
|
+
return true;
|
|
1214
850
|
}
|
|
1215
851
|
|
|
1216
|
-
private
|
|
1217
|
-
if (
|
|
1218
|
-
return payload;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
if (payload && typeof payload === 'object') {
|
|
1222
|
-
const record = payload as Record<string, unknown>;
|
|
1223
|
-
const candidates = [
|
|
1224
|
-
record.history,
|
|
1225
|
-
record.conversations,
|
|
1226
|
-
record.data,
|
|
1227
|
-
record.items,
|
|
1228
|
-
record.messages,
|
|
1229
|
-
];
|
|
1230
|
-
|
|
1231
|
-
for (const candidate of candidates) {
|
|
1232
|
-
if (Array.isArray(candidate)) {
|
|
1233
|
-
return candidate;
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
if (record.data && typeof record.data === 'object' && !Array.isArray(record.data)) {
|
|
1238
|
-
const nested = this.extractHistoryEntries(record.data);
|
|
1239
|
-
if (nested.length > 0) {
|
|
1240
|
-
return nested;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
return [];
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
private extractConversationId(payload: unknown): string | null | undefined {
|
|
1249
|
-
if (payload && typeof payload === 'object') {
|
|
1250
|
-
const record = payload as Record<string, unknown>;
|
|
1251
|
-
const candidates = [
|
|
1252
|
-
record.conversationId,
|
|
1253
|
-
record.conversationUUID,
|
|
1254
|
-
record.conversationUuid,
|
|
1255
|
-
record.uuid,
|
|
1256
|
-
record.id,
|
|
1257
|
-
];
|
|
1258
|
-
|
|
1259
|
-
for (const candidate of candidates) {
|
|
1260
|
-
if (candidate === null) {
|
|
1261
|
-
return null;
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
if (candidate !== undefined) {
|
|
1265
|
-
return String(candidate);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
return undefined;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
private isMessageHistoryEntries(entries: unknown[]) {
|
|
1274
|
-
return entries.some((entry) => this.looksLikeMessageHistoryEntry(entry));
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
private looksLikeMessageHistoryEntry(entry: unknown) {
|
|
1278
|
-
if (!entry || typeof entry !== 'object') {
|
|
852
|
+
private shouldIgnoreAssistantPayload(action?: string) {
|
|
853
|
+
if (!action) {
|
|
1279
854
|
return false;
|
|
1280
855
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
) {
|
|
1294
|
-
return
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
:
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
if (
|
|
856
|
+
const normalized = action.toLowerCase();
|
|
857
|
+
return (
|
|
858
|
+
normalized === 'processing' ||
|
|
859
|
+
normalized === 'conversationrenamed' ||
|
|
860
|
+
normalized === 'conversationdeleted'
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
private extractString(
|
|
865
|
+
data: Record<string, unknown> | undefined,
|
|
866
|
+
keys: string[],
|
|
867
|
+
): string | null {
|
|
868
|
+
if (!data || typeof data !== 'object') {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
for (const key of keys) {
|
|
872
|
+
const value = data[key];
|
|
873
|
+
if (typeof value === 'string' && value.trim()) {
|
|
874
|
+
return value;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
handleHeaderActionClick(action: HeaderActionConfig, index: number) {
|
|
881
|
+
const detail = {
|
|
882
|
+
index,
|
|
883
|
+
id: action.id ?? null,
|
|
884
|
+
ariaLabel: action.ariaLabel ?? null,
|
|
885
|
+
iconUrl: action.iconUrl,
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
const allowed = this.dispatchEvent(
|
|
889
|
+
new CustomEvent('rioassist:header-action', {
|
|
890
|
+
detail,
|
|
891
|
+
bubbles: true,
|
|
892
|
+
composed: true,
|
|
893
|
+
cancelable: true,
|
|
894
|
+
}),
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
if (!allowed) {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (typeof action.onClick === 'function') {
|
|
902
|
+
action.onClick();
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
handleCloseAction() {
|
|
907
|
+
if (this.isFullscreen) {
|
|
908
|
+
this.exitFullscreen(true);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (this.showConversations) {
|
|
913
|
+
this.closeConversationsPanel();
|
|
914
|
+
} else {
|
|
915
|
+
this.closePanel();
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
enterFullscreen() {
|
|
920
|
+
if (this.isFullscreen) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
this.isFullscreen = true;
|
|
925
|
+
this.open = false;
|
|
926
|
+
this.showConversations = false;
|
|
927
|
+
this.requestConversationHistory();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
exitFullscreen(restorePanel: boolean) {
|
|
931
|
+
if (!this.isFullscreen) {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
this.isFullscreen = false;
|
|
936
|
+
this.conversationMenuId = null;
|
|
937
|
+
this.showNewConversationShortcut = false;
|
|
938
|
+
if (restorePanel) {
|
|
939
|
+
this.open = true;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
handleCreateConversation() {
|
|
944
|
+
if (!this.hasActiveConversation) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
this.clearLoadingGuard();
|
|
949
|
+
this.isLoading = false;
|
|
950
|
+
this.messages = [];
|
|
951
|
+
this.message = '';
|
|
952
|
+
this.errorMessage = '';
|
|
953
|
+
this.showConversations = false;
|
|
954
|
+
this.teardownRioClient();
|
|
955
|
+
this.currentConversationId = this.generateConversationId();
|
|
956
|
+
this.activeConversationTitle = null;
|
|
957
|
+
this.showNewConversationShortcut = false;
|
|
958
|
+
this.dispatchEvent(
|
|
959
|
+
new CustomEvent('rioassist:new-conversation', {
|
|
960
|
+
bubbles: true,
|
|
961
|
+
composed: true,
|
|
962
|
+
}),
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
handleConversationListScroll(event: Event) {
|
|
967
|
+
const target = event.currentTarget as HTMLElement | null;
|
|
968
|
+
if (!target) {
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
this.updateConversationScrollbar(target);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
handleConversationScrollbarPointerDown(event: PointerEvent) {
|
|
975
|
+
const track = event.currentTarget as HTMLElement | null;
|
|
976
|
+
const list = this.renderRoot.querySelector(
|
|
977
|
+
'.conversation-list--sidebar',
|
|
978
|
+
) as HTMLElement | null;
|
|
979
|
+
|
|
980
|
+
if (!track || !list) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const trackRect = track.getBoundingClientRect();
|
|
985
|
+
const thumbHeight = trackRect.height * (this.conversationScrollbar.height / 100);
|
|
986
|
+
const maxThumbTop = Math.max(trackRect.height - thumbHeight, 0);
|
|
987
|
+
const scrollRange = Math.max(list.scrollHeight - list.clientHeight, 1);
|
|
988
|
+
const currentThumbTop = (list.scrollTop / scrollRange) * maxThumbTop;
|
|
989
|
+
const offsetY = event.clientY - trackRect.top;
|
|
990
|
+
const isOnThumb = offsetY >= currentThumbTop && offsetY <= currentThumbTop + thumbHeight;
|
|
991
|
+
|
|
992
|
+
const nextThumbTop = isOnThumb
|
|
993
|
+
? currentThumbTop
|
|
994
|
+
: Math.min(Math.max(offsetY - thumbHeight / 2, 0), maxThumbTop);
|
|
995
|
+
|
|
996
|
+
if (!isOnThumb) {
|
|
997
|
+
list.scrollTop = (nextThumbTop / Math.max(maxThumbTop, 1)) * (list.scrollHeight - list.clientHeight);
|
|
998
|
+
this.updateConversationScrollbar(list);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
track.setPointerCapture(event.pointerId);
|
|
1002
|
+
this.conversationScrollbarDraggingId = event.pointerId;
|
|
1003
|
+
this.conversationScrollbarDragState = {
|
|
1004
|
+
startY: event.clientY,
|
|
1005
|
+
startThumbTop: nextThumbTop,
|
|
1006
|
+
trackHeight: trackRect.height,
|
|
1007
|
+
thumbHeight,
|
|
1008
|
+
list,
|
|
1009
|
+
};
|
|
1010
|
+
event.preventDefault();
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
handleConversationScrollbarPointerMove(event: PointerEvent) {
|
|
1014
|
+
if (
|
|
1015
|
+
this.conversationScrollbarDraggingId === null ||
|
|
1016
|
+
this.conversationScrollbarDraggingId !== event.pointerId ||
|
|
1017
|
+
!this.conversationScrollbarDragState
|
|
1018
|
+
) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const {
|
|
1023
|
+
startY,
|
|
1024
|
+
startThumbTop,
|
|
1025
|
+
trackHeight,
|
|
1026
|
+
thumbHeight,
|
|
1027
|
+
list,
|
|
1028
|
+
} = this.conversationScrollbarDragState;
|
|
1029
|
+
|
|
1030
|
+
const maxThumbTop = Math.max(trackHeight - thumbHeight, 0);
|
|
1031
|
+
const deltaY = event.clientY - startY;
|
|
1032
|
+
const thumbTop = Math.min(Math.max(startThumbTop + deltaY, 0), maxThumbTop);
|
|
1033
|
+
const scrollRange = list.scrollHeight - list.clientHeight;
|
|
1034
|
+
|
|
1035
|
+
if (scrollRange > 0) {
|
|
1036
|
+
list.scrollTop = (thumbTop / Math.max(maxThumbTop, 1)) * scrollRange;
|
|
1037
|
+
this.updateConversationScrollbar(list);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
event.preventDefault();
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
handleConversationScrollbarPointerUp(event: PointerEvent) {
|
|
1044
|
+
if (this.conversationScrollbarDraggingId !== event.pointerId) {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const track = event.currentTarget as HTMLElement | null;
|
|
1049
|
+
track?.releasePointerCapture(event.pointerId);
|
|
1050
|
+
|
|
1051
|
+
this.conversationScrollbarDraggingId = null;
|
|
1052
|
+
this.conversationScrollbarDragState = null;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private enqueueConversationScrollbarMeasure() {
|
|
1056
|
+
if (this.conversationScrollbarRaf !== null) {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
this.conversationScrollbarRaf = requestAnimationFrame(() => {
|
|
1061
|
+
this.conversationScrollbarRaf = null;
|
|
1062
|
+
this.updateConversationScrollbar();
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
private updateConversationScrollbar(target?: HTMLElement | null) {
|
|
1067
|
+
const element =
|
|
1068
|
+
target ??
|
|
1069
|
+
(this.renderRoot.querySelector(
|
|
1070
|
+
'.conversation-list--sidebar',
|
|
1071
|
+
) as HTMLElement | null);
|
|
1072
|
+
|
|
1073
|
+
if (!element) {
|
|
1074
|
+
if (this.conversationScrollbar.visible) {
|
|
1075
|
+
this.conversationScrollbar = { height: 0, top: 0, visible: false };
|
|
1076
|
+
}
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const { scrollHeight, clientHeight, scrollTop } = element;
|
|
1081
|
+
if (scrollHeight <= clientHeight + 1) {
|
|
1082
|
+
if (this.conversationScrollbar.visible) {
|
|
1083
|
+
this.conversationScrollbar = { height: 0, top: 0, visible: false };
|
|
1084
|
+
}
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const ratio = clientHeight / scrollHeight;
|
|
1089
|
+
const height = Math.max(ratio * 100, 8);
|
|
1090
|
+
const maxTop = 100 - height;
|
|
1091
|
+
const top =
|
|
1092
|
+
scrollTop / (scrollHeight - clientHeight) * (maxTop > 0 ? maxTop : 0);
|
|
1093
|
+
|
|
1094
|
+
this.conversationScrollbar = {
|
|
1095
|
+
height,
|
|
1096
|
+
top,
|
|
1097
|
+
visible: true,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async onSuggestionClick(suggestion: string) {
|
|
1102
|
+
await this.processMessage(suggestion);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async handleSubmit(event: SubmitEvent) {
|
|
1106
|
+
event.preventDefault();
|
|
1107
|
+
await this.processMessage(this.message);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
private createMessage(role: ChatRole, text: string): ChatMessage {
|
|
1111
|
+
const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
1112
|
+
? crypto.randomUUID()
|
|
1113
|
+
: `${Date.now()}-${Math.random()}`;
|
|
1114
|
+
|
|
1115
|
+
return {
|
|
1116
|
+
id,
|
|
1117
|
+
role,
|
|
1118
|
+
text,
|
|
1119
|
+
html: this.renderMarkdown(text),
|
|
1120
|
+
timestamp: Date.now(),
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
private async processMessage(rawValue: string) {
|
|
1125
|
+
const content = rawValue.trim();
|
|
1126
|
+
if (!content || this.isLoading) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (!this.currentConversationId) {
|
|
1131
|
+
this.currentConversationId = this.generateConversationId();
|
|
1132
|
+
this.activeConversationTitle = null;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const wasEmptyConversation = this.messages.length === 0;
|
|
1136
|
+
|
|
1137
|
+
this.dispatchEvent(
|
|
1138
|
+
new CustomEvent('rioassist:send', {
|
|
1139
|
+
detail: {
|
|
1140
|
+
message: content,
|
|
1141
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
1142
|
+
token: this.rioToken,
|
|
1143
|
+
},
|
|
1144
|
+
bubbles: true,
|
|
1145
|
+
composed: true,
|
|
1146
|
+
}),
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1149
|
+
const userMessage = this.createMessage('user', content);
|
|
1150
|
+
this.messages = [...this.messages, userMessage];
|
|
1151
|
+
if (wasEmptyConversation) {
|
|
1152
|
+
this.showNewConversationShortcut = true;
|
|
1153
|
+
this.refreshConversationsAfterResponse = true;
|
|
1154
|
+
}
|
|
1155
|
+
this.message = '';
|
|
1156
|
+
this.errorMessage = '';
|
|
1157
|
+
this.isLoading = true;
|
|
1158
|
+
this.startLoadingGuard();
|
|
1159
|
+
|
|
1160
|
+
try {
|
|
1161
|
+
const client = this.ensureRioClient();
|
|
1162
|
+
await client.sendMessage(content, this.currentConversationId);
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
this.clearLoadingGuard();
|
|
1165
|
+
this.isLoading = false;
|
|
1166
|
+
this.errorMessage = error instanceof Error
|
|
1167
|
+
? error.message
|
|
1168
|
+
: 'Nao foi possivel enviar a mensagem para o agente.';
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
private ensureRioClient() {
|
|
1173
|
+
const token = this.rioToken.trim();
|
|
1174
|
+
if (!token) {
|
|
1175
|
+
throw new Error(
|
|
1176
|
+
'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (!this.rioClient || !this.rioClient.matchesToken(token)) {
|
|
1181
|
+
this.teardownRioClient();
|
|
1182
|
+
this.rioClient = new RioWebsocketClient(token);
|
|
1183
|
+
this.rioUnsubscribe = this.rioClient.onMessage((incoming) => {
|
|
1184
|
+
this.handleIncomingMessage(incoming);
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return this.rioClient;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
private handleIncomingMessage(message: RioIncomingMessage) {
|
|
1192
|
+
if (this.isHistoryPayload(message)) {
|
|
1193
|
+
this.logHistoryPayload(message);
|
|
1194
|
+
this.handleHistoryPayload(message.data);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (this.handleConversationSystemAction(message)) {
|
|
1507
1199
|
return;
|
|
1508
1200
|
}
|
|
1509
1201
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
this.activeConversationTitle = title;
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
private toIsoString(value: unknown) {
|
|
1517
|
-
if (typeof value === 'string' || typeof value === 'number') {
|
|
1518
|
-
const date = new Date(value);
|
|
1519
|
-
if (!Number.isNaN(date.getTime())) {
|
|
1520
|
-
return date.toISOString();
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
return new Date().toISOString();
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
private startLoadingGuard() {
|
|
1528
|
-
this.clearLoadingGuard();
|
|
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);
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
private clearLoadingGuard() {
|
|
1546
|
-
if (this.loadingTimer !== null) {
|
|
1547
|
-
window.clearTimeout(this.loadingTimer);
|
|
1548
|
-
this.loadingTimer = null;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
if (this.loadingTimerSlow !== null) {
|
|
1552
|
-
window.clearTimeout(this.loadingTimerSlow);
|
|
1553
|
-
this.loadingTimerSlow = null;
|
|
1202
|
+
if (this.handleConversationActionError(message)) {
|
|
1203
|
+
return;
|
|
1554
1204
|
}
|
|
1555
1205
|
|
|
1556
|
-
if (this.
|
|
1557
|
-
|
|
1558
|
-
this.loadingTimerTimeout = null;
|
|
1206
|
+
if (this.shouldIgnoreAssistantPayload(message.action)) {
|
|
1207
|
+
return;
|
|
1559
1208
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
const
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
}
|
|
1209
|
+
|
|
1210
|
+
console.info('[RioAssist][ws] resposta de mensagem recebida', {
|
|
1211
|
+
action: message.action ?? 'message',
|
|
1212
|
+
text: message.text,
|
|
1213
|
+
raw: message.raw,
|
|
1214
|
+
data: message.data,
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// Handle "processing" type messages - just keep loading state, don't create message
|
|
1218
|
+
if (message.action === 'processing') {
|
|
1219
|
+
console.info('[RioAssist][ws] processando mensagem - aguardando resposta final');
|
|
1220
|
+
// Keep isLoading = true, don't create a message balloon
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const assistantMessage = this.createMessage('assistant', message.text);
|
|
1225
|
+
this.messages = [...this.messages, assistantMessage];
|
|
1226
|
+
this.clearLoadingGuard();
|
|
1227
|
+
this.isLoading = false;
|
|
1228
|
+
|
|
1229
|
+
if (this.refreshConversationsAfterResponse) {
|
|
1230
|
+
this.refreshConversationsAfterResponse = false;
|
|
1231
|
+
this.requestConversationHistory();
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
private teardownRioClient() {
|
|
1236
|
+
if (this.rioUnsubscribe) {
|
|
1237
|
+
this.rioUnsubscribe();
|
|
1238
|
+
this.rioUnsubscribe = null;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
if (this.rioClient) {
|
|
1242
|
+
this.rioClient.close();
|
|
1243
|
+
this.rioClient = null;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
async requestConversationHistory(conversationId?: string) {
|
|
1248
|
+
try {
|
|
1249
|
+
const client = this.ensureRioClient();
|
|
1250
|
+
const limit = 50;
|
|
1251
|
+
|
|
1252
|
+
console.info('[RioAssist][history] solicitando historico de conversas', {
|
|
1253
|
+
conversationId: conversationId ?? null,
|
|
1254
|
+
limit,
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
this.conversationHistoryError = '';
|
|
1258
|
+
this.conversationHistoryLoading = true;
|
|
1259
|
+
await client.requestHistory({ conversationId, limit });
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
console.error('[RioAssist][history] erro ao solicitar historico', error);
|
|
1262
|
+
this.conversationHistoryError =
|
|
1263
|
+
error instanceof Error && error.message
|
|
1264
|
+
? error.message
|
|
1265
|
+
: 'Nao foi possivel carregar as conversas.';
|
|
1266
|
+
this.conversationHistoryLoading = false;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
private handleHistoryPayload(payload: unknown) {
|
|
1271
|
+
const entries = this.extractHistoryEntries(payload);
|
|
1272
|
+
const conversationId = this.extractConversationId(payload);
|
|
1273
|
+
|
|
1274
|
+
if (conversationId !== null && conversationId !== undefined) {
|
|
1275
|
+
this.applyMessageHistory(entries, conversationId);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
if (this.isMessageHistoryEntries(entries)) {
|
|
1280
|
+
this.applyMessageHistory(entries);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
this.applyConversationHistoryFromEntries(entries);
|
|
1285
|
+
|
|
1286
|
+
if (this.refreshConversationsAfterResponse) {
|
|
1287
|
+
this.refreshConversationsAfterResponse = false;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
private isHistoryPayload(message: RioIncomingMessage) {
|
|
1292
|
+
if (
|
|
1293
|
+
typeof message.action === 'string' &&
|
|
1294
|
+
message.action.toLowerCase().includes('history')
|
|
1295
|
+
) {
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const data = message.data;
|
|
1300
|
+
if (data && typeof data === 'object') {
|
|
1301
|
+
const action = (data as any).action;
|
|
1302
|
+
if (typeof action === 'string' && action.toLowerCase().includes('history')) {
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (Array.isArray((data as any).history) || Array.isArray((data as any).conversations)) {
|
|
1307
|
+
return true;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
return false;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
private logHistoryPayload(message: RioIncomingMessage) {
|
|
1315
|
+
const label = '[RioAssist][history] payload recebido do websocket';
|
|
1316
|
+
if (message.data !== null && message.data !== undefined) {
|
|
1317
|
+
console.info(label, message.data);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
console.info(label, message.raw);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
private applyConversationHistoryFromEntries(entries: unknown[]) {
|
|
1325
|
+
if (entries.length === 0) {
|
|
1326
|
+
console.info('[RioAssist][history] payload sem itens para montar lista de conversas');
|
|
1327
|
+
this.conversations = [];
|
|
1328
|
+
this.conversationHistoryLoading = false;
|
|
1329
|
+
this.conversationHistoryError = '';
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const map = new Map<string, ConversationItem>();
|
|
1334
|
+
|
|
1335
|
+
entries.forEach((entry, index) => {
|
|
1336
|
+
if (!entry || typeof entry !== 'object') {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const normalized = this.normalizeConversationItem(
|
|
1341
|
+
entry as Record<string, unknown>,
|
|
1342
|
+
index,
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
if (!normalized) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const current = map.get(normalized.id);
|
|
1350
|
+
if (!current) {
|
|
1351
|
+
map.set(normalized.id, normalized);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
const currentTime = Date.parse(current.updatedAt);
|
|
1356
|
+
const nextTime = Date.parse(normalized.updatedAt);
|
|
1357
|
+
|
|
1358
|
+
if (Number.isFinite(nextTime) && nextTime > currentTime) {
|
|
1359
|
+
map.set(normalized.id, normalized);
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
const conversations = Array.from(map.values()).sort((a, b) => {
|
|
1364
|
+
const order = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
|
|
1365
|
+
return Number.isFinite(order) ? order : 0;
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
this.conversations = conversations;
|
|
1369
|
+
this.conversationHistoryLoading = false;
|
|
1370
|
+
this.conversationHistoryError = '';
|
|
1371
|
+
this.syncActiveConversationTitle();
|
|
1372
|
+
console.info('[RioAssist][history] conversas normalizadas', conversations);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
private applyMessageHistory(entries: unknown[], conversationId?: string | null) {
|
|
1376
|
+
if (entries.length === 0) {
|
|
1377
|
+
console.info('[RioAssist][history] lista de mensagens vazia', { conversationId });
|
|
1378
|
+
this.messages = [];
|
|
1379
|
+
this.showConversations = false;
|
|
1380
|
+
this.clearLoadingGuard();
|
|
1381
|
+
this.isLoading = false;
|
|
1382
|
+
this.conversationHistoryLoading = false;
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const normalized = entries.flatMap((entry, index) =>
|
|
1387
|
+
this.normalizeHistoryMessages(entry as Record<string, unknown>, index),
|
|
1388
|
+
);
|
|
1389
|
+
|
|
1390
|
+
if (conversationId) {
|
|
1391
|
+
this.currentConversationId = conversationId;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
this.messages = normalized;
|
|
1395
|
+
this.showConversations = false;
|
|
1396
|
+
this.clearLoadingGuard();
|
|
1397
|
+
this.isLoading = false;
|
|
1398
|
+
this.showNewConversationShortcut = normalized.length > 0;
|
|
1399
|
+
this.conversationHistoryLoading = false;
|
|
1400
|
+
this.refreshConversationsAfterResponse = false;
|
|
1401
|
+
|
|
1402
|
+
console.info('[RioAssist][history] mensagens carregadas', {
|
|
1403
|
+
conversationId: conversationId ?? null,
|
|
1404
|
+
total: normalized.length,
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
private extractHistoryEntries(payload: unknown): unknown[] {
|
|
1409
|
+
if (Array.isArray(payload)) {
|
|
1410
|
+
return payload;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
if (payload && typeof payload === 'object') {
|
|
1414
|
+
const record = payload as Record<string, unknown>;
|
|
1415
|
+
const candidates = [
|
|
1416
|
+
record.history,
|
|
1417
|
+
record.conversations,
|
|
1418
|
+
record.data,
|
|
1419
|
+
record.items,
|
|
1420
|
+
record.messages,
|
|
1421
|
+
];
|
|
1422
|
+
|
|
1423
|
+
for (const candidate of candidates) {
|
|
1424
|
+
if (Array.isArray(candidate)) {
|
|
1425
|
+
return candidate;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
if (record.data && typeof record.data === 'object' && !Array.isArray(record.data)) {
|
|
1430
|
+
const nested = this.extractHistoryEntries(record.data);
|
|
1431
|
+
if (nested.length > 0) {
|
|
1432
|
+
return nested;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
return [];
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
private extractConversationId(payload: unknown): string | null | undefined {
|
|
1441
|
+
if (payload && typeof payload === 'object') {
|
|
1442
|
+
const record = payload as Record<string, unknown>;
|
|
1443
|
+
const candidates = [
|
|
1444
|
+
record.conversationId,
|
|
1445
|
+
record.conversationUUID,
|
|
1446
|
+
record.conversationUuid,
|
|
1447
|
+
record.uuid,
|
|
1448
|
+
record.id,
|
|
1449
|
+
];
|
|
1450
|
+
|
|
1451
|
+
for (const candidate of candidates) {
|
|
1452
|
+
if (candidate === null) {
|
|
1453
|
+
return null;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (candidate !== undefined) {
|
|
1457
|
+
return String(candidate);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
return undefined;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
private isMessageHistoryEntries(entries: unknown[]) {
|
|
1466
|
+
return entries.some((entry) => this.looksLikeMessageHistoryEntry(entry));
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
private looksLikeMessageHistoryEntry(entry: unknown) {
|
|
1470
|
+
if (!entry || typeof entry !== 'object') {
|
|
1471
|
+
return false;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const item = entry as Record<string, unknown>;
|
|
1475
|
+
const role = item.role ?? item.sender ?? item.from ?? item.author ?? item.type;
|
|
1476
|
+
if (typeof role === 'string' && role.trim().length > 0) {
|
|
1477
|
+
return true;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if (
|
|
1481
|
+
typeof item.content === 'string' ||
|
|
1482
|
+
typeof item.message === 'string' ||
|
|
1483
|
+
typeof item.text === 'string' ||
|
|
1484
|
+
typeof item.response === 'string'
|
|
1485
|
+
) {
|
|
1486
|
+
return true;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
if (Array.isArray(item.parts) && item.parts.length > 0) {
|
|
1490
|
+
return true;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
return false;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
private normalizeConversationItem(
|
|
1497
|
+
value: Record<string, unknown>,
|
|
1498
|
+
index: number,
|
|
1499
|
+
): ConversationItem | null {
|
|
1500
|
+
const rawId =
|
|
1501
|
+
value.id ??
|
|
1502
|
+
value.conversationId ??
|
|
1503
|
+
value.conversationUUID ??
|
|
1504
|
+
value.conversationUuid ??
|
|
1505
|
+
value.uuid;
|
|
1506
|
+
|
|
1507
|
+
const id = rawId !== undefined && rawId !== null ? String(rawId) : `history-${index + 1}`;
|
|
1508
|
+
|
|
1509
|
+
const rawTitle =
|
|
1510
|
+
value.title ??
|
|
1511
|
+
value.name ??
|
|
1512
|
+
value.topic ??
|
|
1513
|
+
value.subject ??
|
|
1514
|
+
value.question ??
|
|
1515
|
+
value.query ??
|
|
1516
|
+
value.message;
|
|
1517
|
+
|
|
1518
|
+
const title =
|
|
1519
|
+
typeof rawTitle === 'string' && rawTitle.trim().length > 0
|
|
1520
|
+
? rawTitle.trim()
|
|
1521
|
+
: `Conversa ${index + 1}`;
|
|
1522
|
+
|
|
1523
|
+
const rawUpdated =
|
|
1524
|
+
value.updatedAt ??
|
|
1525
|
+
value.updated_at ??
|
|
1526
|
+
value.lastMessageAt ??
|
|
1527
|
+
value.last_message_at ??
|
|
1528
|
+
value.createdAt ??
|
|
1529
|
+
value.created_at ??
|
|
1530
|
+
value.timestamp ??
|
|
1531
|
+
value.date;
|
|
1532
|
+
|
|
1533
|
+
const updatedAt = this.toIsoString(rawUpdated);
|
|
1534
|
+
|
|
1535
|
+
return { id, title, updatedAt };
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
private normalizeHistoryMessages(
|
|
1539
|
+
value: Record<string, unknown>,
|
|
1540
|
+
index: number,
|
|
1541
|
+
): ChatMessage[] {
|
|
1542
|
+
const messages: ChatMessage[] = [];
|
|
1543
|
+
|
|
1544
|
+
const rawUserText = value.message ?? value.question ?? value.query ?? value.text ?? value.content;
|
|
1545
|
+
const userText = typeof rawUserText === 'string' ? rawUserText.trim() : '';
|
|
1546
|
+
|
|
1547
|
+
const rawResponseText =
|
|
1548
|
+
value.response ?? value.answer ?? value.reply ?? value.completion ?? value.body ?? value.preview;
|
|
1549
|
+
const responseText = typeof rawResponseText === 'string' ? rawResponseText.trim() : '';
|
|
1550
|
+
|
|
1551
|
+
const rawId = value.id ?? value.messageId ?? value.uuid ?? value.conversationMessageId;
|
|
1552
|
+
const baseId = rawId !== undefined && rawId !== null
|
|
1553
|
+
? String(rawId)
|
|
1554
|
+
: `history-${index + 1}`;
|
|
1555
|
+
|
|
1556
|
+
const userTimestampValue =
|
|
1557
|
+
value.timestamp ??
|
|
1558
|
+
value.createdAt ??
|
|
1559
|
+
value.created_at ??
|
|
1560
|
+
value.date ??
|
|
1561
|
+
value.time;
|
|
1562
|
+
const assistantTimestampValue =
|
|
1563
|
+
value.responseTimestamp ??
|
|
1564
|
+
value.responseTime ??
|
|
1565
|
+
value.responseDate ??
|
|
1566
|
+
value.response_at ??
|
|
1567
|
+
value.updatedAt ??
|
|
1568
|
+
value.updated_at;
|
|
1569
|
+
|
|
1570
|
+
const userTimestamp = this.parseTimestamp(userTimestampValue);
|
|
1571
|
+
const assistantTimestamp = this.parseTimestamp(
|
|
1572
|
+
assistantTimestampValue,
|
|
1573
|
+
userTimestamp + 1,
|
|
1574
|
+
);
|
|
1575
|
+
|
|
1576
|
+
if (responseText) {
|
|
1577
|
+
if (userText) {
|
|
1578
|
+
messages.push({
|
|
1579
|
+
id: `${baseId}-user`,
|
|
1580
|
+
role: 'user',
|
|
1581
|
+
text: userText,
|
|
1582
|
+
html: this.renderMarkdown(userText),
|
|
1583
|
+
timestamp: userTimestamp,
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
messages.push({
|
|
1588
|
+
id: `${baseId}-assistant`,
|
|
1589
|
+
role: 'assistant',
|
|
1590
|
+
text: responseText,
|
|
1591
|
+
html: this.renderMarkdown(responseText),
|
|
1592
|
+
timestamp: assistantTimestamp,
|
|
1593
|
+
});
|
|
1594
|
+
} else if (userText) {
|
|
1595
|
+
// Se n�o tiver resposta, n�o exibimos a mensagem do usuario isolada.
|
|
1596
|
+
return [];
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (messages.length > 0) {
|
|
1600
|
+
return messages;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const fallback = this.normalizeSingleHistoryMessage(value, index);
|
|
1604
|
+
return fallback ? [fallback] : [];
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
private normalizeSingleHistoryMessage(
|
|
1608
|
+
value: Record<string, unknown>,
|
|
1609
|
+
index: number,
|
|
1610
|
+
): ChatMessage | null {
|
|
1611
|
+
const rawText =
|
|
1612
|
+
value.text ??
|
|
1613
|
+
value.message ??
|
|
1614
|
+
value.content ??
|
|
1615
|
+
value.response ??
|
|
1616
|
+
value.body ??
|
|
1617
|
+
value.preview;
|
|
1618
|
+
|
|
1619
|
+
const text = typeof rawText === 'string' && rawText.trim().length > 0
|
|
1620
|
+
? rawText
|
|
1621
|
+
: '';
|
|
1622
|
+
|
|
1623
|
+
if (!text) {
|
|
1624
|
+
return null;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
const role = this.normalizeRole(
|
|
1628
|
+
value.role ??
|
|
1629
|
+
value.sender ??
|
|
1630
|
+
value.from ??
|
|
1631
|
+
value.author ??
|
|
1632
|
+
value.type ??
|
|
1633
|
+
value.direction,
|
|
1634
|
+
);
|
|
1635
|
+
|
|
1636
|
+
const rawId = value.id ?? value.messageId ?? value.uuid ?? value.conversationMessageId;
|
|
1637
|
+
const id = rawId !== undefined && rawId !== null
|
|
1638
|
+
? String(rawId)
|
|
1639
|
+
: `history-message-${index + 1}`;
|
|
1640
|
+
|
|
1641
|
+
const timestampValue =
|
|
1642
|
+
value.timestamp ??
|
|
1643
|
+
value.createdAt ??
|
|
1644
|
+
value.created_at ??
|
|
1645
|
+
value.updatedAt ??
|
|
1646
|
+
value.updated_at ??
|
|
1647
|
+
value.date ??
|
|
1648
|
+
value.time;
|
|
1649
|
+
|
|
1650
|
+
const timestamp = this.parseTimestamp(timestampValue);
|
|
1651
|
+
|
|
1652
|
+
return {
|
|
1653
|
+
id,
|
|
1654
|
+
role,
|
|
1655
|
+
text,
|
|
1656
|
+
html: this.renderMarkdown(text),
|
|
1657
|
+
timestamp,
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
private normalizeRole(value: unknown): ChatRole {
|
|
1662
|
+
if (typeof value === 'string') {
|
|
1663
|
+
const normalized = value.toLowerCase();
|
|
1664
|
+
if (normalized.includes('user') || normalized.includes('client')) {
|
|
1665
|
+
return 'user';
|
|
1666
|
+
}
|
|
1667
|
+
if (normalized.includes('assistant') || normalized.includes('agent') || normalized.includes('bot')) {
|
|
1668
|
+
return 'assistant';
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
return 'assistant';
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
private parseTimestamp(value: unknown, fallback?: number) {
|
|
1676
|
+
const parsed = Date.parse(this.toIsoString(value));
|
|
1677
|
+
if (Number.isFinite(parsed)) {
|
|
1678
|
+
return parsed;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
if (Number.isFinite(fallback ?? NaN)) {
|
|
1682
|
+
return fallback as number;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
return Date.now();
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
private lookupConversationTitle(conversationId: string | null) {
|
|
1689
|
+
if (!conversationId) {
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
const found = this.conversations.find((item) => item.id === conversationId);
|
|
1694
|
+
return found ? found.title : null;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
private syncActiveConversationTitle() {
|
|
1698
|
+
if (!this.currentConversationId) {
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
const title = this.lookupConversationTitle(this.currentConversationId);
|
|
1703
|
+
if (title) {
|
|
1704
|
+
this.activeConversationTitle = title;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
private toIsoString(value: unknown) {
|
|
1709
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
1710
|
+
const date = new Date(value);
|
|
1711
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1712
|
+
return date.toISOString();
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
return new Date().toISOString();
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
private startLoadingGuard() {
|
|
1720
|
+
this.clearLoadingGuard();
|
|
1721
|
+
this.loadingLabelInternal = 'RIO Insight está respondendo';
|
|
1722
|
+
|
|
1723
|
+
// Após 20s, mensagem de processamento prolongado.
|
|
1724
|
+
this.loadingTimerSlow = window.setTimeout(() => {
|
|
1725
|
+
this.loadingLabelInternal = 'RIO Insight continua respondendo';
|
|
1726
|
+
this.requestUpdate();
|
|
1727
|
+
}, 20000);
|
|
1728
|
+
|
|
1729
|
+
// Após 60s, aviso de demora maior.
|
|
1730
|
+
this.loadingTimerTimeout = window.setTimeout(() => {
|
|
1731
|
+
this.loadingLabelInternal =
|
|
1732
|
+
'RIO Insight ainda está processando sua resposta. Peço que aguarde um pouco mais';
|
|
1733
|
+
this.requestUpdate();
|
|
1734
|
+
}, 60000);
|
|
1735
|
+
|
|
1736
|
+
// Após 120s, novo aviso de demora maior.
|
|
1737
|
+
this.loadingTimerTimeout = window.setTimeout(() => {
|
|
1738
|
+
this.loadingLabelInternal =
|
|
1739
|
+
'Essa solicitação está demorando um pouco mais que o esperado. Pode favor, aguarde mais um pouco';
|
|
1740
|
+
this.requestUpdate();
|
|
1741
|
+
}, 120000);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
private clearLoadingGuard() {
|
|
1745
|
+
if (this.loadingTimer !== null) {
|
|
1746
|
+
window.clearTimeout(this.loadingTimer);
|
|
1747
|
+
this.loadingTimer = null;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
if (this.loadingTimerSlow !== null) {
|
|
1751
|
+
window.clearTimeout(this.loadingTimerSlow);
|
|
1752
|
+
this.loadingTimerSlow = null;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (this.loadingTimerTimeout !== null) {
|
|
1756
|
+
window.clearTimeout(this.loadingTimerTimeout);
|
|
1757
|
+
this.loadingTimerTimeout = null;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
private scrollConversationToBottom() {
|
|
1762
|
+
const containers = Array.from(
|
|
1763
|
+
this.renderRoot.querySelectorAll('.panel-content'),
|
|
1764
|
+
) as HTMLElement[];
|
|
1765
|
+
|
|
1766
|
+
containers.forEach((container) => {
|
|
1767
|
+
requestAnimationFrame(() => {
|
|
1768
|
+
container.scrollTop = container.scrollHeight;
|
|
1769
|
+
});
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
private renderMarkdown(content: string) {
|
|
1774
|
+
const rendered = this.markdownRenderer.render(content);
|
|
1775
|
+
const clean = DOMPurify.sanitize(rendered, {
|
|
1776
|
+
ALLOWED_TAGS: [
|
|
1777
|
+
'a',
|
|
1778
|
+
'p',
|
|
1779
|
+
'ul',
|
|
1780
|
+
'ol',
|
|
1781
|
+
'li',
|
|
1782
|
+
'code',
|
|
1783
|
+
'pre',
|
|
1784
|
+
'strong',
|
|
1785
|
+
'em',
|
|
1786
|
+
'blockquote',
|
|
1787
|
+
'table',
|
|
1788
|
+
'thead',
|
|
1789
|
+
'tbody',
|
|
1790
|
+
'tr',
|
|
1791
|
+
'th',
|
|
1792
|
+
'td',
|
|
1793
|
+
'del',
|
|
1794
|
+
'hr',
|
|
1795
|
+
'br',
|
|
1796
|
+
'img',
|
|
1797
|
+
'span',
|
|
1798
|
+
'input',
|
|
1799
|
+
],
|
|
1800
|
+
ALLOWED_ATTR: [
|
|
1801
|
+
'href',
|
|
1802
|
+
'title',
|
|
1803
|
+
'target',
|
|
1804
|
+
'rel',
|
|
1805
|
+
'src',
|
|
1806
|
+
'alt',
|
|
1807
|
+
'class',
|
|
1808
|
+
'type',
|
|
1809
|
+
'checked',
|
|
1810
|
+
'disabled',
|
|
1811
|
+
'aria-label',
|
|
1812
|
+
],
|
|
1813
|
+
ALLOW_DATA_ATTR: false,
|
|
1814
|
+
FORBID_TAGS: ['style', 'script'],
|
|
1815
|
+
USE_PROFILES: { html: true },
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
const container = document.createElement('div');
|
|
1819
|
+
container.innerHTML = clean;
|
|
1820
|
+
|
|
1821
|
+
container.querySelectorAll('a').forEach((anchor) => {
|
|
1822
|
+
anchor.setAttribute('target', '_blank');
|
|
1823
|
+
anchor.setAttribute('rel', 'noopener noreferrer');
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
container.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {
|
|
1827
|
+
checkbox.setAttribute('disabled', '');
|
|
1828
|
+
checkbox.setAttribute('tabindex', '-1');
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
return container.innerHTML;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
render() {
|
|
1835
|
+
return renderRioAssist(this);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
}
|
|
1839
|
+
declare global {
|
|
1840
|
+
interface HTMLElementTagNameMap {
|
|
1841
|
+
'rio-assist-widget': RioAssistWidget;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
if (!customElements.get('rio-assist-widget')) {
|
|
1846
|
+
customElements.define('rio-assist-widget', RioAssistWidget);
|
|
1847
|
+
}
|