rio-assist-widget 0.1.0
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 +57 -0
- package/dist/rio-assist.js +1087 -0
- package/index.html +46 -0
- package/package.json +27 -0
- package/playground-preview.png +0 -0
- package/src/assets/icons/checkFrame.png +0 -0
- package/src/assets/icons/edit.png +0 -0
- package/src/assets/icons/expandScreen.png +0 -0
- package/src/assets/icons/hamburgerMenuIcon.png +0 -0
- package/src/assets/icons/homeIcon.png +0 -0
- package/src/assets/icons/iaButtonIcon.png +0 -0
- package/src/assets/icons/iaCentralIcon.png +0 -0
- package/src/assets/icons/infoFrame.png +0 -0
- package/src/assets/icons/plusFileSelection.png +0 -0
- package/src/assets/icons/profileFrame.png +0 -0
- package/src/assets/icons/searchIcon.png +0 -0
- package/src/assets/icons/threePoints.png +0 -0
- package/src/assets/icons/trash.png +0 -0
- package/src/assets/icons/voiceRecoverIcon.png +0 -0
- package/src/components/conversations-panel/conversations-panel.styles.ts +243 -0
- package/src/components/conversations-panel/conversations-panel.template.ts +150 -0
- package/src/components/floating-button/floating-button.styles.ts +48 -0
- package/src/components/floating-button/floating-button.template.ts +16 -0
- package/src/components/fullscreen/fullscreen.styles.ts +159 -0
- package/src/components/fullscreen/fullscreen.template.ts +71 -0
- package/src/components/mini-panel/mini-panel.styles.ts +331 -0
- package/src/components/mini-panel/mini-panel.template.ts +167 -0
- package/src/components/rio-assist/index.ts +1 -0
- package/src/components/rio-assist/rio-assist.styles.ts +33 -0
- package/src/components/rio-assist/rio-assist.template.ts +21 -0
- package/src/components/rio-assist/rio-assist.ts +478 -0
- package/src/main.ts +72 -0
- package/src/playground.ts +23 -0
- package/src/services/rioWebsocket.ts +167 -0
- package/tsconfig.json +26 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +19 -0
- package/widget.png +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
3
|
+
import type { RioAssistWidget } from '../rio-assist/rio-assist';
|
|
4
|
+
import { renderConversationsPanel } from '../conversations-panel/conversations-panel.template';
|
|
5
|
+
|
|
6
|
+
const hamburgerIconUrl = new URL('../../assets/icons/hamburgerMenuIcon.png', import.meta.url).href;
|
|
7
|
+
const expandIconUrl = new URL('../../assets/icons/expandScreen.png', import.meta.url).href;
|
|
8
|
+
const iaCentralIconUrl = new URL('../../assets/icons/iaCentralIcon.png', import.meta.url).href;
|
|
9
|
+
const plusFileSelectionUrl = new URL('../../assets/icons/plusFileSelection.png', import.meta.url).href;
|
|
10
|
+
|
|
11
|
+
export const renderChatSurface = (component: RioAssistWidget) => {
|
|
12
|
+
const hasMessages = component.messages.length > 0;
|
|
13
|
+
|
|
14
|
+
const heroCard = html`
|
|
15
|
+
<div class="hero-card">
|
|
16
|
+
<img src=${iaCentralIconUrl} alt="IA assistente" class="hero-card__icon" />
|
|
17
|
+
<h3>Como posso te ajudar hoje?</h3>
|
|
18
|
+
</div>
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const conversation = html`
|
|
22
|
+
<div class="conversation">
|
|
23
|
+
${component.messages.map(
|
|
24
|
+
(message) => html`
|
|
25
|
+
<div
|
|
26
|
+
class=${classMap({
|
|
27
|
+
message: true,
|
|
28
|
+
'message--user': message.role === 'user',
|
|
29
|
+
'message--assistant': message.role === 'assistant',
|
|
30
|
+
})}
|
|
31
|
+
>
|
|
32
|
+
<p>${message.text}</p>
|
|
33
|
+
<time>
|
|
34
|
+
${new Date(message.timestamp).toLocaleTimeString('pt-BR', {
|
|
35
|
+
hour: '2-digit',
|
|
36
|
+
minute: '2-digit',
|
|
37
|
+
})}
|
|
38
|
+
</time>
|
|
39
|
+
</div>
|
|
40
|
+
`,
|
|
41
|
+
)}
|
|
42
|
+
${component.isLoading
|
|
43
|
+
? html`
|
|
44
|
+
<div class="message message--assistant typing">
|
|
45
|
+
<span>IA está respondendo...</span>
|
|
46
|
+
</div>
|
|
47
|
+
`
|
|
48
|
+
: null}
|
|
49
|
+
</div>
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
return html`
|
|
53
|
+
<div class="panel-body">
|
|
54
|
+
<div
|
|
55
|
+
class=${classMap({
|
|
56
|
+
'panel-content': true,
|
|
57
|
+
'panel-content--empty': !hasMessages,
|
|
58
|
+
})}
|
|
59
|
+
>
|
|
60
|
+
${hasMessages ? conversation : heroCard}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
${component.errorMessage
|
|
64
|
+
? html`<p class="error-banner">${component.errorMessage}</p>`
|
|
65
|
+
: null}
|
|
66
|
+
|
|
67
|
+
<div class="panel-footer">
|
|
68
|
+
${component.suggestions.length > 0
|
|
69
|
+
? html`
|
|
70
|
+
<div class="suggestions-wrapper">
|
|
71
|
+
<p class="suggestions-label">Sugestoes de perguntas:</p>
|
|
72
|
+
<div class="suggestions">
|
|
73
|
+
${component.suggestions.map(
|
|
74
|
+
(suggestion) => html`
|
|
75
|
+
<button
|
|
76
|
+
class="suggestion"
|
|
77
|
+
type="button"
|
|
78
|
+
@click=${() => component.onSuggestionClick(suggestion)}
|
|
79
|
+
>
|
|
80
|
+
${suggestion}
|
|
81
|
+
</button>
|
|
82
|
+
`,
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
`
|
|
87
|
+
: null}
|
|
88
|
+
|
|
89
|
+
<form
|
|
90
|
+
@submit=${(event: SubmitEvent) => component.handleSubmit(event)}
|
|
91
|
+
aria-busy=${component.isLoading}
|
|
92
|
+
>
|
|
93
|
+
<input
|
|
94
|
+
type="text"
|
|
95
|
+
placeholder=${component.placeholder}
|
|
96
|
+
.value=${component.message}
|
|
97
|
+
@input=${(event: InputEvent) => {
|
|
98
|
+
component.message = (event.target as HTMLInputElement).value;
|
|
99
|
+
}}
|
|
100
|
+
?disabled=${component.isLoading}
|
|
101
|
+
/>
|
|
102
|
+
</form>
|
|
103
|
+
|
|
104
|
+
<p class="footnote">
|
|
105
|
+
IA pode cometer erros. Por isso lembre-se de conferir informacoes importantes.
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
`;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const renderMiniPanel = (component: RioAssistWidget) => {
|
|
113
|
+
const chatSurface = renderChatSurface(component);
|
|
114
|
+
|
|
115
|
+
return html`
|
|
116
|
+
<aside class=${classMap({ panel: true, open: component.open })} role="dialog">
|
|
117
|
+
<header class="panel-header">
|
|
118
|
+
<div class="panel-header__top">
|
|
119
|
+
<span class="panel-title">${component.titleText}</span>
|
|
120
|
+
<button
|
|
121
|
+
class="close-button"
|
|
122
|
+
@click=${() => component.handleCloseAction()}
|
|
123
|
+
aria-label="Fechar RIO Assist"
|
|
124
|
+
>
|
|
125
|
+
x
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="panel-header__actions">
|
|
129
|
+
<button
|
|
130
|
+
class="conversations-button"
|
|
131
|
+
type="button"
|
|
132
|
+
@click=${() => component.toggleConversationsPanel()}
|
|
133
|
+
>
|
|
134
|
+
<img src=${hamburgerIconUrl} alt="" aria-hidden="true" />
|
|
135
|
+
Minhas Conversas
|
|
136
|
+
</button>
|
|
137
|
+
<div class="panel-header__icons">
|
|
138
|
+
${component.showConversations
|
|
139
|
+
? html`
|
|
140
|
+
<button
|
|
141
|
+
class="panel-header__icon-button conversations-plus-button"
|
|
142
|
+
type="button"
|
|
143
|
+
aria-label="Nova conversa"
|
|
144
|
+
@click=${() => component.handleCreateConversation()}
|
|
145
|
+
>
|
|
146
|
+
<img src=${plusFileSelectionUrl} alt="" aria-hidden="true" />
|
|
147
|
+
</button>
|
|
148
|
+
`
|
|
149
|
+
: null}
|
|
150
|
+
<button
|
|
151
|
+
class="panel-header__icon-button"
|
|
152
|
+
type="button"
|
|
153
|
+
aria-label="Expandir painel"
|
|
154
|
+
@click=${() => component.enterFullscreen()}
|
|
155
|
+
>
|
|
156
|
+
<img src=${expandIconUrl} alt="" aria-hidden="true" />
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</header>
|
|
161
|
+
|
|
162
|
+
${chatSurface}
|
|
163
|
+
|
|
164
|
+
${renderConversationsPanel(component, { variant: 'drawer' })}
|
|
165
|
+
</aside>
|
|
166
|
+
`;
|
|
167
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RioAssistWidget } from './rio-assist';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
import { floatingButtonStyles } from '../floating-button/floating-button.styles';
|
|
3
|
+
import { miniPanelStyles } from '../mini-panel/mini-panel.styles';
|
|
4
|
+
import { fullscreenStyles } from '../fullscreen/fullscreen.styles';
|
|
5
|
+
import { conversationsPanelStyles } from '../conversations-panel/conversations-panel.styles';
|
|
6
|
+
|
|
7
|
+
const baseStyles = css`
|
|
8
|
+
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@700&display=swap');
|
|
9
|
+
|
|
10
|
+
:host {
|
|
11
|
+
position: fixed;
|
|
12
|
+
inset: 0;
|
|
13
|
+
font-family: 'Inter', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
|
14
|
+
color: #1c2a33;
|
|
15
|
+
z-index: 2147483000;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
button {
|
|
19
|
+
font: inherit;
|
|
20
|
+
border: none;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
border-radius: 999px;
|
|
23
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
export const widgetStyles = [
|
|
28
|
+
baseStyles,
|
|
29
|
+
floatingButtonStyles,
|
|
30
|
+
miniPanelStyles,
|
|
31
|
+
fullscreenStyles,
|
|
32
|
+
conversationsPanelStyles,
|
|
33
|
+
];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
3
|
+
import type { RioAssistWidget } from './rio-assist';
|
|
4
|
+
import { renderFloatingButton } from '../floating-button/floating-button.template';
|
|
5
|
+
import { renderMiniPanel } from '../mini-panel/mini-panel.template';
|
|
6
|
+
import { renderFullscreen } from '../fullscreen/fullscreen.template';
|
|
7
|
+
|
|
8
|
+
export const renderRioAssist = (component: RioAssistWidget) => {
|
|
9
|
+
const canvasClasses = classMap({
|
|
10
|
+
canvas: true,
|
|
11
|
+
'canvas--fullscreen': component.isFullscreen,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return html`
|
|
15
|
+
<div class=${canvasClasses}>
|
|
16
|
+
${renderFloatingButton(component)}
|
|
17
|
+
${renderMiniPanel(component)}
|
|
18
|
+
${component.isFullscreen ? renderFullscreen(component) : null}
|
|
19
|
+
</div>
|
|
20
|
+
`;
|
|
21
|
+
};
|
|
@@ -0,0 +1,478 @@
|
|
|
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
|
+
conversationMenuId: { state: true },
|
|
43
|
+
conversationMenuPlacement: { state: true },
|
|
44
|
+
isFullscreen: { type: Boolean, state: true },
|
|
45
|
+
conversationScrollbar: { state: true },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
open = false;
|
|
49
|
+
|
|
50
|
+
message = '';
|
|
51
|
+
|
|
52
|
+
titleText = 'RIO Assist';
|
|
53
|
+
|
|
54
|
+
buttonLabel = 'RIO Assist';
|
|
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
|
+
|
|
76
|
+
conversationMenuId: string | null = null;
|
|
77
|
+
|
|
78
|
+
conversationMenuPlacement: 'above' | 'below' = 'below';
|
|
79
|
+
|
|
80
|
+
isFullscreen = false;
|
|
81
|
+
|
|
82
|
+
conversationScrollbar = {
|
|
83
|
+
height: 0,
|
|
84
|
+
top: 0,
|
|
85
|
+
visible: false,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
private conversationScrollbarRaf: number | null = null;
|
|
89
|
+
|
|
90
|
+
private rioClient: RioWebsocketClient | null = null;
|
|
91
|
+
|
|
92
|
+
private rioUnsubscribe: (() => void) | null = null;
|
|
93
|
+
|
|
94
|
+
private loadingTimer: number | null = null;
|
|
95
|
+
|
|
96
|
+
conversations: ConversationItem[] = Array.from({ length: 20 }).map(
|
|
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
|
+
}
|
|
154
|
+
|
|
155
|
+
get filteredConversations() {
|
|
156
|
+
const query = this.conversationSearch.trim().toLowerCase();
|
|
157
|
+
if (!query) {
|
|
158
|
+
return this.conversations;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return this.conversations.filter((conversation) =>
|
|
162
|
+
conversation.title.toLowerCase().includes(query),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
togglePanel() {
|
|
167
|
+
if (this.isFullscreen) {
|
|
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;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
toggleConversationsPanel() {
|
|
198
|
+
this.showConversations = !this.showConversations;
|
|
199
|
+
if (!this.showConversations) {
|
|
200
|
+
this.conversationMenuId = null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
handleConversationSearch(event: InputEvent) {
|
|
205
|
+
this.conversationSearch = (event.target as HTMLInputElement).value;
|
|
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}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
handleCloseAction() {
|
|
257
|
+
if (this.isFullscreen) {
|
|
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;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
exitFullscreen(restorePanel: boolean) {
|
|
280
|
+
if (!this.isFullscreen) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.isFullscreen = false;
|
|
285
|
+
this.conversationMenuId = null;
|
|
286
|
+
if (restorePanel) {
|
|
287
|
+
this.open = true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
handleCreateConversation() {
|
|
292
|
+
console.info('[Mock] Criar nova conversa');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
handleConversationListScroll(event: Event) {
|
|
296
|
+
const target = event.currentTarget as HTMLElement | null;
|
|
297
|
+
if (!target) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.updateConversationScrollbar(target);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private enqueueConversationScrollbarMeasure() {
|
|
304
|
+
if (this.conversationScrollbarRaf !== null) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.conversationScrollbarRaf = requestAnimationFrame(() => {
|
|
309
|
+
this.conversationScrollbarRaf = null;
|
|
310
|
+
this.updateConversationScrollbar();
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private updateConversationScrollbar(target?: HTMLElement | null) {
|
|
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
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const { scrollHeight, clientHeight, scrollTop } = element;
|
|
329
|
+
if (scrollHeight <= clientHeight + 1) {
|
|
330
|
+
if (this.conversationScrollbar.visible) {
|
|
331
|
+
this.conversationScrollbar = { height: 0, top: 0, visible: false };
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const ratio = clientHeight / scrollHeight;
|
|
337
|
+
const height = Math.max(ratio * 100, 8);
|
|
338
|
+
const maxTop = 100 - height;
|
|
339
|
+
const top =
|
|
340
|
+
scrollTop / (scrollHeight - clientHeight) * (maxTop > 0 ? maxTop : 0);
|
|
341
|
+
|
|
342
|
+
this.conversationScrollbar = {
|
|
343
|
+
height,
|
|
344
|
+
top,
|
|
345
|
+
visible: true,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async onSuggestionClick(suggestion: string) {
|
|
350
|
+
await this.processMessage(suggestion);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async handleSubmit(event: SubmitEvent) {
|
|
354
|
+
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
|
+
}
|
|
370
|
+
|
|
371
|
+
private async processMessage(rawValue: string) {
|
|
372
|
+
const content = rawValue.trim();
|
|
373
|
+
if (!content || this.isLoading) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.dispatchEvent(
|
|
378
|
+
new CustomEvent('rioassist:send', {
|
|
379
|
+
detail: {
|
|
380
|
+
message: content,
|
|
381
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
382
|
+
token: this.rioToken,
|
|
383
|
+
},
|
|
384
|
+
bubbles: true,
|
|
385
|
+
composed: true,
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const userMessage = this.createMessage('user', content);
|
|
390
|
+
this.messages = [...this.messages, userMessage];
|
|
391
|
+
this.message = '';
|
|
392
|
+
this.errorMessage = '';
|
|
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
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!this.rioClient || !this.rioClient.matchesToken(token)) {
|
|
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;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private teardownRioClient() {
|
|
435
|
+
if (this.rioUnsubscribe) {
|
|
436
|
+
this.rioUnsubscribe();
|
|
437
|
+
this.rioUnsubscribe = null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (this.rioClient) {
|
|
441
|
+
this.rioClient.close();
|
|
442
|
+
this.rioClient = null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private startLoadingGuard() {
|
|
447
|
+
this.clearLoadingGuard();
|
|
448
|
+
this.loadingTimer = window.setTimeout(() => {
|
|
449
|
+
this.loadingTimer = null;
|
|
450
|
+
this.isLoading = false;
|
|
451
|
+
}, 15000);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private clearLoadingGuard() {
|
|
455
|
+
if (this.loadingTimer !== null) {
|
|
456
|
+
window.clearTimeout(this.loadingTimer);
|
|
457
|
+
this.loadingTimer = null;
|
|
458
|
+
}
|
|
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
|
+
|
|
472
|
+
if (!customElements.get('rio-assist-widget')) {
|
|
473
|
+
customElements.define('rio-assist-widget', RioAssistWidget);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
|