rio-assist-widget 0.1.18 → 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/README.md +9 -3
- package/dist/rio-assist.js +201 -65
- package/index.html +1 -2
- 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 +66 -13
- package/src/components/mini-panel/mini-panel.template.ts +4 -1
- package/src/components/rio-assist/rio-assist.styles.ts +30 -1
- package/src/components/rio-assist/rio-assist.template.ts +65 -0
- package/src/components/rio-assist/rio-assist.ts +1433 -1008
- package/src/main.ts +15 -9
- package/src/playground.ts +9 -3
- package/src/services/rioWebsocket.ts +23 -0
|
@@ -1,34 +1,58 @@
|
|
|
1
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';
|
|
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
11
|
|
|
12
12
|
type ChatRole = 'user' | 'assistant';
|
|
13
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 = {
|
|
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;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type ConversationActionErrorState = ConversationActionAttempt & {
|
|
55
|
+
message: string;
|
|
32
56
|
};
|
|
33
57
|
|
|
34
58
|
export type HeaderActionConfig = {
|
|
@@ -36,9 +60,9 @@ export type HeaderActionConfig = {
|
|
|
36
60
|
iconUrl: string;
|
|
37
61
|
ariaLabel?: string;
|
|
38
62
|
onClick?: () => void;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export class RioAssistWidget extends LitElement {
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export class RioAssistWidget extends LitElement {
|
|
42
66
|
static styles = widgetStyles;
|
|
43
67
|
|
|
44
68
|
static properties = {
|
|
@@ -57,15 +81,17 @@ export class RioAssistWidget extends LitElement {
|
|
|
57
81
|
showConversations: { type: Boolean, state: true },
|
|
58
82
|
conversationSearch: { type: String, state: true },
|
|
59
83
|
conversationMenuId: { state: true },
|
|
60
|
-
conversationMenuPlacement: { state: true },
|
|
61
|
-
isFullscreen: { type: Boolean, state: true },
|
|
62
|
-
conversationScrollbar: { state: true },
|
|
63
|
-
showNewConversationShortcut: { type: Boolean, state: true },
|
|
84
|
+
conversationMenuPlacement: { state: true },
|
|
85
|
+
isFullscreen: { type: Boolean, state: true },
|
|
86
|
+
conversationScrollbar: { state: true },
|
|
87
|
+
showNewConversationShortcut: { type: Boolean, state: true },
|
|
64
88
|
conversations: { state: true },
|
|
65
89
|
conversationHistoryLoading: { type: Boolean, state: true },
|
|
66
90
|
activeConversationTitle: { state: true },
|
|
67
91
|
conversationHistoryError: { type: String, state: true },
|
|
68
92
|
deleteConversationTarget: { attribute: false },
|
|
93
|
+
renameConversationTarget: { attribute: false },
|
|
94
|
+
conversationActionError: { attribute: false },
|
|
69
95
|
headerActions: { attribute: false },
|
|
70
96
|
homeUrl: { type: String, attribute: 'data-home-url' },
|
|
71
97
|
};
|
|
@@ -88,12 +114,18 @@ export class RioAssistWidget extends LitElement {
|
|
|
88
114
|
|
|
89
115
|
suggestionsSource = '';
|
|
90
116
|
|
|
117
|
+
private randomizedSuggestions: string[] = [];
|
|
118
|
+
|
|
91
119
|
messages: ChatMessage[] = [];
|
|
92
120
|
|
|
93
121
|
isLoading = false;
|
|
94
122
|
|
|
95
123
|
errorMessage = '';
|
|
96
124
|
|
|
125
|
+
get loadingLabel() {
|
|
126
|
+
return this.loadingLabelInternal;
|
|
127
|
+
}
|
|
128
|
+
|
|
97
129
|
showConversations = false;
|
|
98
130
|
|
|
99
131
|
conversationSearch = '';
|
|
@@ -102,141 +134,180 @@ export class RioAssistWidget extends LitElement {
|
|
|
102
134
|
|
|
103
135
|
conversationMenuPlacement: 'above' | 'below' = 'below';
|
|
104
136
|
|
|
105
|
-
isFullscreen = false;
|
|
106
|
-
|
|
107
|
-
showNewConversationShortcut = false;
|
|
108
|
-
|
|
109
|
-
conversationScrollbar = {
|
|
110
|
-
height: 0,
|
|
111
|
-
top: 0,
|
|
112
|
-
visible: false,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
conversationHistoryLoading = false;
|
|
116
|
-
|
|
117
|
-
conversationHistoryError = '';
|
|
118
|
-
|
|
119
|
-
deleteConversationTarget: ConversationDeleteTarget | null = null;
|
|
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
|
+
|
|
153
|
+
renameConversationTarget: ConversationRenameTarget | null = null;
|
|
120
154
|
|
|
121
|
-
|
|
155
|
+
conversationActionError: ConversationActionErrorState | null = null;
|
|
122
156
|
|
|
123
|
-
|
|
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;
|
|
124
164
|
|
|
125
165
|
headerActions: HeaderActionConfig[] = [];
|
|
126
166
|
|
|
127
167
|
homeUrl = '';
|
|
128
168
|
|
|
129
|
-
private
|
|
130
|
-
if (!this.conversationUserId) {
|
|
131
|
-
this.conversationUserId = this.inferUserIdFromToken();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const userSegment = this.conversationUserId ?? 'user';
|
|
135
|
-
const id = `default-${userSegment}-${this.randomId(8)}`;
|
|
136
|
-
console.info('[RioAssist][conversation] gerando conversationId', id);
|
|
137
|
-
return id;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private inferUserIdFromToken(): string | null {
|
|
141
|
-
const token = this.rioToken.trim();
|
|
142
|
-
if (!token || !token.includes('.')) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const [, payload] = token.split('.');
|
|
147
|
-
try {
|
|
148
|
-
const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
|
|
149
|
-
const candidate =
|
|
150
|
-
decoded?.userId ??
|
|
151
|
-
decoded?.user_id ??
|
|
152
|
-
decoded?.sub ??
|
|
153
|
-
decoded?.id ??
|
|
154
|
-
decoded?.email ??
|
|
155
|
-
decoded?.username;
|
|
156
|
-
|
|
157
|
-
if (candidate && typeof candidate === 'string') {
|
|
158
|
-
return candidate.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
159
|
-
}
|
|
160
|
-
} catch {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
169
|
+
private pendingConversationAction: ConversationActionAttempt | null = null;
|
|
166
170
|
|
|
167
|
-
private
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
}
|
|
175
217
|
|
|
176
218
|
private conversationScrollbarRaf: number | null = null;
|
|
177
219
|
|
|
178
|
-
private rioClient: RioWebsocketClient | null = null;
|
|
179
|
-
|
|
180
|
-
private rioUnsubscribe: (() => void) | null = null;
|
|
181
|
-
|
|
182
|
-
private loadingTimer: number | null = null;
|
|
183
|
-
|
|
184
|
-
private currentConversationId: string | null = null;
|
|
185
|
-
|
|
186
|
-
private conversationCounter = 0;
|
|
187
|
-
|
|
188
|
-
private conversationUserId: string | null = null;
|
|
189
|
-
|
|
190
|
-
private conversationScrollbarDraggingId: number | null = null;
|
|
191
|
-
|
|
192
|
-
private conversationScrollbarDragState: {
|
|
193
|
-
startY: number;
|
|
194
|
-
startThumbTop: number;
|
|
195
|
-
trackHeight: number;
|
|
196
|
-
thumbHeight: number;
|
|
197
|
-
list: HTMLElement;
|
|
198
|
-
} | null = null;
|
|
199
|
-
|
|
200
|
-
private markdownRenderer = new MarkdownIt({
|
|
201
|
-
html: false,
|
|
202
|
-
linkify: true,
|
|
203
|
-
breaks: true,
|
|
204
|
-
}).use(markdownItTaskLists);
|
|
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);
|
|
205
247
|
|
|
206
248
|
conversations: ConversationItem[] = [];
|
|
207
249
|
|
|
208
250
|
get suggestions(): string[] {
|
|
209
|
-
|
|
251
|
+
return this.randomizedSuggestions;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private parseSuggestions(source: string): string[] {
|
|
255
|
+
if (!source) {
|
|
210
256
|
return [];
|
|
211
257
|
}
|
|
212
258
|
|
|
213
|
-
return
|
|
259
|
+
return source
|
|
214
260
|
.split('|')
|
|
215
261
|
.map((item) => item.trim())
|
|
216
262
|
.filter(Boolean);
|
|
217
263
|
}
|
|
218
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
|
+
|
|
219
290
|
protected updated(changedProperties: PropertyValues): void {
|
|
220
291
|
super.updated(changedProperties);
|
|
221
292
|
this.style.setProperty('--accent-color', this.accentColor);
|
|
222
293
|
|
|
223
|
-
if (
|
|
224
|
-
changedProperties.has('isFullscreen') ||
|
|
225
|
-
changedProperties.has('showConversations') ||
|
|
226
|
-
changedProperties.has('conversations')
|
|
227
|
-
) {
|
|
228
|
-
this.enqueueConversationScrollbarMeasure();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
changedProperties.has('messages') ||
|
|
233
|
-
(changedProperties.has('isLoading') && this.isLoading) ||
|
|
234
|
-
(changedProperties.has('open') && this.open) ||
|
|
235
|
-
(changedProperties.has('isFullscreen') && this.isFullscreen)
|
|
236
|
-
) {
|
|
237
|
-
this.scrollConversationToBottom();
|
|
238
|
-
}
|
|
239
|
-
}
|
|
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
|
+
}
|
|
240
311
|
|
|
241
312
|
protected firstUpdated(): void {
|
|
242
313
|
this.enqueueConversationScrollbarMeasure();
|
|
@@ -290,48 +361,48 @@ export class RioAssistWidget extends LitElement {
|
|
|
290
361
|
}
|
|
291
362
|
}
|
|
292
363
|
|
|
293
|
-
openConversationsPanel() {
|
|
294
|
-
this.showConversations = true;
|
|
295
|
-
this.requestConversationHistory();
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
closeConversationsPanel() {
|
|
299
|
-
this.showConversations = false;
|
|
300
|
-
this.conversationMenuId = null;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
toggleConversationsPanel() {
|
|
304
|
-
this.showConversations = !this.showConversations;
|
|
305
|
-
if (!this.showConversations) {
|
|
306
|
-
this.conversationMenuId = null;
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
this.requestConversationHistory();
|
|
311
|
-
}
|
|
364
|
+
openConversationsPanel() {
|
|
365
|
+
this.showConversations = true;
|
|
366
|
+
this.requestConversationHistory();
|
|
367
|
+
}
|
|
312
368
|
|
|
313
|
-
|
|
314
|
-
this.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
this.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
this.
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
+
}
|
|
335
406
|
|
|
336
407
|
handleConversationMenuToggle(event: Event, id: string) {
|
|
337
408
|
event.stopPropagation();
|
|
@@ -367,186 +438,494 @@ export class RioAssistWidget extends LitElement {
|
|
|
367
438
|
this.conversationMenuId = null;
|
|
368
439
|
}
|
|
369
440
|
}
|
|
370
|
-
|
|
371
|
-
handleConversationAction(action: 'rename' | 'delete', id: string) {
|
|
372
|
-
this.conversationMenuId = null;
|
|
373
|
-
const conversationIndex = this.conversations.findIndex((item) => item.id === id);
|
|
374
|
-
if (conversationIndex === -1) {
|
|
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) {
|
|
375
512
|
return;
|
|
376
513
|
}
|
|
377
514
|
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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 = [];
|
|
386
528
|
}
|
|
387
|
-
|
|
388
|
-
this.dispatchConversationAction('rename', conversation, conversationIndex);
|
|
389
529
|
}
|
|
390
530
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const allowed = this.dispatchEvent(
|
|
394
|
-
new CustomEvent('rioassist:home', {
|
|
395
|
-
detail,
|
|
396
|
-
bubbles: true,
|
|
397
|
-
composed: true,
|
|
398
|
-
cancelable: true,
|
|
399
|
-
}),
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
if (!allowed) {
|
|
531
|
+
private restoreConversationSnapshot(snapshot: ConversationItem | undefined, index: number) {
|
|
532
|
+
if (!snapshot) {
|
|
403
533
|
return;
|
|
404
534
|
}
|
|
405
535
|
|
|
406
|
-
|
|
407
|
-
|
|
536
|
+
const exists = this.conversations.some((conversation) => conversation.id === snapshot.id);
|
|
537
|
+
if (exists) {
|
|
538
|
+
return;
|
|
408
539
|
}
|
|
409
|
-
}
|
|
410
540
|
|
|
411
|
-
|
|
412
|
-
|
|
541
|
+
const next = [...this.conversations];
|
|
542
|
+
const position = index >= 0 && index <= next.length ? index : next.length;
|
|
543
|
+
next.splice(position, 0, snapshot);
|
|
544
|
+
this.conversations = next;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async confirmDeleteConversation() {
|
|
548
|
+
const target = this.deleteConversationTarget;
|
|
549
|
+
if (!target) {
|
|
413
550
|
return;
|
|
414
551
|
}
|
|
415
552
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
};
|
|
424
570
|
|
|
425
|
-
|
|
571
|
+
const success = await this.dispatchConversationAction(
|
|
572
|
+
'delete',
|
|
573
|
+
{ id: target.id, title: target.title },
|
|
574
|
+
target.index,
|
|
575
|
+
);
|
|
576
|
+
if (success) {
|
|
577
|
+
this.deleteConversationTarget = null;
|
|
426
578
|
return;
|
|
427
579
|
}
|
|
428
580
|
|
|
429
|
-
|
|
430
|
-
this.activeConversationTitle = newTitle;
|
|
431
|
-
}
|
|
581
|
+
this.pendingConversationAction = null;
|
|
432
582
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
+
|
|
605
|
+
const newTitle = target.draft.trim();
|
|
606
|
+
if (!newTitle) {
|
|
436
607
|
return;
|
|
437
608
|
}
|
|
438
609
|
|
|
439
|
-
|
|
440
|
-
|
|
610
|
+
this.pendingConversationAction = {
|
|
611
|
+
action: 'rename',
|
|
612
|
+
conversationId: target.id,
|
|
613
|
+
originalTitle: target.title,
|
|
614
|
+
index: target.index,
|
|
615
|
+
newTitle,
|
|
616
|
+
};
|
|
441
617
|
|
|
442
|
-
|
|
618
|
+
const success = await this.dispatchConversationAction(
|
|
619
|
+
'rename',
|
|
620
|
+
{ id: target.id, title: newTitle },
|
|
621
|
+
target.index,
|
|
622
|
+
newTitle,
|
|
623
|
+
);
|
|
624
|
+
if (success) {
|
|
625
|
+
this.renameConversationTarget = null;
|
|
443
626
|
return;
|
|
444
627
|
}
|
|
445
628
|
|
|
446
|
-
this.
|
|
447
|
-
|
|
448
|
-
if (wasActive) {
|
|
449
|
-
this.currentConversationId = null;
|
|
450
|
-
this.activeConversationTitle = null;
|
|
451
|
-
this.messages = [];
|
|
452
|
-
}
|
|
629
|
+
this.pendingConversationAction = null;
|
|
453
630
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (!target) {
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
this.dispatchConversationAction('delete', { id: target.id, title: target.title }, target.index);
|
|
462
|
-
this.deleteConversationTarget = null;
|
|
631
|
+
|
|
632
|
+
cancelRenameConversation() {
|
|
633
|
+
this.renameConversationTarget = null;
|
|
463
634
|
}
|
|
464
635
|
|
|
465
|
-
|
|
466
|
-
this.
|
|
636
|
+
cancelConversationActionError() {
|
|
637
|
+
this.conversationActionError = null;
|
|
638
|
+
this.pendingConversationAction = null;
|
|
467
639
|
}
|
|
468
640
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
index: number,
|
|
473
|
-
) {
|
|
474
|
-
const eventName =
|
|
475
|
-
action === 'rename' ? 'rioassist:conversation-rename' : 'rioassist:conversation-delete';
|
|
476
|
-
const detail = {
|
|
477
|
-
id: conversation.id,
|
|
478
|
-
title: conversation.title,
|
|
479
|
-
index,
|
|
480
|
-
action,
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
const allowed = this.dispatchEvent(
|
|
484
|
-
new CustomEvent(eventName, {
|
|
485
|
-
detail,
|
|
486
|
-
bubbles: true,
|
|
487
|
-
composed: true,
|
|
488
|
-
cancelable: true,
|
|
489
|
-
}),
|
|
490
|
-
);
|
|
491
|
-
|
|
492
|
-
if (!allowed) {
|
|
641
|
+
async retryConversationAction() {
|
|
642
|
+
const errorState = this.conversationActionError;
|
|
643
|
+
if (!errorState) {
|
|
493
644
|
return;
|
|
494
645
|
}
|
|
495
646
|
|
|
496
|
-
|
|
497
|
-
this.
|
|
498
|
-
|
|
499
|
-
|
|
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
|
+
};
|
|
500
665
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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,
|
|
507
675
|
};
|
|
508
676
|
|
|
509
|
-
|
|
510
|
-
new CustomEvent('rioassist:header-action', {
|
|
511
|
-
detail,
|
|
512
|
-
bubbles: true,
|
|
513
|
-
composed: true,
|
|
514
|
-
cancelable: true,
|
|
515
|
-
}),
|
|
516
|
-
);
|
|
517
|
-
|
|
518
|
-
if (!allowed) {
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
677
|
+
this.conversationActionError = null;
|
|
521
678
|
|
|
522
|
-
|
|
523
|
-
action
|
|
524
|
-
|
|
679
|
+
await this.dispatchConversationAction(
|
|
680
|
+
errorState.action,
|
|
681
|
+
{ id: errorState.conversationId, title: errorState.newTitle ?? errorState.originalTitle },
|
|
682
|
+
safeIndex,
|
|
683
|
+
errorState.newTitle,
|
|
684
|
+
);
|
|
525
685
|
}
|
|
526
686
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
+
};
|
|
532
701
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
702
|
+
const allowed = this.dispatchEvent(
|
|
703
|
+
new CustomEvent(eventName, {
|
|
704
|
+
detail,
|
|
705
|
+
bubbles: true,
|
|
706
|
+
composed: true,
|
|
707
|
+
cancelable: true,
|
|
708
|
+
}),
|
|
709
|
+
);
|
|
539
710
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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>;
|
|
766
|
+
const id = this.extractString(data, ['conversationId', 'id']);
|
|
767
|
+
const newTitle = this.extractString(data, ['newTitle', 'title']);
|
|
768
|
+
if (id && newTitle) {
|
|
769
|
+
this.applyConversationRename(id, newTitle);
|
|
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
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (action === 'conversationdeleted') {
|
|
784
|
+
const data = message.data as Record<string, unknown>;
|
|
785
|
+
const id = this.extractString(data, ['conversationId', 'id']);
|
|
786
|
+
if (id) {
|
|
787
|
+
this.applyConversationDeletion(id);
|
|
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
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (action === 'processing') {
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private handleConversationActionError(message: RioIncomingMessage) {
|
|
809
|
+
const action = (message.action ?? '').toLowerCase();
|
|
810
|
+
if (action !== 'error') {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
|
|
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);
|
|
825
|
+
}
|
|
826
|
+
|
|
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
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
this.conversationActionError = {
|
|
837
|
+
...pending,
|
|
838
|
+
message: errorText,
|
|
839
|
+
};
|
|
840
|
+
this.pendingConversationAction = null;
|
|
841
|
+
this.clearLoadingGuard();
|
|
842
|
+
this.isLoading = false;
|
|
843
|
+
return true;
|
|
543
844
|
}
|
|
544
845
|
|
|
545
|
-
this.
|
|
546
|
-
this.
|
|
547
|
-
this.
|
|
548
|
-
|
|
846
|
+
this.errorMessage = errorText;
|
|
847
|
+
this.clearLoadingGuard();
|
|
848
|
+
this.isLoading = false;
|
|
849
|
+
return true;
|
|
549
850
|
}
|
|
851
|
+
|
|
852
|
+
private shouldIgnoreAssistantPayload(action?: string) {
|
|
853
|
+
if (!action) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
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
|
+
}
|
|
550
929
|
|
|
551
930
|
exitFullscreen(restorePanel: boolean) {
|
|
552
931
|
if (!this.isFullscreen) {
|
|
@@ -561,25 +940,25 @@ export class RioAssistWidget extends LitElement {
|
|
|
561
940
|
}
|
|
562
941
|
}
|
|
563
942
|
|
|
564
|
-
handleCreateConversation() {
|
|
565
|
-
if (!this.hasActiveConversation) {
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
this.clearLoadingGuard();
|
|
570
|
-
this.isLoading = false;
|
|
571
|
-
this.messages = [];
|
|
572
|
-
this.message = '';
|
|
573
|
-
this.errorMessage = '';
|
|
574
|
-
this.showConversations = false;
|
|
575
|
-
this.teardownRioClient();
|
|
576
|
-
this.currentConversationId = this.generateConversationId();
|
|
577
|
-
this.activeConversationTitle = null;
|
|
578
|
-
this.showNewConversationShortcut = false;
|
|
579
|
-
this.dispatchEvent(
|
|
580
|
-
new CustomEvent('rioassist:new-conversation', {
|
|
581
|
-
bubbles: true,
|
|
582
|
-
composed: true,
|
|
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,
|
|
583
962
|
}),
|
|
584
963
|
);
|
|
585
964
|
}
|
|
@@ -706,95 +1085,95 @@ export class RioAssistWidget extends LitElement {
|
|
|
706
1085
|
return;
|
|
707
1086
|
}
|
|
708
1087
|
|
|
709
|
-
const ratio = clientHeight / scrollHeight;
|
|
710
|
-
const height = Math.max(ratio * 100, 8);
|
|
711
|
-
const maxTop = 100 - height;
|
|
712
|
-
const top =
|
|
713
|
-
scrollTop / (scrollHeight - clientHeight) * (maxTop > 0 ? maxTop : 0);
|
|
714
|
-
|
|
715
|
-
this.conversationScrollbar = {
|
|
716
|
-
height,
|
|
717
|
-
top,
|
|
718
|
-
visible: true,
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
async onSuggestionClick(suggestion: string) {
|
|
723
|
-
await this.processMessage(suggestion);
|
|
724
|
-
}
|
|
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);
|
|
725
1093
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
this.
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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,
|
|
763
1142
|
token: this.rioToken,
|
|
764
1143
|
},
|
|
765
1144
|
bubbles: true,
|
|
766
1145
|
composed: true,
|
|
767
1146
|
}),
|
|
768
1147
|
);
|
|
769
|
-
|
|
770
|
-
const userMessage = this.createMessage('user', content);
|
|
771
|
-
this.messages = [...this.messages, userMessage];
|
|
772
|
-
if (wasEmptyConversation) {
|
|
773
|
-
this.showNewConversationShortcut = true;
|
|
774
|
-
this.refreshConversationsAfterResponse = true;
|
|
775
|
-
}
|
|
776
|
-
this.message = '';
|
|
777
|
-
this.errorMessage = '';
|
|
778
|
-
this.isLoading = true;
|
|
779
|
-
this.startLoadingGuard();
|
|
780
|
-
|
|
781
|
-
try {
|
|
782
|
-
const client = this.ensureRioClient();
|
|
783
|
-
await client.sendMessage(content, this.currentConversationId);
|
|
784
|
-
} catch (error) {
|
|
785
|
-
this.clearLoadingGuard();
|
|
786
|
-
this.isLoading = false;
|
|
787
|
-
this.errorMessage = error instanceof Error
|
|
788
|
-
? error.message
|
|
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
|
|
789
1168
|
: 'Nao foi possivel enviar a mensagem para o agente.';
|
|
790
1169
|
}
|
|
791
1170
|
}
|
|
792
1171
|
|
|
793
|
-
private ensureRioClient() {
|
|
794
|
-
const token = this.rioToken.trim();
|
|
795
|
-
if (!token) {
|
|
796
|
-
throw new Error(
|
|
797
|
-
'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
|
|
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.',
|
|
798
1177
|
);
|
|
799
1178
|
}
|
|
800
1179
|
|
|
@@ -805,34 +1184,53 @@ export class RioAssistWidget extends LitElement {
|
|
|
805
1184
|
this.handleIncomingMessage(incoming);
|
|
806
1185
|
});
|
|
807
1186
|
}
|
|
808
|
-
|
|
809
|
-
return this.rioClient;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
private handleIncomingMessage(message: RioIncomingMessage) {
|
|
813
|
-
if (this.isHistoryPayload(message)) {
|
|
814
|
-
this.logHistoryPayload(message);
|
|
815
|
-
this.handleHistoryPayload(message.data);
|
|
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)) {
|
|
816
1199
|
return;
|
|
817
1200
|
}
|
|
818
1201
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
raw: message.raw,
|
|
823
|
-
data: message.data,
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
const assistantMessage = this.createMessage('assistant', message.text);
|
|
827
|
-
this.messages = [...this.messages, assistantMessage];
|
|
828
|
-
this.clearLoadingGuard();
|
|
829
|
-
this.isLoading = false;
|
|
1202
|
+
if (this.handleConversationActionError(message)) {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
830
1205
|
|
|
831
|
-
if (this.
|
|
832
|
-
|
|
833
|
-
this.requestConversationHistory();
|
|
1206
|
+
if (this.shouldIgnoreAssistantPayload(message.action)) {
|
|
1207
|
+
return;
|
|
834
1208
|
}
|
|
835
|
-
|
|
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
|
+
}
|
|
836
1234
|
|
|
837
1235
|
private teardownRioClient() {
|
|
838
1236
|
if (this.rioUnsubscribe) {
|
|
@@ -840,583 +1238,610 @@ export class RioAssistWidget extends LitElement {
|
|
|
840
1238
|
this.rioUnsubscribe = null;
|
|
841
1239
|
}
|
|
842
1240
|
|
|
843
|
-
if (this.rioClient) {
|
|
844
|
-
this.rioClient.close();
|
|
845
|
-
this.rioClient = null;
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
async requestConversationHistory(conversationId?: string) {
|
|
850
|
-
try {
|
|
851
|
-
const client = this.ensureRioClient();
|
|
852
|
-
const limit = 50;
|
|
853
|
-
|
|
854
|
-
console.info('[RioAssist][history] solicitando historico de conversas', {
|
|
855
|
-
conversationId: conversationId ?? null,
|
|
856
|
-
limit,
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
this.conversationHistoryError = '';
|
|
860
|
-
this.conversationHistoryLoading = true;
|
|
861
|
-
await client.requestHistory({ conversationId, limit });
|
|
862
|
-
} catch (error) {
|
|
863
|
-
console.error('[RioAssist][history] erro ao solicitar historico', error);
|
|
864
|
-
this.conversationHistoryError =
|
|
865
|
-
error instanceof Error && error.message
|
|
866
|
-
? error.message
|
|
867
|
-
: 'Nao foi possivel carregar as conversas.';
|
|
868
|
-
this.conversationHistoryLoading = false;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
private handleHistoryPayload(payload: unknown) {
|
|
873
|
-
const entries = this.extractHistoryEntries(payload);
|
|
874
|
-
const conversationId = this.extractConversationId(payload);
|
|
875
|
-
|
|
876
|
-
if (conversationId !== null && conversationId !== undefined) {
|
|
877
|
-
this.applyMessageHistory(entries, conversationId);
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
if (this.isMessageHistoryEntries(entries)) {
|
|
882
|
-
this.applyMessageHistory(entries);
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
this.applyConversationHistoryFromEntries(entries);
|
|
887
|
-
|
|
888
|
-
if (this.refreshConversationsAfterResponse) {
|
|
889
|
-
this.refreshConversationsAfterResponse = false;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
private isHistoryPayload(message: RioIncomingMessage) {
|
|
894
|
-
if (
|
|
895
|
-
typeof message.action === 'string' &&
|
|
896
|
-
message.action.toLowerCase().includes('history')
|
|
897
|
-
) {
|
|
898
|
-
return true;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
const data = message.data;
|
|
902
|
-
if (data && typeof data === 'object') {
|
|
903
|
-
const action = (data as any).action;
|
|
904
|
-
if (typeof action === 'string' && action.toLowerCase().includes('history')) {
|
|
905
|
-
return true;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
if (Array.isArray((data as any).history) || Array.isArray((data as any).conversations)) {
|
|
909
|
-
return true;
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
return false;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
private logHistoryPayload(message: RioIncomingMessage) {
|
|
917
|
-
const label = '[RioAssist][history] payload recebido do websocket';
|
|
918
|
-
if (message.data !== null && message.data !== undefined) {
|
|
919
|
-
console.info(label, message.data);
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
console.info(label, message.raw);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
private applyConversationHistoryFromEntries(entries: unknown[]) {
|
|
927
|
-
if (entries.length === 0) {
|
|
928
|
-
console.info('[RioAssist][history] payload sem itens para montar lista de conversas');
|
|
929
|
-
this.conversations = [];
|
|
930
|
-
this.conversationHistoryLoading = false;
|
|
931
|
-
this.conversationHistoryError = '';
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
const map = new Map<string, ConversationItem>();
|
|
936
|
-
|
|
937
|
-
entries.forEach((entry, index) => {
|
|
938
|
-
if (!entry || typeof entry !== 'object') {
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
const normalized = this.normalizeConversationItem(
|
|
943
|
-
entry as Record<string, unknown>,
|
|
944
|
-
index,
|
|
945
|
-
);
|
|
946
|
-
|
|
947
|
-
if (!normalized) {
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
const current = map.get(normalized.id);
|
|
952
|
-
if (!current) {
|
|
953
|
-
map.set(normalized.id, normalized);
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
const currentTime = Date.parse(current.updatedAt);
|
|
958
|
-
const nextTime = Date.parse(normalized.updatedAt);
|
|
959
|
-
|
|
960
|
-
if (Number.isFinite(nextTime) && nextTime > currentTime) {
|
|
961
|
-
map.set(normalized.id, normalized);
|
|
962
|
-
}
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
const conversations = Array.from(map.values()).sort((a, b) => {
|
|
966
|
-
const order = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
|
|
967
|
-
return Number.isFinite(order) ? order : 0;
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
this.conversations = conversations;
|
|
971
|
-
this.conversationHistoryLoading = false;
|
|
972
|
-
this.conversationHistoryError = '';
|
|
973
|
-
this.syncActiveConversationTitle();
|
|
974
|
-
console.info('[RioAssist][history] conversas normalizadas', conversations);
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
private applyMessageHistory(entries: unknown[], conversationId?: string | null) {
|
|
978
|
-
if (entries.length === 0) {
|
|
979
|
-
console.info('[RioAssist][history] lista de mensagens vazia', { conversationId });
|
|
980
|
-
this.messages = [];
|
|
981
|
-
this.showConversations = false;
|
|
982
|
-
this.clearLoadingGuard();
|
|
983
|
-
this.isLoading = false;
|
|
984
|
-
this.conversationHistoryLoading = false;
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
const normalized = entries.flatMap((entry, index) =>
|
|
989
|
-
this.normalizeHistoryMessages(entry as Record<string, unknown>, index),
|
|
990
|
-
);
|
|
991
|
-
|
|
992
|
-
if (conversationId) {
|
|
993
|
-
this.currentConversationId = conversationId;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
this.messages = normalized;
|
|
997
|
-
this.showConversations = false;
|
|
998
|
-
this.clearLoadingGuard();
|
|
999
|
-
this.isLoading = false;
|
|
1000
|
-
this.showNewConversationShortcut = normalized.length > 0;
|
|
1001
|
-
this.conversationHistoryLoading = false;
|
|
1002
|
-
this.refreshConversationsAfterResponse = false;
|
|
1003
|
-
|
|
1004
|
-
console.info('[RioAssist][history] mensagens carregadas', {
|
|
1005
|
-
conversationId: conversationId ?? null,
|
|
1006
|
-
total: normalized.length,
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
private extractHistoryEntries(payload: unknown): unknown[] {
|
|
1011
|
-
if (Array.isArray(payload)) {
|
|
1012
|
-
return payload;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
if (payload && typeof payload === 'object') {
|
|
1016
|
-
const record = payload as Record<string, unknown>;
|
|
1017
|
-
const candidates = [
|
|
1018
|
-
record.history,
|
|
1019
|
-
record.conversations,
|
|
1020
|
-
record.data,
|
|
1021
|
-
record.items,
|
|
1022
|
-
record.messages,
|
|
1023
|
-
];
|
|
1024
|
-
|
|
1025
|
-
for (const candidate of candidates) {
|
|
1026
|
-
if (Array.isArray(candidate)) {
|
|
1027
|
-
return candidate;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
if (record.data && typeof record.data === 'object' && !Array.isArray(record.data)) {
|
|
1032
|
-
const nested = this.extractHistoryEntries(record.data);
|
|
1033
|
-
if (nested.length > 0) {
|
|
1034
|
-
return nested;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
return [];
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
private extractConversationId(payload: unknown): string | null | undefined {
|
|
1043
|
-
if (payload && typeof payload === 'object') {
|
|
1044
|
-
const record = payload as Record<string, unknown>;
|
|
1045
|
-
const candidates = [
|
|
1046
|
-
record.conversationId,
|
|
1047
|
-
record.conversationUUID,
|
|
1048
|
-
record.conversationUuid,
|
|
1049
|
-
record.uuid,
|
|
1050
|
-
record.id,
|
|
1051
|
-
];
|
|
1052
|
-
|
|
1053
|
-
for (const candidate of candidates) {
|
|
1054
|
-
if (candidate === null) {
|
|
1055
|
-
return null;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
if (candidate !== undefined) {
|
|
1059
|
-
return String(candidate);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
return undefined;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
private isMessageHistoryEntries(entries: unknown[]) {
|
|
1068
|
-
return entries.some((entry) => this.looksLikeMessageHistoryEntry(entry));
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
private looksLikeMessageHistoryEntry(entry: unknown) {
|
|
1072
|
-
if (!entry || typeof entry !== 'object') {
|
|
1073
|
-
return false;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
const item = entry as Record<string, unknown>;
|
|
1077
|
-
const role = item.role ?? item.sender ?? item.from ?? item.author ?? item.type;
|
|
1078
|
-
if (typeof role === 'string' && role.trim().length > 0) {
|
|
1079
|
-
return true;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
if (
|
|
1083
|
-
typeof item.content === 'string' ||
|
|
1084
|
-
typeof item.message === 'string' ||
|
|
1085
|
-
typeof item.text === 'string' ||
|
|
1086
|
-
typeof item.response === 'string'
|
|
1087
|
-
) {
|
|
1088
|
-
return true;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
if (Array.isArray(item.parts) && item.parts.length > 0) {
|
|
1092
|
-
return true;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
return false;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
private normalizeConversationItem(
|
|
1099
|
-
value: Record<string, unknown>,
|
|
1100
|
-
index: number,
|
|
1101
|
-
): ConversationItem | null {
|
|
1102
|
-
const rawId =
|
|
1103
|
-
value.id ??
|
|
1104
|
-
value.conversationId ??
|
|
1105
|
-
value.conversationUUID ??
|
|
1106
|
-
value.conversationUuid ??
|
|
1107
|
-
value.uuid;
|
|
1108
|
-
|
|
1109
|
-
const id = rawId !== undefined && rawId !== null ? String(rawId) : `history-${index + 1}`;
|
|
1110
|
-
|
|
1111
|
-
const rawTitle =
|
|
1112
|
-
value.title ??
|
|
1113
|
-
value.name ??
|
|
1114
|
-
value.topic ??
|
|
1115
|
-
value.subject ??
|
|
1116
|
-
value.question ??
|
|
1117
|
-
value.query ??
|
|
1118
|
-
value.message;
|
|
1119
|
-
|
|
1120
|
-
const title =
|
|
1121
|
-
typeof rawTitle === 'string' && rawTitle.trim().length > 0
|
|
1122
|
-
? rawTitle.trim()
|
|
1123
|
-
: `Conversa ${index + 1}`;
|
|
1124
|
-
|
|
1125
|
-
const rawUpdated =
|
|
1126
|
-
value.updatedAt ??
|
|
1127
|
-
value.updated_at ??
|
|
1128
|
-
value.lastMessageAt ??
|
|
1129
|
-
value.last_message_at ??
|
|
1130
|
-
value.createdAt ??
|
|
1131
|
-
value.created_at ??
|
|
1132
|
-
value.timestamp ??
|
|
1133
|
-
value.date;
|
|
1134
|
-
|
|
1135
|
-
const updatedAt = this.toIsoString(rawUpdated);
|
|
1136
|
-
|
|
1137
|
-
return { id, title, updatedAt };
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
private normalizeHistoryMessages(
|
|
1141
|
-
value: Record<string, unknown>,
|
|
1142
|
-
index: number,
|
|
1143
|
-
): ChatMessage[] {
|
|
1144
|
-
const messages: ChatMessage[] = [];
|
|
1145
|
-
|
|
1146
|
-
const rawUserText = value.message ?? value.question ?? value.query ?? value.text ?? value.content;
|
|
1147
|
-
const userText = typeof rawUserText === 'string' ? rawUserText.trim() : '';
|
|
1148
|
-
|
|
1149
|
-
const rawResponseText =
|
|
1150
|
-
value.response ?? value.answer ?? value.reply ?? value.completion ?? value.body ?? value.preview;
|
|
1151
|
-
const responseText = typeof rawResponseText === 'string' ? rawResponseText.trim() : '';
|
|
1152
|
-
|
|
1153
|
-
const rawId = value.id ?? value.messageId ?? value.uuid ?? value.conversationMessageId;
|
|
1154
|
-
const baseId = rawId !== undefined && rawId !== null
|
|
1155
|
-
? String(rawId)
|
|
1156
|
-
: `history-${index + 1}`;
|
|
1157
|
-
|
|
1158
|
-
const userTimestampValue =
|
|
1159
|
-
value.timestamp ??
|
|
1160
|
-
value.createdAt ??
|
|
1161
|
-
value.created_at ??
|
|
1162
|
-
value.date ??
|
|
1163
|
-
value.time;
|
|
1164
|
-
const assistantTimestampValue =
|
|
1165
|
-
value.responseTimestamp ??
|
|
1166
|
-
value.responseTime ??
|
|
1167
|
-
value.responseDate ??
|
|
1168
|
-
value.response_at ??
|
|
1169
|
-
value.updatedAt ??
|
|
1170
|
-
value.updated_at;
|
|
1171
|
-
|
|
1172
|
-
const userTimestamp = this.parseTimestamp(userTimestampValue);
|
|
1173
|
-
const assistantTimestamp = this.parseTimestamp(
|
|
1174
|
-
assistantTimestampValue,
|
|
1175
|
-
userTimestamp + 1,
|
|
1176
|
-
);
|
|
1177
|
-
|
|
1178
|
-
if (responseText) {
|
|
1179
|
-
if (userText) {
|
|
1180
|
-
messages.push({
|
|
1181
|
-
id: `${baseId}-user`,
|
|
1182
|
-
role: 'user',
|
|
1183
|
-
text: userText,
|
|
1184
|
-
html: this.renderMarkdown(userText),
|
|
1185
|
-
timestamp: userTimestamp,
|
|
1186
|
-
});
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
messages.push({
|
|
1190
|
-
id: `${baseId}-assistant`,
|
|
1191
|
-
role: 'assistant',
|
|
1192
|
-
text: responseText,
|
|
1193
|
-
html: this.renderMarkdown(responseText),
|
|
1194
|
-
timestamp: assistantTimestamp,
|
|
1195
|
-
});
|
|
1196
|
-
} else if (userText) {
|
|
1197
|
-
// Se n�o tiver resposta, n�o exibimos a mensagem do usuario isolada.
|
|
1198
|
-
return [];
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
if (messages.length > 0) {
|
|
1202
|
-
return messages;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
const fallback = this.normalizeSingleHistoryMessage(value, index);
|
|
1206
|
-
return fallback ? [fallback] : [];
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
private normalizeSingleHistoryMessage(
|
|
1210
|
-
value: Record<string, unknown>,
|
|
1211
|
-
index: number,
|
|
1212
|
-
): ChatMessage | null {
|
|
1213
|
-
const rawText =
|
|
1214
|
-
value.text ??
|
|
1215
|
-
value.message ??
|
|
1216
|
-
value.content ??
|
|
1217
|
-
value.response ??
|
|
1218
|
-
value.body ??
|
|
1219
|
-
value.preview;
|
|
1220
|
-
|
|
1221
|
-
const text = typeof rawText === 'string' && rawText.trim().length > 0
|
|
1222
|
-
? rawText
|
|
1223
|
-
: '';
|
|
1224
|
-
|
|
1225
|
-
if (!text) {
|
|
1226
|
-
return null;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
const role = this.normalizeRole(
|
|
1230
|
-
value.role ??
|
|
1231
|
-
value.sender ??
|
|
1232
|
-
value.from ??
|
|
1233
|
-
value.author ??
|
|
1234
|
-
value.type ??
|
|
1235
|
-
value.direction,
|
|
1236
|
-
);
|
|
1237
|
-
|
|
1238
|
-
const rawId = value.id ?? value.messageId ?? value.uuid ?? value.conversationMessageId;
|
|
1239
|
-
const id = rawId !== undefined && rawId !== null
|
|
1240
|
-
? String(rawId)
|
|
1241
|
-
: `history-message-${index + 1}`;
|
|
1242
|
-
|
|
1243
|
-
const timestampValue =
|
|
1244
|
-
value.timestamp ??
|
|
1245
|
-
value.createdAt ??
|
|
1246
|
-
value.created_at ??
|
|
1247
|
-
value.updatedAt ??
|
|
1248
|
-
value.updated_at ??
|
|
1249
|
-
value.date ??
|
|
1250
|
-
value.time;
|
|
1251
|
-
|
|
1252
|
-
const timestamp = this.parseTimestamp(timestampValue);
|
|
1253
|
-
|
|
1254
|
-
return {
|
|
1255
|
-
id,
|
|
1256
|
-
role,
|
|
1257
|
-
text,
|
|
1258
|
-
html: this.renderMarkdown(text),
|
|
1259
|
-
timestamp,
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
private normalizeRole(value: unknown): ChatRole {
|
|
1264
|
-
if (typeof value === 'string') {
|
|
1265
|
-
const normalized = value.toLowerCase();
|
|
1266
|
-
if (normalized.includes('user') || normalized.includes('client')) {
|
|
1267
|
-
return 'user';
|
|
1268
|
-
}
|
|
1269
|
-
if (normalized.includes('assistant') || normalized.includes('agent') || normalized.includes('bot')) {
|
|
1270
|
-
return 'assistant';
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
return 'assistant';
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
private parseTimestamp(value: unknown, fallback?: number) {
|
|
1278
|
-
const parsed = Date.parse(this.toIsoString(value));
|
|
1279
|
-
if (Number.isFinite(parsed)) {
|
|
1280
|
-
return parsed;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
if (Number.isFinite(fallback ?? NaN)) {
|
|
1284
|
-
return fallback as number;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
return Date.now();
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
private lookupConversationTitle(conversationId: string | null) {
|
|
1291
|
-
if (!conversationId) {
|
|
1292
|
-
return null;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
const found = this.conversations.find((item) => item.id === conversationId);
|
|
1296
|
-
return found ? found.title : null;
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
private syncActiveConversationTitle() {
|
|
1300
|
-
if (!this.currentConversationId) {
|
|
1301
|
-
return;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
const title = this.lookupConversationTitle(this.currentConversationId);
|
|
1305
|
-
if (title) {
|
|
1306
|
-
this.activeConversationTitle = title;
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
private toIsoString(value: unknown) {
|
|
1311
|
-
if (typeof value === 'string' || typeof value === 'number') {
|
|
1312
|
-
const date = new Date(value);
|
|
1313
|
-
if (!Number.isNaN(date.getTime())) {
|
|
1314
|
-
return date.toISOString();
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
return new Date().toISOString();
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
private startLoadingGuard() {
|
|
1322
|
-
this.clearLoadingGuard();
|
|
1323
|
-
this.
|
|
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
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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
|
+
}
|
|
1414
1839
|
declare global {
|
|
1415
1840
|
interface HTMLElementTagNameMap {
|
|
1416
1841
|
'rio-assist-widget': RioAssistWidget;
|
|
1417
1842
|
}
|
|
1418
1843
|
}
|
|
1419
|
-
|
|
1420
|
-
if (!customElements.get('rio-assist-widget')) {
|
|
1421
|
-
customElements.define('rio-assist-widget', RioAssistWidget);
|
|
1422
|
-
}
|
|
1844
|
+
|
|
1845
|
+
if (!customElements.get('rio-assist-widget')) {
|
|
1846
|
+
customElements.define('rio-assist-widget', RioAssistWidget);
|
|
1847
|
+
}
|