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.
Files changed (38) hide show
  1. package/README.md +57 -0
  2. package/dist/rio-assist.js +1087 -0
  3. package/index.html +46 -0
  4. package/package.json +27 -0
  5. package/playground-preview.png +0 -0
  6. package/src/assets/icons/checkFrame.png +0 -0
  7. package/src/assets/icons/edit.png +0 -0
  8. package/src/assets/icons/expandScreen.png +0 -0
  9. package/src/assets/icons/hamburgerMenuIcon.png +0 -0
  10. package/src/assets/icons/homeIcon.png +0 -0
  11. package/src/assets/icons/iaButtonIcon.png +0 -0
  12. package/src/assets/icons/iaCentralIcon.png +0 -0
  13. package/src/assets/icons/infoFrame.png +0 -0
  14. package/src/assets/icons/plusFileSelection.png +0 -0
  15. package/src/assets/icons/profileFrame.png +0 -0
  16. package/src/assets/icons/searchIcon.png +0 -0
  17. package/src/assets/icons/threePoints.png +0 -0
  18. package/src/assets/icons/trash.png +0 -0
  19. package/src/assets/icons/voiceRecoverIcon.png +0 -0
  20. package/src/components/conversations-panel/conversations-panel.styles.ts +243 -0
  21. package/src/components/conversations-panel/conversations-panel.template.ts +150 -0
  22. package/src/components/floating-button/floating-button.styles.ts +48 -0
  23. package/src/components/floating-button/floating-button.template.ts +16 -0
  24. package/src/components/fullscreen/fullscreen.styles.ts +159 -0
  25. package/src/components/fullscreen/fullscreen.template.ts +71 -0
  26. package/src/components/mini-panel/mini-panel.styles.ts +331 -0
  27. package/src/components/mini-panel/mini-panel.template.ts +167 -0
  28. package/src/components/rio-assist/index.ts +1 -0
  29. package/src/components/rio-assist/rio-assist.styles.ts +33 -0
  30. package/src/components/rio-assist/rio-assist.template.ts +21 -0
  31. package/src/components/rio-assist/rio-assist.ts +478 -0
  32. package/src/main.ts +72 -0
  33. package/src/playground.ts +23 -0
  34. package/src/services/rioWebsocket.ts +167 -0
  35. package/tsconfig.json +26 -0
  36. package/tsconfig.node.json +11 -0
  37. package/vite.config.ts +19 -0
  38. 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
+