@katechat/ui 1.0.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 (47) hide show
  1. package/.prettierrc +9 -0
  2. package/esbuild.js +56 -0
  3. package/jest.config.js +24 -0
  4. package/package.json +75 -0
  5. package/postcss.config.cjs +14 -0
  6. package/src/__mocks__/fileMock.js +1 -0
  7. package/src/__mocks__/styleMock.js +1 -0
  8. package/src/components/chat/ChatMessagesContainer.module.scss +77 -0
  9. package/src/components/chat/ChatMessagesContainer.tsx +151 -0
  10. package/src/components/chat/ChatMessagesList.tsx +216 -0
  11. package/src/components/chat/index.ts +4 -0
  12. package/src/components/chat/input/ChatInput.module.scss +113 -0
  13. package/src/components/chat/input/ChatInput.tsx +259 -0
  14. package/src/components/chat/input/index.ts +1 -0
  15. package/src/components/chat/message/ChatMessage.Carousel.module.scss +7 -0
  16. package/src/components/chat/message/ChatMessage.module.scss +378 -0
  17. package/src/components/chat/message/ChatMessage.tsx +271 -0
  18. package/src/components/chat/message/ChatMessagePreview.tsx +22 -0
  19. package/src/components/chat/message/LinkedChatMessage.tsx +64 -0
  20. package/src/components/chat/message/MessageStatus.tsx +38 -0
  21. package/src/components/chat/message/controls/CopyMessageButton.tsx +32 -0
  22. package/src/components/chat/message/index.ts +4 -0
  23. package/src/components/icons/ProviderIcon.tsx +49 -0
  24. package/src/components/icons/index.ts +1 -0
  25. package/src/components/index.ts +3 -0
  26. package/src/components/modal/ImagePopup.tsx +97 -0
  27. package/src/components/modal/index.ts +1 -0
  28. package/src/controls/FileDropzone/FileDropzone.module.scss +15 -0
  29. package/src/controls/FileDropzone/FileDropzone.tsx +120 -0
  30. package/src/controls/index.ts +1 -0
  31. package/src/core/ai.ts +1 -0
  32. package/src/core/index.ts +4 -0
  33. package/src/core/message.ts +59 -0
  34. package/src/core/model.ts +23 -0
  35. package/src/core/user.ts +8 -0
  36. package/src/hooks/index.ts +2 -0
  37. package/src/hooks/useIntersectionObserver.ts +24 -0
  38. package/src/hooks/useTheme.tsx +66 -0
  39. package/src/index.ts +5 -0
  40. package/src/lib/__tests__/markdown.parser.test.ts +289 -0
  41. package/src/lib/__tests__/markdown.parser.testUtils.ts +31 -0
  42. package/src/lib/__tests__/markdown.parser_sanitizeUrl.test.ts +130 -0
  43. package/src/lib/assert.ts +14 -0
  44. package/src/lib/markdown.parser.ts +189 -0
  45. package/src/setupTests.ts +1 -0
  46. package/src/types/scss.d.ts +4 -0
  47. package/tsconfig.json +26 -0
@@ -0,0 +1,378 @@
1
+ .messageContainer {
2
+ width: 100%;
3
+ overflow-x: hidden;
4
+ display: flex;
5
+ flex-direction: row;
6
+
7
+ .main,
8
+ .linked {
9
+ padding: var(--mantine-spacing-md);
10
+ flex-grow: 1;
11
+ width: 50%;
12
+ display: flex;
13
+ flex-direction: column;
14
+ border-radius: var(--mantine-radius-md);
15
+ }
16
+
17
+ .linked {
18
+ margin: 0 var(--mantine-spacing-md);
19
+ }
20
+
21
+ .linkedToggle {
22
+ display: none;
23
+ }
24
+ }
25
+
26
+ @media (max-width: 1280px) {
27
+ .messageContainer {
28
+ flex-direction: column;
29
+ position: relative;
30
+
31
+ .linkedToggle {
32
+ position: absolute;
33
+ right: 0.5rem;
34
+ top: 0.5rem;
35
+ display: flex;
36
+ z-index: 5;
37
+ }
38
+
39
+ .main,
40
+ .linked {
41
+ width: 100%;
42
+ margin: 0;
43
+ border-radius: 0;
44
+ }
45
+
46
+ .hidden {
47
+ display: none;
48
+ }
49
+ }
50
+ }
51
+
52
+ .linkedMessageContainer {
53
+ width: 100%;
54
+ overflow-x: hidden;
55
+ display: flex;
56
+ flex-direction: column;
57
+ }
58
+
59
+ .message {
60
+ display: flex;
61
+ flex-direction: column;
62
+ align-items: stretch;
63
+ padding: var(--mb-padding, var(--mantine-spacing-sm));
64
+ border-radius: var(--mantine-radius-md);
65
+
66
+ margin-top: var(--mantine-spacing-xs);
67
+
68
+ :global p {
69
+ margin-block-start: 0;
70
+ margin-block-end: 0;
71
+ }
72
+
73
+ :global blockquote {
74
+ padding: 0 var(--mb-padding, var(--mantine-spacing-sm));
75
+ border-left: 0.5rem solid var(--mantine-color-default-border);
76
+ margin: 0.25rem 0;
77
+ }
78
+
79
+ :global hr {
80
+ width: 100%;
81
+ margin: 0.5rem 0 0.25rem;
82
+ border: none;
83
+ border-top: 1px solid var(--mantine-color-default-border);
84
+ }
85
+
86
+ :global img {
87
+ max-width: 256px;
88
+ max-height: 256px;
89
+ align-self: center;
90
+ height: auto;
91
+ margin: 0 auto;
92
+ cursor: pointer;
93
+ }
94
+
95
+ &.preview {
96
+ height: 200px;
97
+ padding: var(--mb-padding, var(--mantine-spacing-sm)) 0 0 0;
98
+ font-size: 80%;
99
+
100
+ :global img {
101
+ max-width: 50%;
102
+ height: auto;
103
+ }
104
+ }
105
+
106
+ .detailsBlock {
107
+ margin-top: 1rem;
108
+ font-size: 80%;
109
+ display: flex;
110
+ flex-direction: column;
111
+ gap: 0;
112
+
113
+ :global ol,
114
+ :global ul {
115
+ margin: 0.25rem 0;
116
+ padding: 0 0 0 1rem;
117
+ }
118
+
119
+ :global .message-details-header {
120
+ margin-bottom: var(--mantine-spacing-md);
121
+ margin-left: 1rem;
122
+
123
+ :global svg {
124
+ color: var(--mantine-color-gray-7);
125
+ }
126
+ }
127
+
128
+ :global .message-details-content {
129
+ padding-left: var(--mantine-spacing-md);
130
+ border-left: 1px solid var(--mantine-color-gray-7);
131
+ margin-left: 1.5rem;
132
+ }
133
+ }
134
+
135
+ :global table {
136
+ margin-top: var(--mantine-spacing-md);
137
+ margin-bottom: var(--mantine-spacing-md);
138
+ border-spacing: 0;
139
+
140
+ td,
141
+ th {
142
+ padding: var(--mb-padding, var(--mantine-spacing-xs));
143
+ margin: 0;
144
+ border-top: 1px solid var(--mantine-color-default-border);
145
+ border-left: 1px solid var(--mantine-color-default-border);
146
+ }
147
+
148
+ tr td:last-child,
149
+ tr th:last-child {
150
+ border-right: 1px solid var(--mantine-color-default-border);
151
+ }
152
+ tr:last-child td {
153
+ border-bottom: 1px solid var(--mantine-color-default-border);
154
+ }
155
+
156
+ tr:nth-child(2n) {
157
+ background-color: var(--mantine-color-dark-5);
158
+ }
159
+
160
+ thead {
161
+ tr {
162
+ background-color: var(--mantine-color-dark-5);
163
+ }
164
+
165
+ tr:first-child th:first-child {
166
+ border-top-left-radius: var(--mantine-radius-md);
167
+ }
168
+ tr:first-child th:last-child {
169
+ border-top-right-radius: var(--mantine-radius-md);
170
+ }
171
+ }
172
+
173
+ tr:last-child td:first-child {
174
+ border-bottom-left-radius: var(--mantine-radius-md);
175
+ }
176
+ tr:last-child td:last-child {
177
+ border-bottom-right-radius: var(--mantine-radius-md);
178
+ }
179
+ }
180
+
181
+ :global pre {
182
+ margin: 0.5rem 0;
183
+ padding: 1em;
184
+ clip-path: none;
185
+ font-size: 90%;
186
+ transition: all var(--theme-default-time) ease;
187
+ font-family: var(--monospace-font-family);
188
+ overflow-x: auto;
189
+ }
190
+
191
+ :global .code-header {
192
+ font-size: 0.875rem;
193
+ margin-top: 0.5rem;
194
+ padding: 0.25rem 1rem 0.25rem 0.5rem;
195
+ border-top-right-radius: 0.25em;
196
+ border-top-left-radius: 0.25em;
197
+
198
+ display: flex;
199
+ justify-content: space-between;
200
+ align-items: center;
201
+ flex-direction: row;
202
+ white-space: normal;
203
+ gap: 0.5rem;
204
+
205
+ :global .title {
206
+ width: fit-content;
207
+ font-weight: 700;
208
+ display: flex;
209
+ align-items: center;
210
+ flex-direction: row;
211
+ cursor: pointer;
212
+ user-select: none;
213
+ min-height: 2rem;
214
+
215
+ :global .header-toggle {
216
+ margin-right: 0.5rem;
217
+ display: none;
218
+ display: inline-block;
219
+ transform: rotateZ(90deg);
220
+ transition: transform var(--theme-default-time) ease;
221
+ }
222
+ }
223
+
224
+ :global .code-header-actions {
225
+ align-content: flex-end;
226
+ }
227
+
228
+ :global .action-btn {
229
+ padding: 0.1rem;
230
+ line-height: 1;
231
+ display: flex;
232
+ justify-content: center;
233
+ align-items: center;
234
+ width: fit-content;
235
+ gap: 0.25rem;
236
+ cursor: pointer;
237
+ }
238
+
239
+ &.collapsed {
240
+ border-bottom-right-radius: 0.25em;
241
+ border-bottom-left-radius: 0.25em;
242
+ margin-bottom: 0;
243
+ :global .title {
244
+ :global .header-toggle {
245
+ transform: translateY(0.2rem) rotateZ(0deg);
246
+ }
247
+ }
248
+
249
+ .code-copy-btn {
250
+ display: none;
251
+ }
252
+ }
253
+ }
254
+
255
+ :global .code-block {
256
+ :global pre {
257
+ margin: 0;
258
+ white-space: pre;
259
+ }
260
+
261
+ flex-grow: 1;
262
+ overflow-x: auto;
263
+ margin-top: 0;
264
+ margin-bottom: 1rem;
265
+ border-bottom-right-radius: 0.25em;
266
+ border-bottom-left-radius: 0.25em;
267
+ transition:
268
+ clip-path 0.5s ease,
269
+ height 0.5s ease,
270
+ opacity 0.5s ease;
271
+
272
+ &.collapsed {
273
+ clip-path: inset(0 0 100% 0);
274
+ overflow-y: hidden;
275
+ height: 0;
276
+ opacity: 0;
277
+ }
278
+ }
279
+
280
+ :global .code-data {
281
+ display: none;
282
+ }
283
+
284
+ :global .code-footer {
285
+ font-size: 90%;
286
+ display: flex;
287
+ align-items: flex-start;
288
+ margin-bottom: 0.7rem;
289
+
290
+ &.collapsed {
291
+ display: none;
292
+ }
293
+ }
294
+
295
+ :global .katex-html {
296
+ display: inline-block;
297
+ margin: 0.5rem 0.5rem 0 0;
298
+ }
299
+ }
300
+
301
+ .messageFooter {
302
+ display: flex;
303
+ flex-direction: row;
304
+ align-items: center;
305
+ gap: 0.5rem;
306
+ padding: 0.5rem 0 0.25rem;
307
+ opacity: 0.5;
308
+ transition: opacity var(--theme-default-time) ease;
309
+
310
+ :global .check-icon {
311
+ display: none;
312
+ }
313
+
314
+ &:hover {
315
+ opacity: 1;
316
+ }
317
+ }
318
+
319
+ [data-mantine-color-scheme="dark"] {
320
+ .messageContainer {
321
+ :global .code-block,
322
+ :global pre {
323
+ background-color: var(--mantine-color-dark-6);
324
+ }
325
+
326
+ :global .code-header {
327
+ background-color: var(--mantine-color-dark-6);
328
+ &:hover {
329
+ background-color: var(--mantine-color-dark-5);
330
+ }
331
+ }
332
+
333
+ .message.user {
334
+ background-color: var(--mantine-color-dark-9);
335
+ }
336
+ .message.error {
337
+ color: var(--mantine-color-yellow-9);
338
+ }
339
+
340
+ .linked {
341
+ background-color: var(--mantine-color-dark-8);
342
+ }
343
+ }
344
+ }
345
+
346
+ [data-mantine-color-scheme="light"] {
347
+ .messageContainer {
348
+ :global .code-block,
349
+ :global pre {
350
+ background-color: var(--mantine-color-gray-3);
351
+ }
352
+
353
+ :global .code-header {
354
+ background-color: var(--mantine-color-gray-3);
355
+ &:hover {
356
+ background-color: var(--mantine-color-gray-4);
357
+ }
358
+ }
359
+
360
+ .message.user {
361
+ background-color: var(--mantine-color-blue-0);
362
+ }
363
+ .message.error {
364
+ color: var(--mantine-color-red-5);
365
+ }
366
+ .linked {
367
+ background-color: var(--mantine-color-gray-3);
368
+ }
369
+
370
+ tr:nth-child(2n) {
371
+ background-color: var(--mantine-color-blue-1);
372
+ }
373
+
374
+ thead tr {
375
+ background-color: var(--mantine-color-blue-1);
376
+ }
377
+ }
378
+ }
@@ -0,0 +1,271 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import { Text, Group, Avatar, Switch, Loader, Button, Collapse, Box } from "@mantine/core";
3
+ import { Carousel } from "@mantine/carousel";
4
+ import { IconRobot, IconUser } from "@tabler/icons-react";
5
+ import { MessageRole, Message } from "@/core/message";
6
+ import { LinkedChatMessage, MessageStatus } from "@/components/chat";
7
+ import { debounce } from "lodash";
8
+ import { ProviderIcon } from "@/components/icons/ProviderIcon";
9
+ import { Model } from "@/core";
10
+ import { CopyMessageButton } from "./controls/CopyMessageButton";
11
+
12
+ import classes from "./ChatMessage.module.scss";
13
+ import carouselClasses from "./ChatMessage.Carousel.module.scss";
14
+
15
+ interface ChatMessageProps {
16
+ message: Message;
17
+ index: number;
18
+ disabled?: boolean;
19
+ pluginsLoader?: (message: Message) => React.ReactNode;
20
+ messageDetailsLoader?: (message: Message) => React.ReactNode;
21
+ models?: Model[];
22
+ }
23
+
24
+ export const ChatMessage = (props: ChatMessageProps) => {
25
+ const { message, index, disabled = false, pluginsLoader, messageDetailsLoader, models } = props;
26
+
27
+ const {
28
+ role,
29
+ id,
30
+ modelName,
31
+ modelId,
32
+ content,
33
+ html,
34
+ updatedAt,
35
+ user,
36
+ streaming = false,
37
+ linkedMessages,
38
+ status,
39
+ statusInfo,
40
+ } = message;
41
+
42
+ const componentRef = useRef<HTMLDivElement>(null);
43
+ const disableActions = useMemo(() => disabled || streaming, [disabled, streaming]);
44
+ const [showMainMessage, setShowMainMessage] = React.useState(true);
45
+ const [showDetails, setShowDetails] = React.useState(false);
46
+
47
+ const timestamp = new Date(updatedAt).toLocaleString();
48
+ const isUserMessage = role === MessageRole.USER;
49
+ const username = isUserMessage
50
+ ? `${user?.firstName || ""} ${user?.lastName || ""}`.trim() || "You"
51
+ : modelName || "AI";
52
+
53
+ const codeHeaderTemplate = `
54
+ <span class="title">
55
+ <span class="header-toggle">
56
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
57
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
58
+ class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right">
59
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
60
+ <path d="M9 6l6 6l-6 6" />
61
+ </svg>
62
+ </span>
63
+ <span class="language"><LANG></span>
64
+ </span>
65
+
66
+ <div class="code-header-actions">
67
+ <div type="button" class="action-btn mantine-focus-auto mantine-active code-copy-btn">
68
+ <div class="copy-icon">
69
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
70
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
71
+ class="icon icon-tabler icons-tabler-outline icon-tabler-copy">
72
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
73
+ <path
74
+ d="M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z" />
75
+ <path
76
+ d="M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1" />
77
+ </svg>
78
+ </div>
79
+ <div class="check-icon" style="display: none;">
80
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
81
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
82
+ class="icon icon-tabler icons-tabler-outline icon-tabler-copy-check">
83
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
84
+ <path stroke="none" d="M0 0h24v24H0z" />
85
+ <path
86
+ d="M7 9.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z" />
87
+ <path d="M4.012 16.737a2 2 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1" />
88
+ <path d="M11 14l2 2l4 -4" />
89
+ </svg>
90
+ </div>
91
+ <span>Copy</span>
92
+ </div>
93
+ </div>
94
+ `;
95
+
96
+ const processCodeElements = useCallback(
97
+ debounce(() => {
98
+ if (!componentRef.current) return;
99
+
100
+ componentRef.current.querySelectorAll("pre").forEach(pre => {
101
+ if (pre.querySelector(".code-data") && !pre?.parentElement?.classList?.contains("code-block")) {
102
+ const data = pre.querySelector(".code-data");
103
+ const block = document.createElement("div");
104
+ const header = document.createElement("div");
105
+ block.className = "code-block";
106
+ header.className = "code-header";
107
+ header.innerHTML = codeHeaderTemplate.replaceAll("<LANG>", data?.getAttribute("data-lang") || "plaintext");
108
+
109
+ pre.parentNode?.insertBefore(header, pre);
110
+ pre.parentNode?.insertBefore(block, pre);
111
+ block.appendChild(pre);
112
+ }
113
+ });
114
+
115
+ componentRef.current.querySelectorAll("img").forEach(img => {
116
+ if (!img?.classList?.contains("message-image")) {
117
+ img.classList.add("message-image");
118
+ const fileName = img.src.split("/").pop() || "";
119
+ img.setAttribute("data-file-name", fileName);
120
+ }
121
+ });
122
+ }, 250),
123
+ []
124
+ );
125
+
126
+ useEffect(() => {
127
+ if (streaming) return;
128
+
129
+ if (componentRef.current) {
130
+ const observer = new MutationObserver(processCodeElements);
131
+ observer.observe(componentRef.current, { childList: true, subtree: true });
132
+ processCodeElements(); // Initial call to inject code elements
133
+ return () => observer.disconnect();
134
+ }
135
+ }, [role, streaming]);
136
+
137
+ const toggleDetails = () => setShowDetails(s => !s);
138
+
139
+ const details = useMemo(() => {
140
+ return messageDetailsLoader ? messageDetailsLoader(message) : null;
141
+ }, [messageDetailsLoader, message]);
142
+
143
+ const mainMessage = useMemo(() => {
144
+ const plugins = pluginsLoader ? pluginsLoader(message) : null;
145
+ const model = models?.find(m => m.modelId === message?.modelId);
146
+
147
+ return (
148
+ <>
149
+ <Group align="center">
150
+ <Avatar color="gray" radius="xl" size="md" src={isUserMessage ? message?.user?.avatarUrl : undefined}>
151
+ {isUserMessage ? (
152
+ <IconUser />
153
+ ) : model ? (
154
+ <ProviderIcon apiProvider={model.apiProvider} provider={model.provider} />
155
+ ) : (
156
+ <IconRobot />
157
+ )}
158
+ </Avatar>
159
+ <Group gap="xs">
160
+ <Text size="sm" fw={500} c={isUserMessage ? "blue" : "teal"}>
161
+ {username}
162
+ </Text>
163
+ <Text size="xs" c="dimmed">
164
+ {timestamp}
165
+ </Text>
166
+ {status && <MessageStatus status={status} />}
167
+ {statusInfo && (
168
+ <Text size="xs" c="dimmed">
169
+ {statusInfo}
170
+ </Text>
171
+ )}
172
+ </Group>
173
+ </Group>
174
+ <div className={`${classes.message} ${classes[role] || ""} ${streaming ? classes.streaming : ""}`}>
175
+ {streaming && !content && <Loader size="md" mb="md" />}
176
+
177
+ {html ? (
178
+ html.map((part, index) => <div key={index} dangerouslySetInnerHTML={{ __html: part }} />)
179
+ ) : (
180
+ <div>{content}</div>
181
+ )}
182
+
183
+ {details && (
184
+ <Box mt="md">
185
+ <Group mb={5}>
186
+ <Button onClick={toggleDetails} p="xs" variant="light" size="xs">
187
+ Details
188
+ </Button>
189
+ </Group>
190
+
191
+ <Collapse in={showDetails}>
192
+ <div className={classes.detailsBlock}>{details}</div>
193
+ </Collapse>
194
+ </Box>
195
+ )}
196
+
197
+ <div className={classes.messageFooter}>
198
+ <CopyMessageButton messageId={id} messageIndex={index} />
199
+
200
+ {plugins}
201
+ </div>
202
+ </div>
203
+ </>
204
+ );
205
+ }, [
206
+ role,
207
+ username,
208
+ timestamp,
209
+ content,
210
+ html,
211
+ id,
212
+ modelName,
213
+ modelId,
214
+ models,
215
+ index,
216
+ disableActions,
217
+ details,
218
+ showDetails,
219
+ streaming,
220
+ ]);
221
+
222
+ const linkedMessagesCmp = useMemo(() => {
223
+ if (!linkedMessages || linkedMessages.length === 0) return null;
224
+
225
+ return (
226
+ <Carousel
227
+ withIndicators={linkedMessages.length > 1}
228
+ emblaOptions={{ align: "center", loop: true }}
229
+ slideGap="0"
230
+ withControls={linkedMessages.length > 1}
231
+ initialSlide={linkedMessages.findIndex(m => m.streaming)}
232
+ classNames={carouselClasses}
233
+ >
234
+ {linkedMessages.map((linkedMsg, linkedIndex) => (
235
+ <LinkedChatMessage
236
+ key={linkedMsg.id}
237
+ message={linkedMsg}
238
+ parentIndex={index}
239
+ index={linkedIndex}
240
+ models={models}
241
+ plugins={pluginsLoader?.(linkedMsg)}
242
+ />
243
+ ))}
244
+ </Carousel>
245
+ );
246
+ }, [linkedMessages, models, pluginsLoader, index]);
247
+
248
+ if (!linkedMessagesCmp) {
249
+ return (
250
+ <div className={classes.messageContainer} ref={componentRef}>
251
+ <div className={classes.main}>{mainMessage}</div>
252
+ </div>
253
+ );
254
+ }
255
+
256
+ return (
257
+ <div className={classes.messageContainer} ref={componentRef}>
258
+ <div className={classes.linkedToggle}>
259
+ <Switch
260
+ checked={showMainMessage}
261
+ onChange={event => setShowMainMessage(event.currentTarget.checked)}
262
+ label={showMainMessage ? "Main" : "Others"}
263
+ size="sm"
264
+ />
265
+ </div>
266
+ <div className={[classes.main, showMainMessage ? "" : classes.hidden].join(" ")}>{mainMessage}</div>
267
+ <div className={[classes.linked, showMainMessage ? classes.hidden : ""].join(" ")}>{linkedMessagesCmp}</div>
268
+ </div>
269
+ );
270
+ };
271
+ ChatMessage.displayName = "ChatMessage";
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { ScrollArea } from "@mantine/core";
3
+
4
+ import classes from "./ChatMessage.module.scss";
5
+
6
+ export const ChatMessagePreview: React.FC<{ html?: string[]; text?: string }> = ({ html, text }) => {
7
+ return (
8
+ <ScrollArea type="hover" offsetScrollbars className={[classes.message, classes.preview].join(" ")}>
9
+ {text ? (
10
+ <>
11
+ {html ? (
12
+ html.map((part, index) => <div key={index} dangerouslySetInnerHTML={{ __html: part }} />)
13
+ ) : (
14
+ <div>{text}</div>
15
+ )}
16
+ </>
17
+ ) : (
18
+ "..."
19
+ )}
20
+ </ScrollArea>
21
+ );
22
+ };