rio-assist-widget 0.1.2 → 0.1.5
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 +4 -4
- package/dist/rio-assist.js +1213 -1087
- 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
package/src/main.ts
CHANGED
|
@@ -1,72 +1,74 @@
|
|
|
1
|
-
import './components/rio-assist';
|
|
2
|
-
|
|
3
|
-
export type RioAssistOptions = {
|
|
4
|
-
target?: HTMLElement;
|
|
5
|
-
title?: string;
|
|
6
|
-
buttonLabel?: string;
|
|
7
|
-
placeholder?: string;
|
|
8
|
-
suggestions?: string[];
|
|
9
|
-
accentColor?: string;
|
|
10
|
-
apiBaseUrl?: string;
|
|
11
|
-
rioToken?: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const DEFAULT_OPTIONS: Required<Omit<RioAssistOptions, 'target'>> = {
|
|
15
|
-
title: '
|
|
16
|
-
buttonLabel: '
|
|
17
|
-
placeholder: 'Pergunte alguma coisa',
|
|
18
|
-
suggestions: [
|
|
19
|
-
'Veículos com problemas',
|
|
20
|
-
'Valor das peças',
|
|
21
|
-
'Planos de manutenção',
|
|
22
|
-
],
|
|
23
|
-
accentColor: '#008B9A',
|
|
24
|
-
apiBaseUrl: '',
|
|
25
|
-
rioToken: '',
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const widgetTagName = 'rio-assist-widget';
|
|
29
|
-
|
|
30
|
-
function ensureElement(options: RioAssistOptions = {}) {
|
|
31
|
-
const {
|
|
32
|
-
target = document.body,
|
|
33
|
-
...rest
|
|
34
|
-
} = options;
|
|
35
|
-
|
|
36
|
-
let widget = document.querySelector(widgetTagName) as HTMLElement | null;
|
|
37
|
-
|
|
38
|
-
if (!widget) {
|
|
39
|
-
widget = document.createElement(widgetTagName);
|
|
40
|
-
target.appendChild(widget);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const mergedOptions = { ...DEFAULT_OPTIONS, ...rest };
|
|
44
|
-
|
|
45
|
-
Object.entries(mergedOptions).forEach(([key, value]) => {
|
|
46
|
-
if (value === undefined) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
widget?.setAttribute(
|
|
51
|
-
`data-${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`,
|
|
52
|
-
Array.isArray(value) ? value.join('|') : String(value),
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
declare global {
|
|
58
|
-
interface Window {
|
|
59
|
-
RioAssist?: {
|
|
60
|
-
init: (options?: RioAssistOptions) => void;
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (typeof window !== 'undefined') {
|
|
66
|
-
window.RioAssist = window.RioAssist ?? {
|
|
67
|
-
init: ensureElement,
|
|
68
|
-
};
|
|
69
|
-
window.dispatchEvent(new Event('rio-assist-ready'));
|
|
70
|
-
}
|
|
1
|
+
import './components/rio-assist';
|
|
2
|
+
|
|
3
|
+
export type RioAssistOptions = {
|
|
4
|
+
target?: HTMLElement;
|
|
5
|
+
title?: string;
|
|
6
|
+
buttonLabel?: string;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
suggestions?: string[];
|
|
9
|
+
accentColor?: string;
|
|
10
|
+
apiBaseUrl?: string;
|
|
11
|
+
rioToken?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DEFAULT_OPTIONS: Required<Omit<RioAssistOptions, 'target'>> = {
|
|
15
|
+
title: 'Rio Insight',
|
|
16
|
+
buttonLabel: 'Rio Insight',
|
|
17
|
+
placeholder: 'Pergunte alguma coisa',
|
|
18
|
+
suggestions: [
|
|
19
|
+
'Veículos com problemas',
|
|
20
|
+
'Valor das peças',
|
|
21
|
+
'Planos de manutenção',
|
|
22
|
+
],
|
|
23
|
+
accentColor: '#008B9A',
|
|
24
|
+
apiBaseUrl: '',
|
|
25
|
+
rioToken: '',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const widgetTagName = 'rio-assist-widget';
|
|
29
|
+
|
|
30
|
+
function ensureElement(options: RioAssistOptions = {}) {
|
|
31
|
+
const {
|
|
32
|
+
target = document.body,
|
|
33
|
+
...rest
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
let widget = document.querySelector(widgetTagName) as HTMLElement | null;
|
|
37
|
+
|
|
38
|
+
if (!widget) {
|
|
39
|
+
widget = document.createElement(widgetTagName);
|
|
40
|
+
target.appendChild(widget);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...rest };
|
|
44
|
+
|
|
45
|
+
Object.entries(mergedOptions).forEach(([key, value]) => {
|
|
46
|
+
if (value === undefined) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
widget?.setAttribute(
|
|
51
|
+
`data-${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`,
|
|
52
|
+
Array.isArray(value) ? value.join('|') : String(value),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare global {
|
|
58
|
+
interface Window {
|
|
59
|
+
RioAssist?: {
|
|
60
|
+
init: (options?: RioAssistOptions) => void;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof window !== 'undefined') {
|
|
66
|
+
window.RioAssist = window.RioAssist ?? {
|
|
67
|
+
init: ensureElement,
|
|
68
|
+
};
|
|
69
|
+
window.dispatchEvent(new Event('rio-assist-ready'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
71
73
|
|
|
72
74
|
|
package/src/playground.ts
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import './main';
|
|
2
|
-
|
|
3
|
-
const boot = () => {
|
|
4
|
-
window.RioAssist?.init({
|
|
5
|
-
title: '
|
|
6
|
-
buttonLabel: '
|
|
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
|
-
}
|
|
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
|
+
|
|
22
23
|
|
|
23
24
|
|
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
const WEBSOCKET_URL = 'wss://ws.volkswagen.latam-sandbox.rio.cloud';
|
|
2
|
-
const DEFAULT_AGENT_MODEL = 'eu.amazon.nova-pro-v1:0';
|
|
3
|
-
|
|
4
|
-
export type RioIncomingMessage = {
|
|
5
|
-
text: string;
|
|
6
|
-
raw: string;
|
|
7
|
-
data: unknown;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export class RioWebsocketClient {
|
|
11
|
-
readonly token: string;
|
|
12
|
-
|
|
13
|
-
private socket: WebSocket | null = null;
|
|
14
|
-
|
|
15
|
-
private connectPromise: Promise<void> | null = null;
|
|
16
|
-
|
|
17
|
-
private readonly listeners = new Set<(message: RioIncomingMessage) => void>();
|
|
18
|
-
|
|
19
|
-
constructor(token: string) {
|
|
20
|
-
this.token = token;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
matchesToken(value: string) {
|
|
24
|
-
return this.token === value;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async sendMessage(message: string) {
|
|
28
|
-
const socket = await this.ensureConnection();
|
|
29
|
-
|
|
30
|
-
const payload = {
|
|
31
|
-
action: 'sendMessage',
|
|
32
|
-
message,
|
|
33
|
-
agentModel: DEFAULT_AGENT_MODEL,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
socket.send(JSON.stringify(payload));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
onMessage(listener: (message: RioIncomingMessage) => void) {
|
|
40
|
-
this.listeners.add(listener);
|
|
41
|
-
return () => this.listeners.delete(listener);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
close() {
|
|
45
|
-
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
46
|
-
this.socket.close();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
this.connectPromise = null;
|
|
50
|
-
this.socket = null;
|
|
51
|
-
this.listeners.clear();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private async ensureConnection(): Promise<WebSocket> {
|
|
55
|
-
if (
|
|
56
|
-
this.socket &&
|
|
57
|
-
(this.socket.readyState === WebSocket.OPEN ||
|
|
58
|
-
this.socket.readyState === WebSocket.CONNECTING)
|
|
59
|
-
) {
|
|
60
|
-
await this.connectPromise;
|
|
61
|
-
return this.socket;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
this.socket = new WebSocket(
|
|
65
|
-
`${WEBSOCKET_URL}?token=${encodeURIComponent(this.token)}`,
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
this.socket.addEventListener('message', (event) => this.handleMessage(event));
|
|
69
|
-
this.socket.addEventListener('close', () => {
|
|
70
|
-
this.connectPromise = null;
|
|
71
|
-
this.socket = null;
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
this.connectPromise = new Promise((resolve, reject) => {
|
|
75
|
-
if (!this.socket) {
|
|
76
|
-
reject(new Error('Falha ao criar conexão WebSocket.'));
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const handleOpen = () => {
|
|
81
|
-
cleanup();
|
|
82
|
-
resolve();
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const handleError = () => {
|
|
86
|
-
cleanup();
|
|
87
|
-
this.socket?.close();
|
|
88
|
-
this.socket = null;
|
|
89
|
-
this.connectPromise = null;
|
|
90
|
-
reject(
|
|
91
|
-
new Error(
|
|
92
|
-
'Não foi possível abrir conexão com o websocket do
|
|
93
|
-
),
|
|
94
|
-
);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const cleanup = () => {
|
|
98
|
-
this.socket?.removeEventListener('open', handleOpen);
|
|
99
|
-
this.socket?.removeEventListener('error', handleError);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
this.socket.addEventListener('open', handleOpen, { once: true });
|
|
103
|
-
this.socket.addEventListener('error', handleError, { once: true });
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
await this.connectPromise;
|
|
107
|
-
|
|
108
|
-
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
109
|
-
throw new Error('Conexão WebSocket do
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return this.socket;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private async handleMessage(event: MessageEvent) {
|
|
116
|
-
const raw = await this.readMessage(event.data);
|
|
117
|
-
let parsed: unknown = null;
|
|
118
|
-
let text = raw;
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
parsed = JSON.parse(raw);
|
|
122
|
-
if (typeof parsed === 'object' && parsed !== null) {
|
|
123
|
-
const maybeText =
|
|
124
|
-
(parsed as any).message ??
|
|
125
|
-
(parsed as any).response ??
|
|
126
|
-
(parsed as any).text ??
|
|
127
|
-
(parsed as any).content;
|
|
128
|
-
|
|
129
|
-
if (typeof maybeText === 'string') {
|
|
130
|
-
text = maybeText;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
} catch {
|
|
134
|
-
parsed = null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
this.listeners.forEach((listener) => listener({ text, raw, data: parsed }));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private async readMessage(
|
|
141
|
-
data: MessageEvent['data'],
|
|
142
|
-
): Promise<string> {
|
|
143
|
-
if (typeof data === 'string') {
|
|
144
|
-
return data;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (data instanceof Blob) {
|
|
148
|
-
return data.text();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (data instanceof ArrayBuffer) {
|
|
152
|
-
return new TextDecoder().decode(new Uint8Array(data));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (ArrayBuffer.isView(data)) {
|
|
156
|
-
return new TextDecoder().decode(
|
|
157
|
-
new Uint8Array(
|
|
158
|
-
data.buffer,
|
|
159
|
-
data.byteOffset,
|
|
160
|
-
data.byteLength,
|
|
161
|
-
),
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return String(data ?? '');
|
|
166
|
-
}
|
|
167
|
-
}
|
|
1
|
+
const WEBSOCKET_URL = 'wss://ws.volkswagen.latam-sandbox.rio.cloud';
|
|
2
|
+
const DEFAULT_AGENT_MODEL = 'eu.amazon.nova-pro-v1:0';
|
|
3
|
+
|
|
4
|
+
export type RioIncomingMessage = {
|
|
5
|
+
text: string;
|
|
6
|
+
raw: string;
|
|
7
|
+
data: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class RioWebsocketClient {
|
|
11
|
+
readonly token: string;
|
|
12
|
+
|
|
13
|
+
private socket: WebSocket | null = null;
|
|
14
|
+
|
|
15
|
+
private connectPromise: Promise<void> | null = null;
|
|
16
|
+
|
|
17
|
+
private readonly listeners = new Set<(message: RioIncomingMessage) => void>();
|
|
18
|
+
|
|
19
|
+
constructor(token: string) {
|
|
20
|
+
this.token = token;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
matchesToken(value: string) {
|
|
24
|
+
return this.token === value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async sendMessage(message: string) {
|
|
28
|
+
const socket = await this.ensureConnection();
|
|
29
|
+
|
|
30
|
+
const payload = {
|
|
31
|
+
action: 'sendMessage',
|
|
32
|
+
message,
|
|
33
|
+
agentModel: DEFAULT_AGENT_MODEL,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
socket.send(JSON.stringify(payload));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onMessage(listener: (message: RioIncomingMessage) => void) {
|
|
40
|
+
this.listeners.add(listener);
|
|
41
|
+
return () => this.listeners.delete(listener);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
close() {
|
|
45
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
46
|
+
this.socket.close();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.connectPromise = null;
|
|
50
|
+
this.socket = null;
|
|
51
|
+
this.listeners.clear();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async ensureConnection(): Promise<WebSocket> {
|
|
55
|
+
if (
|
|
56
|
+
this.socket &&
|
|
57
|
+
(this.socket.readyState === WebSocket.OPEN ||
|
|
58
|
+
this.socket.readyState === WebSocket.CONNECTING)
|
|
59
|
+
) {
|
|
60
|
+
await this.connectPromise;
|
|
61
|
+
return this.socket;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.socket = new WebSocket(
|
|
65
|
+
`${WEBSOCKET_URL}?token=${encodeURIComponent(this.token)}`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
this.socket.addEventListener('message', (event) => this.handleMessage(event));
|
|
69
|
+
this.socket.addEventListener('close', () => {
|
|
70
|
+
this.connectPromise = null;
|
|
71
|
+
this.socket = null;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.connectPromise = new Promise((resolve, reject) => {
|
|
75
|
+
if (!this.socket) {
|
|
76
|
+
reject(new Error('Falha ao criar conexão WebSocket.'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const handleOpen = () => {
|
|
81
|
+
cleanup();
|
|
82
|
+
resolve();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleError = () => {
|
|
86
|
+
cleanup();
|
|
87
|
+
this.socket?.close();
|
|
88
|
+
this.socket = null;
|
|
89
|
+
this.connectPromise = null;
|
|
90
|
+
reject(
|
|
91
|
+
new Error(
|
|
92
|
+
'Não foi possível abrir conexão com o websocket do Rio Insight.',
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const cleanup = () => {
|
|
98
|
+
this.socket?.removeEventListener('open', handleOpen);
|
|
99
|
+
this.socket?.removeEventListener('error', handleError);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
this.socket.addEventListener('open', handleOpen, { once: true });
|
|
103
|
+
this.socket.addEventListener('error', handleError, { once: true });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await this.connectPromise;
|
|
107
|
+
|
|
108
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
109
|
+
throw new Error('Conexão WebSocket do Rio Insight não está pronta.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return this.socket;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async handleMessage(event: MessageEvent) {
|
|
116
|
+
const raw = await this.readMessage(event.data);
|
|
117
|
+
let parsed: unknown = null;
|
|
118
|
+
let text = raw;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(raw);
|
|
122
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
123
|
+
const maybeText =
|
|
124
|
+
(parsed as any).message ??
|
|
125
|
+
(parsed as any).response ??
|
|
126
|
+
(parsed as any).text ??
|
|
127
|
+
(parsed as any).content;
|
|
128
|
+
|
|
129
|
+
if (typeof maybeText === 'string') {
|
|
130
|
+
text = maybeText;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
parsed = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.listeners.forEach((listener) => listener({ text, raw, data: parsed }));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async readMessage(
|
|
141
|
+
data: MessageEvent['data'],
|
|
142
|
+
): Promise<string> {
|
|
143
|
+
if (typeof data === 'string') {
|
|
144
|
+
return data;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (data instanceof Blob) {
|
|
148
|
+
return data.text();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (data instanceof ArrayBuffer) {
|
|
152
|
+
return new TextDecoder().decode(new Uint8Array(data));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (ArrayBuffer.isView(data)) {
|
|
156
|
+
return new TextDecoder().decode(
|
|
157
|
+
new Uint8Array(
|
|
158
|
+
data.buffer,
|
|
159
|
+
data.byteOffset,
|
|
160
|
+
data.byteLength,
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return String(data ?? '');
|
|
166
|
+
}
|
|
167
|
+
}
|