rio-assist-widget 0.1.7 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rio-assist-widget",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Web Component do painel lateral Rio Insight, pronto para ser embutido em qualquer projeto.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -26,9 +26,13 @@
26
26
  "README.md"
27
27
  ],
28
28
  "dependencies": {
29
- "lit": "^3.1.2"
29
+ "dompurify": "^3.3.0",
30
+ "lit": "^3.1.2",
31
+ "markdown-it": "^14.1.0",
32
+ "markdown-it-task-lists": "^2.1.1"
30
33
  },
31
34
  "devDependencies": {
35
+ "@types/dompurify": "^3.0.5",
32
36
  "playwright-chromium": "^1.56.1",
33
37
  "typescript": "^5.4.5",
34
38
  "vite": "^5.2.0"
@@ -108,6 +108,7 @@ export const conversationsPanelStyles = css`
108
108
  flex-direction: column;
109
109
  gap: 8px;
110
110
  min-height: 0;
111
+ box-sizing: border-box;
111
112
  }
112
113
 
113
114
  .conversation-list-wrapper {
@@ -125,15 +126,21 @@ export const conversationsPanelStyles = css`
125
126
  .conversations-panel--sidebar .conversation-list {
126
127
  width: 100%;
127
128
  padding-left: 16px;
128
- padding-right: 20px;
129
+ padding-right: 14px;
129
130
  overflow-y: auto;
130
131
  min-height: 0;
132
+ scrollbar-width: none;
133
+ }
134
+
135
+ .conversations-panel--sidebar .conversation-list::-webkit-scrollbar {
136
+ width: 0;
137
+ height: 0;
131
138
  }
132
139
 
133
140
  .conversation-scrollbar {
134
141
  position: absolute;
135
142
  top: 8px;
136
- right: 6px;
143
+ right: 8px;
137
144
  width: 8px;
138
145
  height: calc(100% - 8px);
139
146
  border-radius: 999px;
@@ -261,27 +268,26 @@ export const conversationsPanelStyles = css`
261
268
 
262
269
  .new-conversation-cta__button {
263
270
  width: 100%;
264
- height: 44px;
265
- border-radius: 10px;
271
+ height: 34px;
272
+ border-radius: 4px;
266
273
  border: 1px solid #30b4c0;
267
274
  background: #fff;
268
- color: #008b9a;
275
+ color: #30b4c0;
269
276
  font-family: 'Source Sans Pro', 'Inter', sans-serif;
270
- font-size: 15px;
271
- font-weight: 600;
277
+ font-size: 14px;
278
+ font-weight: 500;
272
279
  display: inline-flex;
273
280
  align-items: center;
274
281
  justify-content: center;
275
- gap: 10px;
276
- padding: 10px 14px;
277
- box-shadow: 0 8px 18px rgba(0, 0, 0, 0.04);
282
+ gap: 8px;
283
+ padding: 8px 12px;
278
284
  cursor: pointer;
279
285
  transition: opacity 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
280
286
  }
281
287
 
282
288
  .new-conversation-cta__button img {
283
- width: 18px;
284
- height: 18px;
289
+ width: 24px;
290
+ height: 24px;
285
291
  }
286
292
 
287
293
  .new-conversation-cta__button:disabled,
@@ -169,25 +169,58 @@ export const miniPanelStyles = css`
169
169
  gap: 12px;
170
170
  }
171
171
 
172
- .message {
173
- border-radius: 16px;
174
- border: 1px solid #e4eaee;
175
- padding: 10px 16px;
176
- max-width: 90%;
172
+ .message {
173
+ border-radius: 16px;
174
+ border: 1px solid #e4eaee;
175
+ padding: 10px 16px;
176
+ max-width: 90%;
177
177
  background: #fff;
178
178
  color: #1f2f36;
179
- font-size: 15px;
180
- }
181
-
182
- .message p {
183
- margin: 0;
184
- line-height: 1.35;
185
- }
186
-
187
- .message--user {
188
- align-self: flex-end;
189
- background: #e5ebf0;
190
- border-color: #cfd6dc;
179
+ font-size: 15px;
180
+ }
181
+
182
+ .message__content {
183
+ line-height: 1.35;
184
+ }
185
+
186
+ .message__content p,
187
+ .message__content ul,
188
+ .message__content ol {
189
+ margin: 4px 0;
190
+ }
191
+
192
+ .message__content pre {
193
+ background: #0d161b;
194
+ color: #f3f7fb;
195
+ border-radius: 8px;
196
+ padding: 10px;
197
+ overflow-x: auto;
198
+ margin: 6px 0;
199
+ font-size: 14px;
200
+ }
201
+
202
+ .message__content code {
203
+ background: #f1f4f7;
204
+ padding: 2px 6px;
205
+ border-radius: 6px;
206
+ }
207
+
208
+ .message__content blockquote {
209
+ border-left: 3px solid #cfd6dc;
210
+ margin: 6px 0;
211
+ padding-left: 10px;
212
+ color: #4b5a65;
213
+ }
214
+
215
+ .message__content ul,
216
+ .message__content ol {
217
+ padding-left: 20px;
218
+ }
219
+
220
+ .message--user {
221
+ align-self: flex-end;
222
+ background: #e5ebf0;
223
+ border-color: #cfd6dc;
191
224
  color: #1f2f36;
192
225
  padding: 8px 10px;
193
226
  }
@@ -1,4 +1,5 @@
1
1
  import { html } from 'lit';
2
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
2
3
  import { classMap } from 'lit/directives/class-map.js';
3
4
  import type { RioAssistWidget } from '../rio-assist/rio-assist';
4
5
  import { renderConversationsPanel } from '../conversations-panel/conversations-panel.template';
@@ -30,7 +31,9 @@ export const renderChatSurface = (component: RioAssistWidget) => {
30
31
  'message--assistant': message.role === 'assistant',
31
32
  })}
32
33
  >
33
- <p>${message.text}</p>
34
+ <div class="message__content">
35
+ ${unsafeHTML(message.html ?? message.text)}
36
+ </div>
34
37
  <time>
35
38
  ${new Date(message.timestamp).toLocaleTimeString('pt-BR', {
36
39
  hour: '2-digit',
@@ -43,7 +46,7 @@ export const renderChatSurface = (component: RioAssistWidget) => {
43
46
  ${component.isLoading
44
47
  ? html`
45
48
  <div class="message message--assistant typing">
46
- <span>IA está respondendo...</span>
49
+ <span>Rio Insight está respondendo...</span>
47
50
  </div>
48
51
  `
49
52
  : null}
@@ -1,19 +1,23 @@
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';
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';
8
11
 
9
12
  type ChatRole = 'user' | 'assistant';
10
13
 
11
- export type ChatMessage = {
12
- id: string;
13
- role: ChatRole;
14
- text: string;
15
- timestamp: number;
16
- };
14
+ export type ChatMessage = {
15
+ id: string;
16
+ role: ChatRole;
17
+ text: string;
18
+ html?: string;
19
+ timestamp: number;
20
+ };
17
21
 
18
22
  type ConversationItem = {
19
23
  id: string;
@@ -98,13 +102,19 @@ export class RioAssistWidget extends LitElement {
98
102
 
99
103
  private conversationScrollbarDraggingId: number | null = null;
100
104
 
101
- private conversationScrollbarDragState: {
102
- startY: number;
103
- startThumbTop: number;
104
- trackHeight: number;
105
- thumbHeight: number;
106
- list: HTMLElement;
107
- } | null = null;
105
+ private conversationScrollbarDragState: {
106
+ startY: number;
107
+ startThumbTop: number;
108
+ trackHeight: number;
109
+ thumbHeight: number;
110
+ list: HTMLElement;
111
+ } | null = null;
112
+
113
+ private markdownRenderer = new MarkdownIt({
114
+ html: false,
115
+ linkify: true,
116
+ breaks: true,
117
+ }).use(markdownItTaskLists);
108
118
 
109
119
  conversations: ConversationItem[] = Array.from({ length: 20 }).map(
110
120
  (_, index) => ({
@@ -479,23 +489,24 @@ export class RioAssistWidget extends LitElement {
479
489
  await this.processMessage(suggestion);
480
490
  }
481
491
 
482
- async handleSubmit(event: SubmitEvent) {
483
- event.preventDefault();
484
- await this.processMessage(this.message);
485
- }
486
-
487
- private createMessage(role: ChatRole, text: string): ChatMessage {
488
- const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto
489
- ? crypto.randomUUID()
490
- : `${Date.now()}-${Math.random()}`;
491
-
492
- return {
493
- id,
494
- role,
495
- text,
496
- timestamp: Date.now(),
497
- };
498
- }
492
+ async handleSubmit(event: SubmitEvent) {
493
+ event.preventDefault();
494
+ await this.processMessage(this.message);
495
+ }
496
+
497
+ private createMessage(role: ChatRole, text: string): ChatMessage {
498
+ const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto
499
+ ? crypto.randomUUID()
500
+ : `${Date.now()}-${Math.random()}`;
501
+
502
+ return {
503
+ id,
504
+ role,
505
+ text,
506
+ html: this.renderMarkdown(text),
507
+ timestamp: Date.now(),
508
+ };
509
+ }
499
510
 
500
511
  private async processMessage(rawValue: string) {
501
512
  const content = rawValue.trim();
@@ -604,6 +615,67 @@ export class RioAssistWidget extends LitElement {
604
615
  });
605
616
  }
606
617
 
618
+ private renderMarkdown(content: string) {
619
+ const rendered = this.markdownRenderer.render(content);
620
+ const clean = DOMPurify.sanitize(rendered, {
621
+ ALLOWED_TAGS: [
622
+ 'a',
623
+ 'p',
624
+ 'ul',
625
+ 'ol',
626
+ 'li',
627
+ 'code',
628
+ 'pre',
629
+ 'strong',
630
+ 'em',
631
+ 'blockquote',
632
+ 'table',
633
+ 'thead',
634
+ 'tbody',
635
+ 'tr',
636
+ 'th',
637
+ 'td',
638
+ 'del',
639
+ 'hr',
640
+ 'br',
641
+ 'img',
642
+ 'span',
643
+ 'input',
644
+ ],
645
+ ALLOWED_ATTR: [
646
+ 'href',
647
+ 'title',
648
+ 'target',
649
+ 'rel',
650
+ 'src',
651
+ 'alt',
652
+ 'class',
653
+ 'type',
654
+ 'checked',
655
+ 'disabled',
656
+ 'aria-label',
657
+ ],
658
+ ALLOW_DATA_ATTR: false,
659
+ FORBID_TAGS: ['style', 'script'],
660
+ USE_PROFILES: { html: true },
661
+ });
662
+
663
+ const container = document.createElement('div');
664
+ container.innerHTML = clean;
665
+
666
+ container.querySelectorAll('a').forEach((anchor) => {
667
+ anchor.setAttribute('target', '_blank');
668
+ anchor.setAttribute('rel', 'noopener noreferrer');
669
+ });
670
+
671
+ container.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {
672
+ checkbox.setAttribute('disabled', '');
673
+ checkbox.setAttribute('tabindex', '-1');
674
+ });
675
+
676
+ return container.innerHTML;
677
+ }
678
+
607
679
  render() {
608
680
  return renderRioAssist(this);
609
681
  }
package/src/playground.ts CHANGED
@@ -1,24 +1,25 @@
1
- import './main';
2
-
3
- const boot = () => {
4
- window.RioAssist?.init({
5
- title: 'Rio Insight',
6
- buttonLabel: 'Rio Insight',
7
- accentColor: '#c02267',
8
- rioToken: 'SEU_TOKEN_RIO_AQUI',
9
- suggestions: [
10
- 'Veículos com problemas',
11
- 'Valor das peças',
12
- 'Planos de manutenção',
13
- ],
14
- });
15
- };
16
-
17
- if (window.RioAssist) {
18
- boot();
19
- } else {
20
- window.addEventListener('rio-assist-ready', boot, { once: true });
21
- }
22
-
23
-
24
-
1
+ import './main';
2
+
3
+ const rioToken = import.meta.env.VITE_RIO_TOKEN || 'SEU_TOKEN_RIO_AQUI';
4
+ const apiBaseUrl = import.meta.env.VITE_RIO_API_BASE_URL || '';
5
+
6
+ const boot = () => {
7
+ window.RioAssist?.init({
8
+ title: 'Rio Insight',
9
+ buttonLabel: 'Rio Insight',
10
+ accentColor: '#c02267',
11
+ rioToken,
12
+ apiBaseUrl,
13
+ suggestions: [
14
+ 'Veículos com problemas',
15
+ 'Valor das peças',
16
+ 'Planos de manutenção',
17
+ ],
18
+ });
19
+ };
20
+
21
+ if (window.RioAssist) {
22
+ boot();
23
+ } else {
24
+ window.addEventListener('rio-assist-ready', boot, { once: true });
25
+ }
@@ -0,0 +1 @@
1
+ declare module 'markdown-it-task-lists';