reachat 2.1.0-alpha.20 → 2.1.0-alpha.22
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/dist/{CSVFileRenderer-B7eSDub6.js → CSVFileRenderer-DC2ZhtNx.js} +2 -2
- package/dist/{CSVFileRenderer-B7eSDub6.js.map → CSVFileRenderer-DC2ZhtNx.js.map} +1 -1
- package/dist/ChatInput/ChatInput.d.ts +9 -1
- package/dist/ChatInput/FileDropzone.d.ts +33 -0
- package/dist/ChatInput/FileInput.d.ts +0 -4
- package/dist/ChatInput/index.d.ts +1 -0
- package/dist/ChatSuggestions/ChatSuggestion.d.ts +9 -0
- package/dist/ChatSuggestions/ChatSuggestions.d.ts +22 -0
- package/dist/ChatSuggestions/index.d.ts +2 -0
- package/dist/{DefaultFileRenderer-CszY8p_0.js → DefaultFileRenderer-3oHDLIk4.js} +2 -2
- package/dist/{DefaultFileRenderer-CszY8p_0.js.map → DefaultFileRenderer-3oHDLIk4.js.map} +1 -1
- package/dist/MessageStatus/MessageStatus.d.ts +44 -0
- package/dist/MessageStatus/MessageStatusItem.d.ts +9 -0
- package/dist/MessageStatus/StatusIcon.d.ts +17 -0
- package/dist/MessageStatus/index.d.ts +3 -0
- package/dist/SessionMessages/SessionMessages.d.ts +4 -0
- package/dist/docs.json +555 -16
- package/dist/{index-DNefh8rs.js → index-CWCW-OiG.js} +477 -124
- package/dist/index-CWCW-OiG.js.map +1 -0
- package/dist/index.css +186 -55
- package/dist/index.d.ts +2 -0
- package/dist/index.js +30 -24
- package/dist/index.umd.cjs +450 -99
- package/dist/index.umd.cjs.map +1 -1
- package/dist/stories/ChatSuggestions.stories.tsx +542 -0
- package/dist/stories/Console.stories.tsx +101 -623
- package/dist/stories/Files.stories.tsx +348 -0
- package/dist/stories/Markdown.stories.tsx +108 -1
- package/dist/stories/MessageStatus.stories.tsx +326 -0
- package/dist/stories/SessionsList.stories.tsx +276 -0
- package/dist/stories/assets/sparkles.svg +7 -0
- package/dist/stories/examples.ts +34 -0
- package/dist/theme.d.ts +46 -0
- package/dist/types.d.ts +10 -0
- package/package.json +7 -4
- package/dist/index-DNefh8rs.js.map +0 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import type { Meta } from '@storybook/react';
|
|
2
|
+
import { subHours } from 'date-fns';
|
|
3
|
+
import { cn } from 'reablocks';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import type { Session } from 'reachat';
|
|
7
|
+
import {
|
|
8
|
+
Chat,
|
|
9
|
+
ChatInput,
|
|
10
|
+
NewSessionButton,
|
|
11
|
+
SessionGroups,
|
|
12
|
+
SessionMessagePanel,
|
|
13
|
+
SessionMessages,
|
|
14
|
+
SessionMessagesHeader,
|
|
15
|
+
SessionsList
|
|
16
|
+
} from 'reachat';
|
|
17
|
+
import UploadIcon from 'reachat';
|
|
18
|
+
import AttachIcon from './assets/paperclip.svg?react';
|
|
19
|
+
import { sessionsWithFiles, sessionWithCSVFiles } from './examples';
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
title: 'Demos/Files',
|
|
23
|
+
component: Chat
|
|
24
|
+
} as Meta;
|
|
25
|
+
|
|
26
|
+
export const FileUploads = () => {
|
|
27
|
+
const [sessions, setSessions] = useState(sessionsWithFiles);
|
|
28
|
+
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
className="dark:bg-(--color-background-basic-black) bg-(--color-background-basic-white)"
|
|
33
|
+
style={{
|
|
34
|
+
position: 'absolute',
|
|
35
|
+
top: 0,
|
|
36
|
+
left: 0,
|
|
37
|
+
right: 0,
|
|
38
|
+
bottom: 0,
|
|
39
|
+
padding: 20,
|
|
40
|
+
margin: 20,
|
|
41
|
+
borderRadius: 5
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<Chat
|
|
45
|
+
viewType="console"
|
|
46
|
+
sessions={sessions}
|
|
47
|
+
activeSessionId="session-files"
|
|
48
|
+
onSendMessage={message =>
|
|
49
|
+
setSessions(sessions => {
|
|
50
|
+
const session = sessions[0];
|
|
51
|
+
|
|
52
|
+
setSelectedFile(null);
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
...session,
|
|
57
|
+
conversations: [
|
|
58
|
+
...session.conversations,
|
|
59
|
+
{
|
|
60
|
+
id: (Math.random() * 100).toString(),
|
|
61
|
+
createdAt: new Date(),
|
|
62
|
+
question: message,
|
|
63
|
+
...(selectedFile
|
|
64
|
+
? {
|
|
65
|
+
files: [
|
|
66
|
+
{
|
|
67
|
+
name: selectedFile.name,
|
|
68
|
+
size: selectedFile.size,
|
|
69
|
+
type: selectedFile.type
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
: [])
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
onFileUpload={setSelectedFile}
|
|
81
|
+
onDeleteSession={() => alert('delete!')}
|
|
82
|
+
>
|
|
83
|
+
<SessionsList>
|
|
84
|
+
<NewSessionButton />
|
|
85
|
+
<SessionGroups />
|
|
86
|
+
</SessionsList>
|
|
87
|
+
<SessionMessagePanel>
|
|
88
|
+
<SessionMessagesHeader />
|
|
89
|
+
<SessionMessages />
|
|
90
|
+
<ChatInput
|
|
91
|
+
attachIcon={
|
|
92
|
+
<AttachIcon className={cn({ 'text-green-500': selectedFile })} />
|
|
93
|
+
}
|
|
94
|
+
allowedFiles={['.pdf', '.docx']}
|
|
95
|
+
/>
|
|
96
|
+
</SessionMessagePanel>
|
|
97
|
+
</Chat>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const DragAndDrop = () => {
|
|
103
|
+
const [sessions, setSessions] = useState<Session[]>([
|
|
104
|
+
{
|
|
105
|
+
id: 'session-dragdrop',
|
|
106
|
+
title: 'Drag and Drop Demo',
|
|
107
|
+
createdAt: subHours(new Date(), 1),
|
|
108
|
+
updatedAt: new Date(),
|
|
109
|
+
conversations: [
|
|
110
|
+
{
|
|
111
|
+
id: 'conv-1',
|
|
112
|
+
question: 'Try dragging and dropping files onto the input below!',
|
|
113
|
+
response:
|
|
114
|
+
'You can drag and drop files directly onto the chat input. When you start dragging a file over the input area, you will see a visual indicator showing where to drop. This works with any file type that is allowed by the chat input configuration.',
|
|
115
|
+
createdAt: new Date()
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
]);
|
|
120
|
+
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
|
|
121
|
+
|
|
122
|
+
const handleFileUpload = (file: File) => {
|
|
123
|
+
setUploadedFiles(prev => [...prev, file]);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
className="dark:bg-(--color-background-basic-black) bg-(--color-background-basic-white)"
|
|
129
|
+
style={{
|
|
130
|
+
position: 'absolute',
|
|
131
|
+
top: 0,
|
|
132
|
+
left: 0,
|
|
133
|
+
right: 0,
|
|
134
|
+
bottom: 0,
|
|
135
|
+
padding: 20,
|
|
136
|
+
margin: 20,
|
|
137
|
+
borderRadius: 5
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<Chat
|
|
141
|
+
viewType="console"
|
|
142
|
+
sessions={sessions}
|
|
143
|
+
activeSessionId="session-dragdrop"
|
|
144
|
+
onSendMessage={message =>
|
|
145
|
+
setSessions(sessions => {
|
|
146
|
+
const session = sessions[0];
|
|
147
|
+
const files = uploadedFiles.map(f => ({
|
|
148
|
+
name: f.name,
|
|
149
|
+
size: f.size,
|
|
150
|
+
type: f.type
|
|
151
|
+
}));
|
|
152
|
+
setUploadedFiles([]);
|
|
153
|
+
|
|
154
|
+
return [
|
|
155
|
+
{
|
|
156
|
+
...session,
|
|
157
|
+
conversations: [
|
|
158
|
+
...session.conversations,
|
|
159
|
+
{
|
|
160
|
+
id: (Math.random() * 100).toString(),
|
|
161
|
+
createdAt: new Date(),
|
|
162
|
+
question: message,
|
|
163
|
+
response: files.length
|
|
164
|
+
? `Received ${files.length} file(s): ${files.map(f => f.name).join(', ')}`
|
|
165
|
+
: 'Message received!',
|
|
166
|
+
...(files.length > 0 ? { files } : {})
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
];
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
onFileUpload={handleFileUpload}
|
|
174
|
+
onDeleteSession={() => alert('delete!')}
|
|
175
|
+
>
|
|
176
|
+
<SessionsList>
|
|
177
|
+
<NewSessionButton />
|
|
178
|
+
<SessionGroups />
|
|
179
|
+
</SessionsList>
|
|
180
|
+
<SessionMessagePanel>
|
|
181
|
+
<SessionMessagesHeader />
|
|
182
|
+
<SessionMessages />
|
|
183
|
+
<div className="flex flex-col gap-2">
|
|
184
|
+
{uploadedFiles.length > 0 && (
|
|
185
|
+
<div className="flex flex-wrap gap-2 px-2">
|
|
186
|
+
{uploadedFiles.map((file, idx) => (
|
|
187
|
+
<div
|
|
188
|
+
key={idx}
|
|
189
|
+
className="flex items-center gap-2 bg-gray-100 dark:bg-gray-800 px-3 py-1 rounded-full text-sm"
|
|
190
|
+
>
|
|
191
|
+
<span>{file.name}</span>
|
|
192
|
+
<button
|
|
193
|
+
onClick={() =>
|
|
194
|
+
setUploadedFiles(prev =>
|
|
195
|
+
prev.filter((_, i) => i !== idx)
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
|
199
|
+
>
|
|
200
|
+
×
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
<ChatInput
|
|
207
|
+
allowedFiles={[
|
|
208
|
+
'.pdf',
|
|
209
|
+
'.docx',
|
|
210
|
+
'.txt',
|
|
211
|
+
'.csv',
|
|
212
|
+
'.jpg',
|
|
213
|
+
'.jpeg',
|
|
214
|
+
'.png',
|
|
215
|
+
'.gif'
|
|
216
|
+
]}
|
|
217
|
+
allowMultipleFiles
|
|
218
|
+
dropIcon={<UploadIcon className="w-8 h-8" />}
|
|
219
|
+
dropText="Drop your files here"
|
|
220
|
+
placeholder="Type a message or drag files here..."
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
</SessionMessagePanel>
|
|
224
|
+
</Chat>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export const CSVPreview = () => {
|
|
230
|
+
return (
|
|
231
|
+
<div
|
|
232
|
+
className="dark:bg-(--color-background-basic-black) bg-(--color-background-basic-white)"
|
|
233
|
+
style={{
|
|
234
|
+
position: 'absolute',
|
|
235
|
+
inset: 0,
|
|
236
|
+
padding: 20,
|
|
237
|
+
margin: 20,
|
|
238
|
+
borderRadius: 5
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
<Chat
|
|
242
|
+
sessions={sessionWithCSVFiles}
|
|
243
|
+
activeSessionId="1"
|
|
244
|
+
onDeleteSession={() => alert('delete!')}
|
|
245
|
+
>
|
|
246
|
+
<SessionsList>
|
|
247
|
+
<NewSessionButton />
|
|
248
|
+
<SessionGroups />
|
|
249
|
+
</SessionsList>
|
|
250
|
+
|
|
251
|
+
<SessionMessagePanel>
|
|
252
|
+
<SessionMessagesHeader />
|
|
253
|
+
<SessionMessages />
|
|
254
|
+
<ChatInput allowedFiles={['.pdf', '.docx', '.csv']} />
|
|
255
|
+
</SessionMessagePanel>
|
|
256
|
+
</Chat>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export const ImageFiles = () => {
|
|
262
|
+
const staticImageFiles = [
|
|
263
|
+
{
|
|
264
|
+
id: '1',
|
|
265
|
+
name: 'landscape.jpg',
|
|
266
|
+
type: 'image/jpeg',
|
|
267
|
+
url: 'https://picsum.photos/200?random=1'
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: '2',
|
|
271
|
+
name: 'portrait.jpg',
|
|
272
|
+
type: 'image/jpeg',
|
|
273
|
+
url: 'https://picsum.photos/200?random=2'
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: '3',
|
|
277
|
+
name: 'abstract.png',
|
|
278
|
+
type: 'image/jpg',
|
|
279
|
+
url: 'https://picsum.photos/200?random=3'
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: '4',
|
|
283
|
+
name: 'nature.jpg',
|
|
284
|
+
type: 'image/jpeg',
|
|
285
|
+
url: 'https://picsum.photos/200?random=4'
|
|
286
|
+
}
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
const sessionWithImages: Session[] = [
|
|
290
|
+
{
|
|
291
|
+
id: 'session-images',
|
|
292
|
+
title: 'Multiple Image Files Showcase',
|
|
293
|
+
createdAt: subHours(new Date(), 1),
|
|
294
|
+
updatedAt: new Date(),
|
|
295
|
+
conversations: [
|
|
296
|
+
{
|
|
297
|
+
id: 'conversation-1',
|
|
298
|
+
question: 'Analyze these images and describe what you see.',
|
|
299
|
+
response:
|
|
300
|
+
"I'm sorry, but as an AI language model, I cannot actually see or analyze images. I can only process and respond to text input. If you'd like me to describe or analyze images, you would need to provide detailed textual descriptions of the images.",
|
|
301
|
+
createdAt: new Date(),
|
|
302
|
+
files: staticImageFiles
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: 'conversation-2',
|
|
306
|
+
question: 'Analyze these images and describe what you see.',
|
|
307
|
+
response:
|
|
308
|
+
"I'm sorry, but as an AI language model, I cannot actually see or analyze images. I can only process and respond to text input. If you'd like me to describe or analyze images, you would need to provide detailed textual descriptions of the images.",
|
|
309
|
+
createdAt: new Date(),
|
|
310
|
+
files: [staticImageFiles[0]]
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
}
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<div
|
|
318
|
+
className="dark:bg-(--color-background-basic-black) bg-(--color-background-basic-white)"
|
|
319
|
+
style={{
|
|
320
|
+
position: 'absolute',
|
|
321
|
+
top: 0,
|
|
322
|
+
left: 0,
|
|
323
|
+
right: 0,
|
|
324
|
+
bottom: 0,
|
|
325
|
+
padding: 20,
|
|
326
|
+
margin: 20,
|
|
327
|
+
borderRadius: 5
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
<Chat
|
|
331
|
+
viewType="console"
|
|
332
|
+
sessions={sessionWithImages}
|
|
333
|
+
activeSessionId="session-images"
|
|
334
|
+
>
|
|
335
|
+
<SessionsList>
|
|
336
|
+
<NewSessionButton />
|
|
337
|
+
<SessionGroups />
|
|
338
|
+
</SessionsList>
|
|
339
|
+
|
|
340
|
+
<SessionMessagePanel>
|
|
341
|
+
<SessionMessagesHeader />
|
|
342
|
+
<SessionMessages />
|
|
343
|
+
<ChatInput />
|
|
344
|
+
</SessionMessagePanel>
|
|
345
|
+
</Chat>
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
};
|
|
@@ -34,7 +34,7 @@ This risk table uses Dragos ransomware intelligence trends along with Acme Contr
|
|
|
34
34
|
|
|
35
35
|
Let me know your next question or if you want me to elaborate on specific risks before we proceed to complete the Strategic Risk Assessment.`;
|
|
36
36
|
|
|
37
|
-
export const
|
|
37
|
+
export const ComplexTable: Story = {
|
|
38
38
|
render: args => (
|
|
39
39
|
<div className="p-8 bg-background-neutral-canvas-base max-w-full overflow-x-auto">
|
|
40
40
|
<Markdown {...args} remarkPlugins={[remarkGfm, remarkMath, remarkCve]}>
|
|
@@ -316,3 +316,110 @@ function greet(name: string): string {
|
|
|
316
316
|
</div>
|
|
317
317
|
)
|
|
318
318
|
};
|
|
319
|
+
|
|
320
|
+
export const MarkdownShowcase: Story = {
|
|
321
|
+
render: args => (
|
|
322
|
+
<div className="p-8 bg-background-neutral-canvas-base max-w-4xl">
|
|
323
|
+
<Markdown {...args}>
|
|
324
|
+
{`
|
|
325
|
+
**The purpose of life is a philosophical question concerning the significance of life or existence in general.**
|
|
326
|
+
|
|
327
|
+
1. Burning of fossil fuels (coal, oil, and natural gas)
|
|
328
|
+
2. Deforestation and land-use changes
|
|
329
|
+
3. Industrial processes
|
|
330
|
+
4. Agriculture and livestock farming
|
|
331
|
+
|
|
332
|
+
or
|
|
333
|
+
|
|
334
|
+
- Burning
|
|
335
|
+
- Deforestation
|
|
336
|
+
- Industrial
|
|
337
|
+
- Agriculture
|
|
338
|
+
|
|
339
|
+
Here is a table to illustrate different perspectives:
|
|
340
|
+
|
|
341
|
+
| Perspective | Description |
|
|
342
|
+
|-------------------|-----------------------------------------------------------------------------|
|
|
343
|
+
| Religious | Belief in a higher power or divine purpose. |
|
|
344
|
+
| Philosophical | Various theories including existentialism, nihilism, and absurdism. |
|
|
345
|
+
| Scientific | Understanding life through biology, evolution, and the universe. |
|
|
346
|
+
| Personal | Individual goals, happiness, and fulfillment. |
|
|
347
|
+
|
|
348
|
+
\`\`\`python
|
|
349
|
+
def purpose_of_life():
|
|
350
|
+
return 42
|
|
351
|
+
\`\`\`
|
|
352
|
+
|
|
353
|
+
\`\`\`json
|
|
354
|
+
{
|
|
355
|
+
"perspectives": [
|
|
356
|
+
{
|
|
357
|
+
"type": "Religious",
|
|
358
|
+
"description": "Belief in a higher power or divine purpose."
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"type": "Philosophical",
|
|
362
|
+
"description": "Various theories including existentialism, nihilism, and absurdism."
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"type": "Scientific",
|
|
366
|
+
"description": "Understanding life through biology, evolution, and the universe."
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"type": "Personal",
|
|
370
|
+
"description": "Individual goals, happiness, and fulfillment."
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
}
|
|
374
|
+
\`\`\`
|
|
375
|
+
|
|
376
|
+
The answer to the ultimate question of life, the universe, and everything is **42**.
|
|
377
|
+
|
|
378
|
+
\`\`\`math
|
|
379
|
+
L = \\frac{1}{2} \\rho v^2 S C_L
|
|
380
|
+
\`\`\`
|
|
381
|
+
|
|
382
|
+
[Perspective](https://en.wikipedia.org/wiki/Philosophical_question)
|
|
383
|
+
`}
|
|
384
|
+
</Markdown>
|
|
385
|
+
</div>
|
|
386
|
+
)
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export const CVEExample: Story = {
|
|
390
|
+
render: args => (
|
|
391
|
+
<div className="p-8 bg-background-neutral-canvas-base max-w-4xl">
|
|
392
|
+
<Markdown {...args} remarkPlugins={[remarkGfm, remarkMath, remarkCve]}>
|
|
393
|
+
{`
|
|
394
|
+
## Analysis
|
|
395
|
+
|
|
396
|
+
The listed CVEs are critical vulnerabilities that need immediate attention.
|
|
397
|
+
|
|
398
|
+
- CVE-2021-34527
|
|
399
|
+
- [CVE-2021-44228](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228) < Has link
|
|
400
|
+
- CVE-2021-45046
|
|
401
|
+
|
|
402
|
+
The \`remarkCve\` plugin automatically converts CVE identifiers into clickable links.
|
|
403
|
+
`}
|
|
404
|
+
</Markdown>
|
|
405
|
+
</div>
|
|
406
|
+
)
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
export const Embeds: Story = {
|
|
410
|
+
render: args => (
|
|
411
|
+
<div className="p-8 bg-background-neutral-canvas-base max-w-4xl">
|
|
412
|
+
<Markdown {...args}>
|
|
413
|
+
{`
|
|
414
|
+
## Watch this video
|
|
415
|
+
|
|
416
|
+
https://youtu.be/enTFE2c68FQ
|
|
417
|
+
|
|
418
|
+
https://www.youtube.com/watch?v=enTFE2c68FQ
|
|
419
|
+
|
|
420
|
+
These links showcase a video about React basics. You can click on either link to watch the video.
|
|
421
|
+
`}
|
|
422
|
+
</Markdown>
|
|
423
|
+
</div>
|
|
424
|
+
)
|
|
425
|
+
};
|