rio-assist-widget 0.1.1 → 0.1.4
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 +5 -5
- package/dist/rio-assist.js +186 -60
- package/index.html +5 -4
- package/package.json +3 -3
- package/src/components/conversations-panel/conversations-panel.styles.ts +52 -1
- package/src/components/conversations-panel/conversations-panel.template.ts +34 -0
- package/src/components/fullscreen/fullscreen.styles.ts +32 -1
- package/src/components/fullscreen/fullscreen.template.ts +25 -1
- package/src/components/mini-panel/mini-panel.styles.ts +8 -4
- package/src/components/mini-panel/mini-panel.template.ts +2 -2
- package/src/components/rio-assist/rio-assist.ts +502 -381
- package/src/main.ts +72 -70
- package/src/playground.ts +22 -21
- package/src/services/rioWebsocket.ts +167 -167
|
@@ -1,84 +1,87 @@
|
|
|
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
|
-
|
|
9
|
-
type ChatRole = 'user' | 'assistant';
|
|
10
|
-
|
|
11
|
-
export type ChatMessage = {
|
|
12
|
-
id: string;
|
|
13
|
-
role: ChatRole;
|
|
14
|
-
text: string;
|
|
15
|
-
timestamp: number;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
type ConversationItem = {
|
|
19
|
-
id: string;
|
|
20
|
-
title: string;
|
|
21
|
-
updatedAt: string;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export class RioAssistWidget extends LitElement {
|
|
25
|
-
static styles = widgetStyles;
|
|
26
|
-
|
|
27
|
-
static properties = {
|
|
28
|
-
open: { type: Boolean, state: true },
|
|
29
|
-
message: { type: String, state: true },
|
|
30
|
-
titleText: { type: String, attribute: 'data-title' },
|
|
31
|
-
buttonLabel: { type: String, attribute: 'data-button-label' },
|
|
32
|
-
placeholder: { type: String, attribute: 'data-placeholder' },
|
|
33
|
-
accentColor: { type: String, attribute: 'data-accent-color' },
|
|
34
|
-
apiBaseUrl: { type: String, attribute: 'data-api-base-url' },
|
|
35
|
-
rioToken: { type: String, attribute: 'data-rio-token' },
|
|
36
|
-
suggestionsSource: { type: String, attribute: 'data-suggestions' },
|
|
37
|
-
messages: { state: true },
|
|
38
|
-
isLoading: { type: Boolean, state: true },
|
|
39
|
-
errorMessage: { type: String, state: true },
|
|
40
|
-
showConversations: { type: Boolean, state: true },
|
|
41
|
-
conversationSearch: { type: String, state: true },
|
|
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
|
+
|
|
9
|
+
type ChatRole = 'user' | 'assistant';
|
|
10
|
+
|
|
11
|
+
export type ChatMessage = {
|
|
12
|
+
id: string;
|
|
13
|
+
role: ChatRole;
|
|
14
|
+
text: string;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ConversationItem = {
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class RioAssistWidget extends LitElement {
|
|
25
|
+
static styles = widgetStyles;
|
|
26
|
+
|
|
27
|
+
static properties = {
|
|
28
|
+
open: { type: Boolean, state: true },
|
|
29
|
+
message: { type: String, state: true },
|
|
30
|
+
titleText: { type: String, attribute: 'data-title' },
|
|
31
|
+
buttonLabel: { type: String, attribute: 'data-button-label' },
|
|
32
|
+
placeholder: { type: String, attribute: 'data-placeholder' },
|
|
33
|
+
accentColor: { type: String, attribute: 'data-accent-color' },
|
|
34
|
+
apiBaseUrl: { type: String, attribute: 'data-api-base-url' },
|
|
35
|
+
rioToken: { type: String, attribute: 'data-rio-token' },
|
|
36
|
+
suggestionsSource: { type: String, attribute: 'data-suggestions' },
|
|
37
|
+
messages: { state: true },
|
|
38
|
+
isLoading: { type: Boolean, state: true },
|
|
39
|
+
errorMessage: { type: String, state: true },
|
|
40
|
+
showConversations: { type: Boolean, state: true },
|
|
41
|
+
conversationSearch: { type: String, state: true },
|
|
42
42
|
conversationMenuId: { state: true },
|
|
43
43
|
conversationMenuPlacement: { state: true },
|
|
44
44
|
isFullscreen: { type: Boolean, state: true },
|
|
45
45
|
conversationScrollbar: { state: true },
|
|
46
|
+
showNewConversationShortcut: { type: Boolean, state: true },
|
|
46
47
|
};
|
|
47
|
-
|
|
48
|
-
open = false;
|
|
49
|
-
|
|
50
|
-
message = '';
|
|
51
|
-
|
|
52
|
-
titleText = '
|
|
53
|
-
|
|
54
|
-
buttonLabel = '
|
|
55
|
-
|
|
56
|
-
placeholder = 'Pergunte alguma coisa';
|
|
57
|
-
|
|
58
|
-
accentColor = '#008B9A';
|
|
59
|
-
|
|
60
|
-
apiBaseUrl = '';
|
|
61
|
-
|
|
62
|
-
rioToken = '';
|
|
63
|
-
|
|
64
|
-
suggestionsSource = '';
|
|
65
|
-
|
|
66
|
-
messages: ChatMessage[] = [];
|
|
67
|
-
|
|
68
|
-
isLoading = false;
|
|
69
|
-
|
|
70
|
-
errorMessage = '';
|
|
71
|
-
|
|
72
|
-
showConversations = false;
|
|
73
|
-
|
|
74
|
-
conversationSearch = '';
|
|
75
|
-
|
|
48
|
+
|
|
49
|
+
open = false;
|
|
50
|
+
|
|
51
|
+
message = '';
|
|
52
|
+
|
|
53
|
+
titleText = 'Rio Insight';
|
|
54
|
+
|
|
55
|
+
buttonLabel = 'Rio Insight';
|
|
56
|
+
|
|
57
|
+
placeholder = 'Pergunte alguma coisa';
|
|
58
|
+
|
|
59
|
+
accentColor = '#008B9A';
|
|
60
|
+
|
|
61
|
+
apiBaseUrl = '';
|
|
62
|
+
|
|
63
|
+
rioToken = '';
|
|
64
|
+
|
|
65
|
+
suggestionsSource = '';
|
|
66
|
+
|
|
67
|
+
messages: ChatMessage[] = [];
|
|
68
|
+
|
|
69
|
+
isLoading = false;
|
|
70
|
+
|
|
71
|
+
errorMessage = '';
|
|
72
|
+
|
|
73
|
+
showConversations = false;
|
|
74
|
+
|
|
75
|
+
conversationSearch = '';
|
|
76
|
+
|
|
76
77
|
conversationMenuId: string | null = null;
|
|
77
78
|
|
|
78
79
|
conversationMenuPlacement: 'above' | 'below' = 'below';
|
|
79
80
|
|
|
80
81
|
isFullscreen = false;
|
|
81
82
|
|
|
83
|
+
showNewConversationShortcut = false;
|
|
84
|
+
|
|
82
85
|
conversationScrollbar = {
|
|
83
86
|
height: 0,
|
|
84
87
|
top: 0,
|
|
@@ -93,107 +96,121 @@ export class RioAssistWidget extends LitElement {
|
|
|
93
96
|
|
|
94
97
|
private loadingTimer: number | null = null;
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
(_, index) => ({
|
|
98
|
-
id: `${index + 1}`,
|
|
99
|
-
title: [
|
|
100
|
-
'Caminhões com problema na frota de veÃculos.',
|
|
101
|
-
'Próximas manutenções periódicas preventivas.',
|
|
102
|
-
'Quais revisões meu plano inclui?',
|
|
103
|
-
'Como automatizar preenchimento de odômetro.',
|
|
104
|
-
'Valor das peças da próxima revisão.',
|
|
105
|
-
'O que é revisão de assentamento?',
|
|
106
|
-
'Alertas crÃticos ativos.',
|
|
107
|
-
'VeÃculo superaquecendo, causas e recomendações.',
|
|
108
|
-
'Calibragem recomendada nos pneus do e-Delivery.',
|
|
109
|
-
'Quantos mil km trocar o óleo do motor.',
|
|
110
|
-
'Qual a vida útil da bateria Moura M100HE.',
|
|
111
|
-
][index % 11],
|
|
112
|
-
updatedAt: new Date(Date.now() - index * 3600_000).toISOString(),
|
|
113
|
-
}),
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
get suggestions(): string[] {
|
|
117
|
-
if (!this.suggestionsSource) {
|
|
118
|
-
return [];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return this.suggestionsSource
|
|
122
|
-
.split('|')
|
|
123
|
-
.map((item) => item.trim())
|
|
124
|
-
.filter(Boolean);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
protected updated(changedProperties: PropertyValues): void {
|
|
128
|
-
super.updated(changedProperties);
|
|
129
|
-
this.style.setProperty('--accent-color', this.accentColor);
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
changedProperties.has('isFullscreen') ||
|
|
133
|
-
changedProperties.has('showConversations') ||
|
|
134
|
-
changedProperties.has('conversations')
|
|
135
|
-
) {
|
|
136
|
-
this.enqueueConversationScrollbarMeasure();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
protected firstUpdated(): void {
|
|
141
|
-
this.enqueueConversationScrollbarMeasure();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
disconnectedCallback(): void {
|
|
145
|
-
super.disconnectedCallback();
|
|
146
|
-
if (this.conversationScrollbarRaf !== null) {
|
|
147
|
-
cancelAnimationFrame(this.conversationScrollbarRaf);
|
|
148
|
-
this.conversationScrollbarRaf = null;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
this.teardownRioClient();
|
|
152
|
-
this.clearLoadingGuard();
|
|
153
|
-
}
|
|
99
|
+
private conversationScrollbarDraggingId: number | null = null;
|
|
154
100
|
|
|
101
|
+
private conversationScrollbarDragState: {
|
|
102
|
+
startY: number;
|
|
103
|
+
startThumbTop: number;
|
|
104
|
+
trackHeight: number;
|
|
105
|
+
thumbHeight: number;
|
|
106
|
+
list: HTMLElement;
|
|
107
|
+
} | null = null;
|
|
108
|
+
|
|
109
|
+
conversations: ConversationItem[] = Array.from({ length: 20 }).map(
|
|
110
|
+
(_, index) => ({
|
|
111
|
+
id: `${index + 1}`,
|
|
112
|
+
title: [
|
|
113
|
+
'Caminhões com problema na frota de veículos.',
|
|
114
|
+
'Próximas manutenções periódicas preventivas.',
|
|
115
|
+
'Quais revisões meu plano inclui?',
|
|
116
|
+
'Como automatizar preenchimento de odômetro.',
|
|
117
|
+
'Valor das peças da próxima revisão.',
|
|
118
|
+
'O que é revisão de assentamento?',
|
|
119
|
+
'Alertas críticos ativos.',
|
|
120
|
+
'Veículo superaquecendo, causas e recomendações.',
|
|
121
|
+
'Calibragem recomendada nos pneus do e-Delivery.',
|
|
122
|
+
'Quantos mil km trocar o óleo do motor.',
|
|
123
|
+
'Qual a vida útil da bateria Moura M100HE.',
|
|
124
|
+
][index % 11],
|
|
125
|
+
updatedAt: new Date(Date.now() - index * 3600_000).toISOString(),
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
get suggestions(): string[] {
|
|
130
|
+
if (!this.suggestionsSource) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return this.suggestionsSource
|
|
135
|
+
.split('|')
|
|
136
|
+
.map((item) => item.trim())
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
protected updated(changedProperties: PropertyValues): void {
|
|
141
|
+
super.updated(changedProperties);
|
|
142
|
+
this.style.setProperty('--accent-color', this.accentColor);
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
changedProperties.has('isFullscreen') ||
|
|
146
|
+
changedProperties.has('showConversations') ||
|
|
147
|
+
changedProperties.has('conversations')
|
|
148
|
+
) {
|
|
149
|
+
this.enqueueConversationScrollbarMeasure();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
protected firstUpdated(): void {
|
|
154
|
+
this.enqueueConversationScrollbarMeasure();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
disconnectedCallback(): void {
|
|
158
|
+
super.disconnectedCallback();
|
|
159
|
+
if (this.conversationScrollbarRaf !== null) {
|
|
160
|
+
cancelAnimationFrame(this.conversationScrollbarRaf);
|
|
161
|
+
this.conversationScrollbarRaf = null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.teardownRioClient();
|
|
165
|
+
this.clearLoadingGuard();
|
|
166
|
+
}
|
|
167
|
+
|
|
155
168
|
get filteredConversations() {
|
|
156
169
|
const query = this.conversationSearch.trim().toLowerCase();
|
|
157
170
|
if (!query) {
|
|
158
171
|
return this.conversations;
|
|
159
172
|
}
|
|
160
|
-
|
|
173
|
+
|
|
161
174
|
return this.conversations.filter((conversation) =>
|
|
162
175
|
conversation.title.toLowerCase().includes(query),
|
|
163
176
|
);
|
|
164
177
|
}
|
|
165
178
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
this.exitFullscreen(false);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
this.open = !this.open;
|
|
173
|
-
this.dispatchEvent(
|
|
174
|
-
new CustomEvent(this.open ? 'rioassist:open' : 'rioassist:close', {
|
|
175
|
-
bubbles: true,
|
|
176
|
-
composed: true,
|
|
177
|
-
}),
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
closePanel() {
|
|
182
|
-
this.isFullscreen = false;
|
|
183
|
-
if (this.open) {
|
|
184
|
-
this.togglePanel();
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
openConversationsPanel() {
|
|
189
|
-
this.showConversations = true;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
closeConversationsPanel() {
|
|
193
|
-
this.showConversations = false;
|
|
194
|
-
this.conversationMenuId = null;
|
|
179
|
+
get hasActiveConversation() {
|
|
180
|
+
return this.messages.length > 0;
|
|
195
181
|
}
|
|
196
182
|
|
|
183
|
+
togglePanel() {
|
|
184
|
+
if (this.isFullscreen) {
|
|
185
|
+
this.exitFullscreen(false);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.open = !this.open;
|
|
190
|
+
this.dispatchEvent(
|
|
191
|
+
new CustomEvent(this.open ? 'rioassist:open' : 'rioassist:close', {
|
|
192
|
+
bubbles: true,
|
|
193
|
+
composed: true,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
closePanel() {
|
|
199
|
+
this.isFullscreen = false;
|
|
200
|
+
if (this.open) {
|
|
201
|
+
this.togglePanel();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
openConversationsPanel() {
|
|
206
|
+
this.showConversations = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
closeConversationsPanel() {
|
|
210
|
+
this.showConversations = false;
|
|
211
|
+
this.conversationMenuId = null;
|
|
212
|
+
}
|
|
213
|
+
|
|
197
214
|
toggleConversationsPanel() {
|
|
198
215
|
this.showConversations = !this.showConversations;
|
|
199
216
|
if (!this.showConversations) {
|
|
@@ -201,81 +218,85 @@ export class RioAssistWidget extends LitElement {
|
|
|
201
218
|
}
|
|
202
219
|
}
|
|
203
220
|
|
|
204
|
-
|
|
205
|
-
this.
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
handleConversationMenuToggle(event: Event, id: string) {
|
|
209
|
-
event.stopPropagation();
|
|
210
|
-
|
|
211
|
-
if (this.conversationMenuId === id) {
|
|
212
|
-
this.conversationMenuId = null;
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const button = event.currentTarget as HTMLElement;
|
|
217
|
-
const container = this.renderRoot.querySelector(
|
|
218
|
-
'.conversations-panel__surface',
|
|
219
|
-
) as HTMLElement | null;
|
|
220
|
-
|
|
221
|
-
if (button && container) {
|
|
222
|
-
const buttonRect = button.getBoundingClientRect();
|
|
223
|
-
const containerRect = container.getBoundingClientRect();
|
|
224
|
-
const spaceBelow = containerRect.bottom - buttonRect.bottom;
|
|
225
|
-
this.conversationMenuPlacement = spaceBelow < 140 ? 'above' : 'below';
|
|
226
|
-
} else {
|
|
227
|
-
this.conversationMenuPlacement = 'below';
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
this.conversationMenuId = id;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
handleConversationsPanelPointer(event: PointerEvent) {
|
|
234
|
-
const target = event.target as HTMLElement;
|
|
235
|
-
if (
|
|
236
|
-
!target.closest('.conversation-menu') &&
|
|
237
|
-
!target.closest('.conversation-menu-button')
|
|
238
|
-
) {
|
|
239
|
-
this.conversationMenuId = null;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
handleConversationAction(action: 'rename' | 'delete', id: string) {
|
|
244
|
-
this.conversationMenuId = null;
|
|
245
|
-
const conversation = this.conversations.find((item) => item.id === id);
|
|
246
|
-
if (!conversation) {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const message = `${
|
|
251
|
-
action === 'rename' ? 'Renomear' : 'Excluir'
|
|
252
|
-
} "${conversation.title}"`;
|
|
253
|
-
console.info(`[Mock] ${message}`);
|
|
221
|
+
toggleNewConversationShortcut() {
|
|
222
|
+
this.showNewConversationShortcut = !this.showNewConversationShortcut;
|
|
254
223
|
}
|
|
255
224
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
this.exitFullscreen(true);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (this.showConversations) {
|
|
263
|
-
this.closeConversationsPanel();
|
|
264
|
-
} else {
|
|
265
|
-
this.closePanel();
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
enterFullscreen() {
|
|
270
|
-
if (this.isFullscreen) {
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
this.isFullscreen = true;
|
|
275
|
-
this.open = false;
|
|
276
|
-
this.showConversations = false;
|
|
225
|
+
handleConversationSearch(event: InputEvent) {
|
|
226
|
+
this.conversationSearch = (event.target as HTMLInputElement).value;
|
|
277
227
|
}
|
|
278
|
-
|
|
228
|
+
|
|
229
|
+
handleConversationMenuToggle(event: Event, id: string) {
|
|
230
|
+
event.stopPropagation();
|
|
231
|
+
|
|
232
|
+
if (this.conversationMenuId === id) {
|
|
233
|
+
this.conversationMenuId = null;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const button = event.currentTarget as HTMLElement;
|
|
238
|
+
const container = this.renderRoot.querySelector(
|
|
239
|
+
'.conversations-panel__surface',
|
|
240
|
+
) as HTMLElement | null;
|
|
241
|
+
|
|
242
|
+
if (button && container) {
|
|
243
|
+
const buttonRect = button.getBoundingClientRect();
|
|
244
|
+
const containerRect = container.getBoundingClientRect();
|
|
245
|
+
const spaceBelow = containerRect.bottom - buttonRect.bottom;
|
|
246
|
+
this.conversationMenuPlacement = spaceBelow < 140 ? 'above' : 'below';
|
|
247
|
+
} else {
|
|
248
|
+
this.conversationMenuPlacement = 'below';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.conversationMenuId = id;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
handleConversationsPanelPointer(event: PointerEvent) {
|
|
255
|
+
const target = event.target as HTMLElement;
|
|
256
|
+
if (
|
|
257
|
+
!target.closest('.conversation-menu') &&
|
|
258
|
+
!target.closest('.conversation-menu-button')
|
|
259
|
+
) {
|
|
260
|
+
this.conversationMenuId = null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
handleConversationAction(action: 'rename' | 'delete', id: string) {
|
|
265
|
+
this.conversationMenuId = null;
|
|
266
|
+
const conversation = this.conversations.find((item) => item.id === id);
|
|
267
|
+
if (!conversation) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const message = `${
|
|
272
|
+
action === 'rename' ? 'Renomear' : 'Excluir'
|
|
273
|
+
} "${conversation.title}"`;
|
|
274
|
+
console.info(`[Mock] ${message}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
handleCloseAction() {
|
|
278
|
+
if (this.isFullscreen) {
|
|
279
|
+
this.exitFullscreen(true);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (this.showConversations) {
|
|
284
|
+
this.closeConversationsPanel();
|
|
285
|
+
} else {
|
|
286
|
+
this.closePanel();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
enterFullscreen() {
|
|
291
|
+
if (this.isFullscreen) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.isFullscreen = true;
|
|
296
|
+
this.open = false;
|
|
297
|
+
this.showConversations = false;
|
|
298
|
+
}
|
|
299
|
+
|
|
279
300
|
exitFullscreen(restorePanel: boolean) {
|
|
280
301
|
if (!this.isFullscreen) {
|
|
281
302
|
return;
|
|
@@ -283,15 +304,33 @@ export class RioAssistWidget extends LitElement {
|
|
|
283
304
|
|
|
284
305
|
this.isFullscreen = false;
|
|
285
306
|
this.conversationMenuId = null;
|
|
307
|
+
this.showNewConversationShortcut = false;
|
|
286
308
|
if (restorePanel) {
|
|
287
309
|
this.open = true;
|
|
288
310
|
}
|
|
289
311
|
}
|
|
290
|
-
|
|
312
|
+
|
|
291
313
|
handleCreateConversation() {
|
|
292
|
-
|
|
293
|
-
|
|
314
|
+
if (!this.hasActiveConversation) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
294
317
|
|
|
318
|
+
this.clearLoadingGuard();
|
|
319
|
+
this.isLoading = false;
|
|
320
|
+
this.messages = [];
|
|
321
|
+
this.message = '';
|
|
322
|
+
this.errorMessage = '';
|
|
323
|
+
this.showConversations = false;
|
|
324
|
+
this.teardownRioClient();
|
|
325
|
+
this.showNewConversationShortcut = false;
|
|
326
|
+
this.dispatchEvent(
|
|
327
|
+
new CustomEvent('rioassist:new-conversation', {
|
|
328
|
+
bubbles: true,
|
|
329
|
+
composed: true,
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
295
334
|
handleConversationListScroll(event: Event) {
|
|
296
335
|
const target = event.currentTarget as HTMLElement | null;
|
|
297
336
|
if (!target) {
|
|
@@ -300,178 +339,260 @@ export class RioAssistWidget extends LitElement {
|
|
|
300
339
|
this.updateConversationScrollbar(target);
|
|
301
340
|
}
|
|
302
341
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
this.conversationScrollbarRaf = requestAnimationFrame(() => {
|
|
309
|
-
this.conversationScrollbarRaf = null;
|
|
310
|
-
this.updateConversationScrollbar();
|
|
311
|
-
});
|
|
312
|
-
}
|
|
342
|
+
handleConversationScrollbarPointerDown(event: PointerEvent) {
|
|
343
|
+
const track = event.currentTarget as HTMLElement | null;
|
|
344
|
+
const list = this.renderRoot.querySelector(
|
|
345
|
+
'.conversation-list--sidebar',
|
|
346
|
+
) as HTMLElement | null;
|
|
313
347
|
|
|
314
|
-
|
|
315
|
-
const element =
|
|
316
|
-
target ??
|
|
317
|
-
(this.renderRoot.querySelector(
|
|
318
|
-
'.conversation-list--sidebar',
|
|
319
|
-
) as HTMLElement | null);
|
|
320
|
-
|
|
321
|
-
if (!element) {
|
|
322
|
-
if (this.conversationScrollbar.visible) {
|
|
323
|
-
this.conversationScrollbar = { height: 0, top: 0, visible: false };
|
|
324
|
-
}
|
|
348
|
+
if (!track || !list) {
|
|
325
349
|
return;
|
|
326
350
|
}
|
|
327
351
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
352
|
+
const trackRect = track.getBoundingClientRect();
|
|
353
|
+
const thumbHeight = trackRect.height * (this.conversationScrollbar.height / 100);
|
|
354
|
+
const maxThumbTop = Math.max(trackRect.height - thumbHeight, 0);
|
|
355
|
+
const scrollRange = Math.max(list.scrollHeight - list.clientHeight, 1);
|
|
356
|
+
const currentThumbTop = (list.scrollTop / scrollRange) * maxThumbTop;
|
|
357
|
+
const offsetY = event.clientY - trackRect.top;
|
|
358
|
+
const isOnThumb = offsetY >= currentThumbTop && offsetY <= currentThumbTop + thumbHeight;
|
|
359
|
+
|
|
360
|
+
const nextThumbTop = isOnThumb
|
|
361
|
+
? currentThumbTop
|
|
362
|
+
: Math.min(Math.max(offsetY - thumbHeight / 2, 0), maxThumbTop);
|
|
363
|
+
|
|
364
|
+
if (!isOnThumb) {
|
|
365
|
+
list.scrollTop = (nextThumbTop / Math.max(maxThumbTop, 1)) * (list.scrollHeight - list.clientHeight);
|
|
366
|
+
this.updateConversationScrollbar(list);
|
|
334
367
|
}
|
|
335
368
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
top,
|
|
345
|
-
visible: true,
|
|
369
|
+
track.setPointerCapture(event.pointerId);
|
|
370
|
+
this.conversationScrollbarDraggingId = event.pointerId;
|
|
371
|
+
this.conversationScrollbarDragState = {
|
|
372
|
+
startY: event.clientY,
|
|
373
|
+
startThumbTop: nextThumbTop,
|
|
374
|
+
trackHeight: trackRect.height,
|
|
375
|
+
thumbHeight,
|
|
376
|
+
list,
|
|
346
377
|
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async onSuggestionClick(suggestion: string) {
|
|
350
|
-
await this.processMessage(suggestion);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async handleSubmit(event: SubmitEvent) {
|
|
354
378
|
event.preventDefault();
|
|
355
|
-
await this.processMessage(this.message);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
private createMessage(role: ChatRole, text: string): ChatMessage {
|
|
359
|
-
const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
360
|
-
? crypto.randomUUID()
|
|
361
|
-
: `${Date.now()}-${Math.random()}`;
|
|
362
|
-
|
|
363
|
-
return {
|
|
364
|
-
id,
|
|
365
|
-
role,
|
|
366
|
-
text,
|
|
367
|
-
timestamp: Date.now(),
|
|
368
|
-
};
|
|
369
379
|
}
|
|
370
380
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
381
|
+
handleConversationScrollbarPointerMove(event: PointerEvent) {
|
|
382
|
+
if (
|
|
383
|
+
this.conversationScrollbarDraggingId === null ||
|
|
384
|
+
this.conversationScrollbarDraggingId !== event.pointerId ||
|
|
385
|
+
!this.conversationScrollbarDragState
|
|
386
|
+
) {
|
|
374
387
|
return;
|
|
375
388
|
}
|
|
376
389
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
this.isLoading = true;
|
|
394
|
-
this.startLoadingGuard();
|
|
395
|
-
|
|
396
|
-
try {
|
|
397
|
-
const client = this.ensureRioClient();
|
|
398
|
-
await client.sendMessage(content);
|
|
399
|
-
} catch (error) {
|
|
400
|
-
this.clearLoadingGuard();
|
|
401
|
-
this.isLoading = false;
|
|
402
|
-
this.errorMessage = error instanceof Error
|
|
403
|
-
? error.message
|
|
404
|
-
: 'Nao foi possivel enviar a mensagem para o agente.';
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
private ensureRioClient() {
|
|
409
|
-
const token = this.rioToken.trim();
|
|
410
|
-
if (!token) {
|
|
411
|
-
throw new Error(
|
|
412
|
-
'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
|
|
413
|
-
);
|
|
390
|
+
const {
|
|
391
|
+
startY,
|
|
392
|
+
startThumbTop,
|
|
393
|
+
trackHeight,
|
|
394
|
+
thumbHeight,
|
|
395
|
+
list,
|
|
396
|
+
} = this.conversationScrollbarDragState;
|
|
397
|
+
|
|
398
|
+
const maxThumbTop = Math.max(trackHeight - thumbHeight, 0);
|
|
399
|
+
const deltaY = event.clientY - startY;
|
|
400
|
+
const thumbTop = Math.min(Math.max(startThumbTop + deltaY, 0), maxThumbTop);
|
|
401
|
+
const scrollRange = list.scrollHeight - list.clientHeight;
|
|
402
|
+
|
|
403
|
+
if (scrollRange > 0) {
|
|
404
|
+
list.scrollTop = (thumbTop / Math.max(maxThumbTop, 1)) * scrollRange;
|
|
405
|
+
this.updateConversationScrollbar(list);
|
|
414
406
|
}
|
|
415
407
|
|
|
416
|
-
|
|
417
|
-
this.teardownRioClient();
|
|
418
|
-
this.rioClient = new RioWebsocketClient(token);
|
|
419
|
-
this.rioUnsubscribe = this.rioClient.onMessage((incoming) => {
|
|
420
|
-
this.handleIncomingMessage(incoming);
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
return this.rioClient;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
private handleIncomingMessage(message: RioIncomingMessage) {
|
|
428
|
-
const assistantMessage = this.createMessage('assistant', message.text);
|
|
429
|
-
this.messages = [...this.messages, assistantMessage];
|
|
430
|
-
this.clearLoadingGuard();
|
|
431
|
-
this.isLoading = false;
|
|
408
|
+
event.preventDefault();
|
|
432
409
|
}
|
|
433
410
|
|
|
434
|
-
|
|
435
|
-
if (this.
|
|
436
|
-
|
|
437
|
-
this.rioUnsubscribe = null;
|
|
411
|
+
handleConversationScrollbarPointerUp(event: PointerEvent) {
|
|
412
|
+
if (this.conversationScrollbarDraggingId !== event.pointerId) {
|
|
413
|
+
return;
|
|
438
414
|
}
|
|
439
415
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
this.rioClient = null;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
416
|
+
const track = event.currentTarget as HTMLElement | null;
|
|
417
|
+
track?.releasePointerCapture(event.pointerId);
|
|
445
418
|
|
|
446
|
-
|
|
447
|
-
this.
|
|
448
|
-
this.loadingTimer = window.setTimeout(() => {
|
|
449
|
-
this.loadingTimer = null;
|
|
450
|
-
this.isLoading = false;
|
|
451
|
-
}, 15000);
|
|
419
|
+
this.conversationScrollbarDraggingId = null;
|
|
420
|
+
this.conversationScrollbarDragState = null;
|
|
452
421
|
}
|
|
453
422
|
|
|
454
|
-
private
|
|
455
|
-
if (this.
|
|
456
|
-
|
|
457
|
-
this.loadingTimer = null;
|
|
423
|
+
private enqueueConversationScrollbarMeasure() {
|
|
424
|
+
if (this.conversationScrollbarRaf !== null) {
|
|
425
|
+
return;
|
|
458
426
|
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
render() {
|
|
462
|
-
return renderRioAssist(this);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
declare global {
|
|
467
|
-
interface HTMLElementTagNameMap {
|
|
468
|
-
'rio-assist-widget': RioAssistWidget;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
427
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
428
|
+
this.conversationScrollbarRaf = requestAnimationFrame(() => {
|
|
429
|
+
this.conversationScrollbarRaf = null;
|
|
430
|
+
this.updateConversationScrollbar();
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private updateConversationScrollbar(target?: HTMLElement | null) {
|
|
435
|
+
const element =
|
|
436
|
+
target ??
|
|
437
|
+
(this.renderRoot.querySelector(
|
|
438
|
+
'.conversation-list--sidebar',
|
|
439
|
+
) as HTMLElement | null);
|
|
440
|
+
|
|
441
|
+
if (!element) {
|
|
442
|
+
if (this.conversationScrollbar.visible) {
|
|
443
|
+
this.conversationScrollbar = { height: 0, top: 0, visible: false };
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const { scrollHeight, clientHeight, scrollTop } = element;
|
|
449
|
+
if (scrollHeight <= clientHeight + 1) {
|
|
450
|
+
if (this.conversationScrollbar.visible) {
|
|
451
|
+
this.conversationScrollbar = { height: 0, top: 0, visible: false };
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const ratio = clientHeight / scrollHeight;
|
|
457
|
+
const height = Math.max(ratio * 100, 8);
|
|
458
|
+
const maxTop = 100 - height;
|
|
459
|
+
const top =
|
|
460
|
+
scrollTop / (scrollHeight - clientHeight) * (maxTop > 0 ? maxTop : 0);
|
|
461
|
+
|
|
462
|
+
this.conversationScrollbar = {
|
|
463
|
+
height,
|
|
464
|
+
top,
|
|
465
|
+
visible: true,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async onSuggestionClick(suggestion: string) {
|
|
470
|
+
await this.processMessage(suggestion);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async handleSubmit(event: SubmitEvent) {
|
|
474
|
+
event.preventDefault();
|
|
475
|
+
await this.processMessage(this.message);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private createMessage(role: ChatRole, text: string): ChatMessage {
|
|
479
|
+
const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
480
|
+
? crypto.randomUUID()
|
|
481
|
+
: `${Date.now()}-${Math.random()}`;
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
id,
|
|
485
|
+
role,
|
|
486
|
+
text,
|
|
487
|
+
timestamp: Date.now(),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private async processMessage(rawValue: string) {
|
|
492
|
+
const content = rawValue.trim();
|
|
493
|
+
if (!content || this.isLoading) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
this.dispatchEvent(
|
|
498
|
+
new CustomEvent('rioassist:send', {
|
|
499
|
+
detail: {
|
|
500
|
+
message: content,
|
|
501
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
502
|
+
token: this.rioToken,
|
|
503
|
+
},
|
|
504
|
+
bubbles: true,
|
|
505
|
+
composed: true,
|
|
506
|
+
}),
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const userMessage = this.createMessage('user', content);
|
|
510
|
+
this.messages = [...this.messages, userMessage];
|
|
511
|
+
this.message = '';
|
|
512
|
+
this.errorMessage = '';
|
|
513
|
+
this.isLoading = true;
|
|
514
|
+
this.startLoadingGuard();
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
const client = this.ensureRioClient();
|
|
518
|
+
await client.sendMessage(content);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
this.clearLoadingGuard();
|
|
521
|
+
this.isLoading = false;
|
|
522
|
+
this.errorMessage = error instanceof Error
|
|
523
|
+
? error.message
|
|
524
|
+
: 'Nao foi possivel enviar a mensagem para o agente.';
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private ensureRioClient() {
|
|
529
|
+
const token = this.rioToken.trim();
|
|
530
|
+
if (!token) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
'Informe o token RIO em data-rio-token para conectar no websocket do assistente.',
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (!this.rioClient || !this.rioClient.matchesToken(token)) {
|
|
537
|
+
this.teardownRioClient();
|
|
538
|
+
this.rioClient = new RioWebsocketClient(token);
|
|
539
|
+
this.rioUnsubscribe = this.rioClient.onMessage((incoming) => {
|
|
540
|
+
this.handleIncomingMessage(incoming);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return this.rioClient;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private handleIncomingMessage(message: RioIncomingMessage) {
|
|
548
|
+
const assistantMessage = this.createMessage('assistant', message.text);
|
|
549
|
+
this.messages = [...this.messages, assistantMessage];
|
|
550
|
+
this.clearLoadingGuard();
|
|
551
|
+
this.isLoading = false;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private teardownRioClient() {
|
|
555
|
+
if (this.rioUnsubscribe) {
|
|
556
|
+
this.rioUnsubscribe();
|
|
557
|
+
this.rioUnsubscribe = null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (this.rioClient) {
|
|
561
|
+
this.rioClient.close();
|
|
562
|
+
this.rioClient = null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private startLoadingGuard() {
|
|
567
|
+
this.clearLoadingGuard();
|
|
568
|
+
this.loadingTimer = window.setTimeout(() => {
|
|
569
|
+
this.loadingTimer = null;
|
|
570
|
+
this.isLoading = false;
|
|
571
|
+
}, 15000);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private clearLoadingGuard() {
|
|
575
|
+
if (this.loadingTimer !== null) {
|
|
576
|
+
window.clearTimeout(this.loadingTimer);
|
|
577
|
+
this.loadingTimer = null;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
render() {
|
|
582
|
+
return renderRioAssist(this);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
}
|
|
586
|
+
declare global {
|
|
587
|
+
interface HTMLElementTagNameMap {
|
|
588
|
+
'rio-assist-widget': RioAssistWidget;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (!customElements.get('rio-assist-widget')) {
|
|
593
|
+
customElements.define('rio-assist-widget', RioAssistWidget);
|
|
594
|
+
}
|
|
595
|
+
|
|
475
596
|
|
|
476
597
|
|
|
477
598
|
|