@admin-layout/tailwind-chat-pro 12.2.4-alpha.81 → 12.2.4-alpha.85

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 (31) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/components/messages-container-ui/ErrorFixCard.d.ts +10 -0
  3. package/lib/components/messages-container-ui/ErrorFixCard.d.ts.map +1 -0
  4. package/lib/components/messages-container-ui/ErrorFixCard.js +219 -0
  5. package/lib/components/messages-container-ui/ErrorFixCard.js.map +1 -0
  6. package/lib/components/messages-container-ui/FilesList.d.ts +19 -0
  7. package/lib/components/messages-container-ui/FilesList.d.ts.map +1 -0
  8. package/lib/components/messages-container-ui/FilesList.js +75 -0
  9. package/lib/components/messages-container-ui/FilesList.js.map +1 -0
  10. package/lib/components/messages-container-ui/ModernMessageGroup.d.ts +46 -0
  11. package/lib/components/messages-container-ui/ModernMessageGroup.d.ts.map +1 -0
  12. package/lib/components/messages-container-ui/ModernMessageGroup.js +1884 -0
  13. package/lib/components/messages-container-ui/ModernMessageGroup.js.map +1 -0
  14. package/lib/components/messages-container-ui/index.d.ts +3 -0
  15. package/lib/components/messages-container-ui/index.d.ts.map +1 -1
  16. package/lib/components/messages-container-ui/messenger-slot-fill-name-enum.d.ts +15 -0
  17. package/lib/components/messages-container-ui/messenger-slot-fill-name-enum.d.ts.map +1 -0
  18. package/lib/components/messages-container-ui/messenger-slot-fill-name-enum.js +15 -0
  19. package/lib/components/messages-container-ui/messenger-slot-fill-name-enum.js.map +1 -0
  20. package/lib/components/messages-container-ui/modernStreaming.d.ts +11 -0
  21. package/lib/components/messages-container-ui/modernStreaming.d.ts.map +1 -0
  22. package/lib/components/messages-container-ui/modernStreaming.js +17 -0
  23. package/lib/components/messages-container-ui/modernStreaming.js.map +1 -0
  24. package/lib/index.js +1 -1
  25. package/package.json +13 -7
  26. package/src/components/messages-container-ui/ErrorFixCard.tsx +239 -0
  27. package/src/components/messages-container-ui/FilesList.tsx +89 -0
  28. package/src/components/messages-container-ui/ModernMessageGroup.tsx +2225 -0
  29. package/src/components/messages-container-ui/index.ts +10 -0
  30. package/src/components/messages-container-ui/messenger-slot-fill-name-enum.ts +14 -0
  31. package/src/components/messages-container-ui/modernStreaming.tsx +18 -0
@@ -0,0 +1,2225 @@
1
+ import { Slot } from '@common-stack/components-pro';
2
+ import { AiAgentMessageRole, IAuthUser, IPost } from 'common';
3
+ import { differenceInMinutes, format } from 'date-fns';
4
+ import { marked } from 'marked';
5
+ import React, { useEffect, useMemo } from 'react';
6
+ import ReactMarkdown from 'react-markdown';
7
+ import remarkBreaks from 'remark-breaks';
8
+ import remarkGfm from 'remark-gfm';
9
+ import { MessengerSlotFillNameEnum } from './messenger-slot-fill-name-enum';
10
+ import { FilesList } from './FilesList';
11
+ import { ErrorFixCard } from './ErrorFixCard';
12
+
13
+ // Configure marked for better markdown rendering
14
+ marked.setOptions({
15
+ breaks: true, // Convert line breaks to <br>
16
+ gfm: true, // GitHub Flavored Markdown
17
+ headerIds: false, // Disable header IDs for cleaner HTML
18
+ mangle: false, // Don't mangle email addresses
19
+ });
20
+
21
+ const ASSISTANT_RESPONSE_THEME_CLASS = 'llm-response-theme';
22
+ const ASSISTANT_RESPONSE_CLASS = 'llm-response';
23
+ const ASSISTANT_RESPONSE_BUBBLE_CLASS = 'llm-response__bubble';
24
+ const ASSISTANT_RESPONSE_MARKDOWN_CLASS = 'llm-response__markdown';
25
+ const USER_MESSAGE_TEXT_SIZE_CLASS = 'text-base';
26
+ const USER_MESSAGE_PROSE_SIZE_CLASS = 'prose-base';
27
+
28
+ function getAssistantResponseClasses(innerClassName?: string): string {
29
+ return [ASSISTANT_RESPONSE_MARKDOWN_CLASS, innerClassName].filter(Boolean).join(' ');
30
+ }
31
+
32
+ export function AssistantResponseWrapper({
33
+ children,
34
+ bubbleClassName,
35
+ }: {
36
+ children: React.ReactNode;
37
+ bubbleClassName?: string;
38
+ }) {
39
+ return (
40
+ <div className={ASSISTANT_RESPONSE_THEME_CLASS}>
41
+ <div className={ASSISTANT_RESPONSE_CLASS}>
42
+ <div className={[ASSISTANT_RESPONSE_BUBBLE_CLASS, bubbleClassName].filter(Boolean).join(' ')}>
43
+ {children}
44
+ </div>
45
+ </div>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ // Enhanced CSS styles for HTML content rendering with prettification
51
+ const htmlContentStyles = `
52
+ .html-content {
53
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
54
+ line-height: 1.6;
55
+ color: #374151;
56
+ }
57
+
58
+ .html-content h1, .html-content h2, .html-content h3, .html-content h4, .html-content h5, .html-content h6 {
59
+ margin-top: 1.5rem;
60
+ margin-bottom: 1rem;
61
+ font-weight: 600;
62
+ line-height: 1.25;
63
+ color: #111827;
64
+ }
65
+
66
+ .html-content h1 {
67
+ font-size: 1.875rem;
68
+ border-bottom: 2px solid #e5e7eb;
69
+ padding-bottom: 0.5rem;
70
+ }
71
+ .${ASSISTANT_RESPONSE_THEME_CLASS} {
72
+ --llm-content-max-width: 100%;
73
+ --llm-font-sans: 'Avenir Next', 'Segoe UI', sans-serif;
74
+ --llm-font-mono: 'IBM Plex Mono', 'SFMono-Regular', Menlo, Consolas, monospace;
75
+ --llm-color-bg: #ffffff;
76
+ --llm-color-surface: #f6f7f8;
77
+ --llm-color-surface-2: #eef1f4;
78
+ --llm-color-surface-hover: #e9edf2;
79
+ --llm-color-border: #e3e7ee;
80
+ --llm-color-border-strong: #cfd7e3;
81
+ --llm-color-text: #121926;
82
+ --llm-color-text-secondary: #667085;
83
+ --llm-color-marker: #8e98a8;
84
+ --llm-color-link-underline: rgba(18, 25, 38, 0.18);
85
+ --llm-color-code-bg: rgba(249, 115, 22, 0.1);
86
+ --llm-color-code-text: #d96b00;
87
+ --llm-color-code-block-border: #ebeef3;
88
+ --llm-shadow-soft: 0 1px 2px rgba(18, 25, 38, 0.04), 0 10px 32px rgba(18, 25, 38, 0.05);
89
+ --llm-radius-bubble: 1.5rem;
90
+ --llm-radius-code: 0.85rem;
91
+ --llm-line-height-body: 1.62;
92
+ --llm-line-height-mobile: 1.55;
93
+ --llm-line-height-heading: 1.35;
94
+ --llm-space-heading-top: 1.45rem;
95
+ --llm-space-heading-bottom: 0.75rem;
96
+ --llm-space-paragraph: 1rem;
97
+ --llm-space-list: 0.9rem;
98
+ color: var(--llm-color-text);
99
+ background: transparent;
100
+ }
101
+
102
+ .${ASSISTANT_RESPONSE_THEME_CLASS}[data-color-mode='dark'],
103
+ [data-color-mode='dark'] .${ASSISTANT_RESPONSE_THEME_CLASS} {
104
+ --llm-color-bg: #0f1115;
105
+ --llm-color-surface: #171a20;
106
+ --llm-color-surface-2: #1f2530;
107
+ --llm-color-surface-hover: #232a36;
108
+ --llm-color-border: #2a3240;
109
+ --llm-color-border-strong: #364052;
110
+ --llm-color-text: #f3f5f8;
111
+ --llm-color-text-secondary: #aab3c2;
112
+ --llm-color-marker: #7f8999;
113
+ --llm-color-link-underline: rgba(243, 245, 248, 0.24);
114
+ --llm-color-code-bg: rgba(251, 146, 60, 0.14);
115
+ --llm-color-code-text: #ffb36b;
116
+ --llm-color-code-block-border: #262d39;
117
+ --llm-shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.28), 0 14px 36px rgba(0, 0, 0, 0.3);
118
+ }
119
+
120
+ .${ASSISTANT_RESPONSE_CLASS} {
121
+ position: relative;
122
+ display: flex;
123
+ flex-direction: column;
124
+ align-items: flex-start;
125
+ justify-content: center;
126
+ width: 100%;
127
+ max-width: var(--llm-content-max-width);
128
+ font-family: var(--llm-font-sans);
129
+ color: var(--llm-color-text);
130
+ }
131
+
132
+ .${ASSISTANT_RESPONSE_BUBBLE_CLASS} {
133
+ position: relative;
134
+ width: 100%;
135
+ min-height: 1.75rem;
136
+ border-radius: var(--llm-radius-bubble);
137
+ color: inherit;
138
+ word-break: break-word;
139
+ }
140
+
141
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} {
142
+ position: relative;
143
+ width: 100%;
144
+ max-width: none;
145
+ color: inherit;
146
+ font-size: 1rem;
147
+ line-height: var(--llm-line-height-body);
148
+ }
149
+
150
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} > :first-child {
151
+ margin-top: 0;
152
+ }
153
+
154
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} > :last-child {
155
+ margin-bottom: 0;
156
+ }
157
+
158
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} p,
159
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} li,
160
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} th,
161
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} td,
162
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} blockquote,
163
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS}.html-content,
164
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS}.assistant-html-content {
165
+ color: inherit;
166
+ }
167
+
168
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} p,
169
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS}.html-content p,
170
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS}.assistant-html-content p {
171
+ margin: 0 0 var(--llm-space-paragraph);
172
+ white-space: pre-wrap;
173
+ }
174
+
175
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} h1,
176
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} h2,
177
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} h3,
178
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} h4,
179
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} h5,
180
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} h6 {
181
+ margin: var(--llm-space-heading-top) 0 var(--llm-space-heading-bottom);
182
+ font-weight: 600;
183
+ letter-spacing: -0.02em;
184
+ line-height: var(--llm-line-height-heading);
185
+ color: var(--llm-color-text);
186
+ }
187
+
188
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} h1 { font-size: 1.75rem; }
189
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} h2 { font-size: 1.5rem; }
190
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} h3 { font-size: 1.25rem; }
191
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} h4 { font-size: 1.05rem; }
192
+
193
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} strong,
194
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} b {
195
+ font-weight: 600;
196
+ color: var(--llm-color-text);
197
+ }
198
+
199
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} em,
200
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} i {
201
+ color: var(--llm-color-text-secondary);
202
+ }
203
+
204
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} a {
205
+ color: currentColor;
206
+ text-decoration: underline;
207
+ text-decoration-color: var(--llm-color-link-underline);
208
+ text-underline-offset: 0.18em;
209
+ transition: color 120ms ease, text-decoration-color 120ms ease;
210
+ }
211
+
212
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} a:hover {
213
+ color: var(--llm-color-text);
214
+ text-decoration-color: currentColor;
215
+ }
216
+
217
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} ul,
218
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} ol {
219
+ margin: 0.35rem 0 var(--llm-space-list);
220
+ padding-left: 1.15rem;
221
+ }
222
+
223
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} ul {
224
+ list-style-type: disc;
225
+ }
226
+
227
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} ol {
228
+ list-style-type: decimal;
229
+ /* Wider gutter so multi-digit markers (e.g. "60.") aren't clipped at the
230
+ content's left edge — long numbered content (verse, step lists) showed
231
+ "0." for line 60. */
232
+ padding-left: 2rem;
233
+ }
234
+
235
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} li {
236
+ margin: 0.28rem 0;
237
+ padding-inline-start: 0.1rem;
238
+ /* \`normal\` collapses the stray "\\n" react-markdown inserts between <li>
239
+ and its child <p>; with \`pre-wrap\` that newline rendered as a blank
240
+ first line, stranding the marker above its text. Intra-paragraph
241
+ whitespace is preserved by the \`li > p\` rule below. */
242
+ white-space: normal;
243
+ }
244
+
245
+ /* Loose list items wrap their text in <p>. Keep pre-wrap for the text but
246
+ drop the paragraph margins so each item is a single tight line — fixes
247
+ poems / verse the model emits as numbered lists rendering as a sprawling,
248
+ double-spaced listicle. */
249
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} li > p {
250
+ margin-top: 0;
251
+ white-space: pre-wrap;
252
+ }
253
+
254
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} li > p:last-child {
255
+ margin-bottom: 0;
256
+ }
257
+
258
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} li > ul,
259
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} li > ol {
260
+ margin-top: 0.35rem;
261
+ margin-bottom: 0.15rem;
262
+ white-space: normal;
263
+ }
264
+
265
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} ::marker {
266
+ color: var(--llm-color-marker);
267
+ }
268
+
269
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} hr {
270
+ margin: 1.25rem 0;
271
+ border: 0;
272
+ border-top: 1px solid var(--llm-color-border);
273
+ }
274
+
275
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} blockquote {
276
+ margin: 1rem 0;
277
+ padding: 0.2rem 0 0.2rem 1rem;
278
+ border-left: 2px solid var(--llm-color-border-strong);
279
+ background: transparent;
280
+ color: var(--llm-color-text-secondary);
281
+ }
282
+
283
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} blockquote p {
284
+ margin-bottom: 0;
285
+ color: inherit;
286
+ }
287
+
288
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} :not(pre) > code,
289
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} .llm-inline-code {
290
+ display: inline-block;
291
+ padding: 0.08rem 0.38rem;
292
+ border: 0;
293
+ border-radius: 0.32rem;
294
+ font-family: var(--llm-font-mono);
295
+ font-size: 0.875em;
296
+ line-height: 1.45;
297
+ background: var(--llm-color-code-bg);
298
+ color: var(--llm-color-code-text);
299
+ box-decoration-break: clone;
300
+ -webkit-box-decoration-break: clone;
301
+ }
302
+
303
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} table {
304
+ width: 100%;
305
+ margin: 1rem 0 1.1rem;
306
+ border-collapse: separate;
307
+ border-spacing: 0;
308
+ border: 1px solid var(--llm-color-border);
309
+ border-radius: 1rem;
310
+ overflow: hidden;
311
+ box-shadow: var(--llm-shadow-soft);
312
+ }
313
+
314
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} thead {
315
+ background: var(--llm-color-surface);
316
+ }
317
+
318
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} th,
319
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} td {
320
+ padding: 0.85rem 1rem;
321
+ border-bottom: 1px solid var(--llm-color-border);
322
+ text-align: left;
323
+ vertical-align: top;
324
+ }
325
+
326
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} th {
327
+ font-size: 0.8rem;
328
+ font-weight: 600;
329
+ letter-spacing: 0.02em;
330
+ color: var(--llm-color-text-secondary);
331
+ background: var(--llm-color-surface);
332
+ }
333
+
334
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} td {
335
+ color: var(--llm-color-text);
336
+ background: var(--llm-color-bg);
337
+ }
338
+
339
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} tbody tr:last-child td {
340
+ border-bottom: 0;
341
+ }
342
+
343
+ @media (max-width: 768px) {
344
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} {
345
+ font-size: 0.95rem;
346
+ line-height: var(--llm-line-height-mobile);
347
+ }
348
+
349
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} th,
350
+ .${ASSISTANT_RESPONSE_MARKDOWN_CLASS} td {
351
+ padding: 0.75rem 0.8rem;
352
+ }
353
+ }
354
+ * we must neutralize it so it doesn't override the pill background/padding.
355
+ */
356
+ .user-message .message-container {
357
+ background: transparent !important;
358
+ padding-top: 0 !important;
359
+ padding-bottom: 0 !important;
360
+ margin: 0 !important;
361
+ transition: none !important;
362
+ box-shadow: none !important;
363
+ }
364
+
365
+ .assistant-message-container {
366
+ background: transparent;
367
+ padding-top: 0;
368
+ padding-bottom: 0;
369
+ margin: 0;
370
+ transition: none;
371
+ box-shadow: none;
372
+ }
373
+
374
+ .assistant-html-content {
375
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
376
+ line-height: 1.6;
377
+ color: #374151;
378
+ }
379
+
380
+ .assistant-html-content h1,
381
+ .assistant-html-content h2,
382
+ .assistant-html-content h3,
383
+ .assistant-html-content h4,
384
+ .assistant-html-content h5,
385
+ .assistant-html-content h6 {
386
+ font-weight: 600;
387
+ line-height: 1.25;
388
+ color: #111827;
389
+ }
390
+
391
+ .assistant-html-content h1 {
392
+ font-size: 1.875rem;
393
+ margin-top: 2rem;
394
+ margin-bottom: 1rem;
395
+ }
396
+
397
+ .assistant-html-content h2 {
398
+ font-size: 1.5rem;
399
+ margin-top: 2rem;
400
+ margin-bottom: 1rem;
401
+ }
402
+
403
+ .assistant-html-content h3 {
404
+ font-size: 1.25rem;
405
+ margin-top: 1.5rem;
406
+ margin-bottom: 1rem;
407
+ }
408
+
409
+ .assistant-html-content h4 {
410
+ font-size: 1.125rem;
411
+ margin-top: 1rem;
412
+ margin-bottom: 0.5rem;
413
+ }
414
+
415
+ .assistant-html-content p {
416
+ margin-top: 0;
417
+ margin-bottom: 1rem;
418
+ line-height: 1.625;
419
+ color: #374151;
420
+ }
421
+
422
+ .assistant-html-content ul,
423
+ .assistant-html-content ol {
424
+ margin: 1rem 0;
425
+ padding-left: 1.25rem;
426
+ }
427
+
428
+ .assistant-html-content ul {
429
+ list-style-type: disc;
430
+ }
431
+
432
+ .assistant-html-content ol {
433
+ list-style-type: decimal;
434
+ }
435
+
436
+ .assistant-html-content li {
437
+ margin-bottom: 0.375rem;
438
+ line-height: 1.625;
439
+ color: #374151;
440
+ }
441
+
442
+ .assistant-html-content li > ul,
443
+ .assistant-html-content li > ol {
444
+ margin-top: 0.375rem;
445
+ margin-bottom: 0.375rem;
446
+ }
447
+
448
+ .assistant-html-content a {
449
+ color: #2563eb;
450
+ text-decoration: none;
451
+ }
452
+
453
+ .assistant-html-content a:hover {
454
+ color: #1d4ed8;
455
+ text-decoration: underline;
456
+ text-underline-offset: 2px;
457
+ }
458
+
459
+ .assistant-html-content strong,
460
+ .assistant-html-content b {
461
+ font-weight: 600;
462
+ color: #111827;
463
+ }
464
+
465
+ .assistant-html-content em,
466
+ .assistant-html-content i {
467
+ font-style: italic;
468
+ color: #4b5563;
469
+ }
470
+
471
+ .assistant-html-content code {
472
+ background-color: #f3f4f6;
473
+ padding: 0.25rem 0.5rem;
474
+ border-radius: 0.375rem;
475
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
476
+ font-size: 0.875rem;
477
+ color: #dc2626;
478
+ border: 1px solid #e5e7eb;
479
+ }
480
+
481
+ .assistant-html-content pre {
482
+ background-color: #f9fafb;
483
+ padding: 1rem;
484
+ border-radius: 0.5rem;
485
+ overflow-x: auto;
486
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
487
+ font-size: 0.875rem;
488
+ line-height: 1.5;
489
+ border: 1px solid #e5e7eb;
490
+ margin: 1.5rem 0;
491
+ }
492
+
493
+ .assistant-html-content pre code {
494
+ background-color: transparent;
495
+ padding: 0;
496
+ border: none;
497
+ color: #374151;
498
+ font-size: inherit;
499
+ }
500
+
501
+ .assistant-html-content blockquote {
502
+ margin: 1.5rem 0;
503
+ padding: 0 0 0 1rem;
504
+ border-left: 4px solid #d1d5db;
505
+ font-style: italic;
506
+ color: #6b7280;
507
+ background-color: transparent;
508
+ border-radius: 0;
509
+ }
510
+
511
+ .assistant-html-content blockquote p {
512
+ margin-bottom: 0;
513
+ }
514
+
515
+ .assistant-html-content table {
516
+ border-collapse: collapse;
517
+ border: 1px solid #e5e7eb;
518
+ width: 100%;
519
+ margin: 1rem 0;
520
+ }
521
+
522
+ .assistant-html-content th,
523
+ .assistant-html-content td {
524
+ text-align: left;
525
+ vertical-align: top;
526
+ }
527
+
528
+ .assistant-html-content th {
529
+ padding: 0.5rem 1rem;
530
+ background-color: #f9fafb;
531
+ border: 1px solid #e5e7eb;
532
+ font-weight: 500;
533
+ font-size: 0.75rem;
534
+ letter-spacing: 0.04em;
535
+ text-transform: uppercase;
536
+ color: #4b5563;
537
+ }
538
+
539
+ .assistant-html-content td {
540
+ padding: 0.5rem 1rem;
541
+ border-top: 1px solid #e5e7eb;
542
+ color: #374151;
543
+ }
544
+
545
+ .assistant-html-content hr {
546
+ border: none;
547
+ border-top: 1px solid #e5e7eb;
548
+ margin: 2rem 0;
549
+ }
550
+
551
+ /* Markdown content styling */
552
+ .markdown-content {
553
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
554
+ line-height: 1.6;
555
+ color: #374151;
556
+ }
557
+
558
+ .markdown-content h1 {
559
+ font-size: 1.5rem;
560
+ font-weight: 700;
561
+ margin-top: 1.5rem;
562
+ margin-bottom: 1rem;
563
+ color: #111827;
564
+ padding-bottom: 0.5rem;
565
+ border-bottom: 2px solid #e5e7eb;
566
+ }
567
+
568
+ .markdown-content h2 {
569
+ font-size: 1.25rem;
570
+ font-weight: 600;
571
+ margin-top: 1.25rem;
572
+ margin-bottom: 0.75rem;
573
+ color: #1f2937;
574
+ padding-bottom: 0.375rem;
575
+ border-bottom: 1px solid #e5e7eb;
576
+ }
577
+
578
+ .markdown-content h3 {
579
+ font-size: 1.125rem;
580
+ font-weight: 600;
581
+ margin-top: 1rem;
582
+ margin-bottom: 0.5rem;
583
+ color: #374151;
584
+ }
585
+
586
+ .markdown-content p {
587
+ margin-bottom: 0.75rem;
588
+ line-height: 1.6;
589
+ color: #4b5563;
590
+ }
591
+
592
+ .markdown-content ul, .markdown-content ol {
593
+ margin: 1rem 0;
594
+ padding-left: 1.5rem;
595
+ }
596
+
597
+ .markdown-content li {
598
+ margin-bottom: 0.5rem;
599
+ line-height: 1.6;
600
+ }
601
+
602
+ .markdown-content code {
603
+ background-color: #f3f4f6;
604
+ padding: 0.125rem 0.375rem;
605
+ border-radius: 0.25rem;
606
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
607
+ font-size: 0.875rem;
608
+ color: #dc2626;
609
+ border: 1px solid #e5e7eb;
610
+ }
611
+
612
+ .markdown-content pre {
613
+ color: #1f2937;
614
+ border-radius: 8px;
615
+ overflow-x: auto;
616
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
617
+ font-size: 0.875rem;
618
+ line-height: 1.5;
619
+ margin: 1rem 0;
620
+ }
621
+
622
+ .markdown-content pre code {
623
+ background-color: transparent;
624
+ padding: 0;
625
+ border: none;
626
+ color: #1f2937;
627
+ font-size: inherit;
628
+ }
629
+
630
+ .markdown-content blockquote {
631
+ border-left: 4px solid #3b82f6;
632
+ background-color: #f8fafc;
633
+ padding: 1rem;
634
+ margin: 1rem 0;
635
+ border-radius: 6px;
636
+ font-style: italic;
637
+ color: #475569;
638
+ border-top: 1px solid #e5e7eb;
639
+ border-right: 1px solid #e5e7eb;
640
+ border-bottom: 1px solid #e5e7eb;
641
+ }
642
+
643
+ .markdown-content table {
644
+ border-collapse: collapse;
645
+ border: 1px solid #e5e7eb;
646
+ width: 100%;
647
+ margin: 1rem 0;
648
+ border-radius: 8px;
649
+ overflow: hidden;
650
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
651
+ }
652
+
653
+ .markdown-content th {
654
+ background-color: #f9fafb;
655
+ color: #374151;
656
+ font-weight: 600;
657
+ padding: 0.75rem;
658
+ text-align: left;
659
+ border-bottom: 1px solid #e5e7eb;
660
+ font-size: 0.875rem;
661
+ }
662
+
663
+ .markdown-content td {
664
+ border: 1px solid #e5e7eb;
665
+ padding: 0.75rem;
666
+ text-align: left;
667
+ vertical-align: top;
668
+ background-color: white;
669
+ font-size: 0.875rem;
670
+ }
671
+
672
+ .markdown-content tr:nth-child(even) td {
673
+ background-color: #f9fafb;
674
+ }
675
+
676
+ .markdown-content tr:hover td {
677
+ background-color: #f3f4f6;
678
+ }
679
+
680
+ /* Enhanced list styling */
681
+ .markdown-content ul li {
682
+ position: relative;
683
+ padding-left: 0.5rem;
684
+ }
685
+
686
+ .markdown-content ul li::before {
687
+ content: "•";
688
+ color: #000;
689
+ font-weight: bold;
690
+ position: absolute;
691
+ left: -1rem;
692
+ }
693
+
694
+ .markdown-content ol li {
695
+ position: relative;
696
+ padding-left: 0.5rem;
697
+ }
698
+
699
+ .markdown-content ol li::before {
700
+ content: counter(list-item) ".";
701
+ font-weight: 600;
702
+ position: absolute;
703
+ left: -1.5rem;
704
+ }
705
+
706
+ /* Code block container styling */
707
+ .code-block-container {
708
+ background: #ffffff;
709
+ border: 1px solid #e5e7eb;
710
+ border-radius: 8px;
711
+ overflow: hidden;
712
+ margin: 1rem 0;
713
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
714
+ }
715
+
716
+ .code-block-header {
717
+ background: #f9fafb;
718
+ padding: 0.5rem 1rem;
719
+ border-bottom: 1px solid #e5e7eb;
720
+ font-weight: 600;
721
+ color: #374151;
722
+ font-size: 0.875rem;
723
+ text-transform: uppercase;
724
+ letter-spacing: 0.05em;
725
+ }
726
+
727
+ .code-block-content {
728
+ padding: 1rem;
729
+ overflow-x: auto;
730
+ }
731
+
732
+ /* Enhanced prose styling for markdown content */
733
+ .prose pre {
734
+ background-color: #f9fafb;
735
+ border: 1px solid #e5e7eb;
736
+ border-radius: 0.5rem;
737
+ padding: 1rem;
738
+ overflow-x: auto;
739
+ }
740
+
741
+ .prose code {
742
+ background-color: #f3f4f6;
743
+ padding: 0.125rem 0.375rem;
744
+ border-radius: 0.25rem;
745
+ font-size: 0.875rem;
746
+ border: 1px solid #e5e7eb;
747
+ }
748
+
749
+ .prose pre code {
750
+ background-color: transparent;
751
+ padding: 0;
752
+ border: none;
753
+ color:#000;
754
+ }
755
+
756
+ .hover\:bg-gray-100:hover {
757
+ background-color: transparent !important;
758
+ }
759
+ `;
760
+
761
+ // Hook to inject CSS styles
762
+ const useInjectStyles = () => {
763
+ useEffect(() => {
764
+ const styleElementPresent = !!document.getElementById('html-content-styles');
765
+
766
+ // Check if styles are already injected
767
+ if (styleElementPresent) {
768
+ return;
769
+ }
770
+
771
+ // Create and inject style element
772
+ const styleElement = document.createElement('style');
773
+ styleElement.id = 'html-content-styles';
774
+ styleElement.textContent = htmlContentStyles;
775
+ document.head.appendChild(styleElement);
776
+
777
+ // Cleanup function
778
+ return () => {
779
+ const existingStyle = document.getElementById('html-content-styles');
780
+ if (existingStyle) {
781
+ existingStyle.remove();
782
+ }
783
+ };
784
+ }, []);
785
+ };
786
+
787
+ interface ModernMessageGroupProps {
788
+ showAuthorName?: boolean;
789
+ showAvatar?: boolean;
790
+ showTimestamp?: boolean;
791
+ messages: IPost[];
792
+ currentUser: IAuthUser;
793
+ onOpen: (element?: any) => void;
794
+ onMessageClick: (msg: IPost) => void;
795
+ isDesktopView?: boolean;
796
+ isSmallScreen?: boolean;
797
+ sandboxErrors?: any[];
798
+ currentFiles?: Record<string, string>;
799
+ onFixError?: (errorMessage: string) => Promise<void>;
800
+ onRecreateSandbox?: (fragmentId: string) => void;
801
+ /** When true, code blocks show copy and download actions in header and footer (default: true) */
802
+ showCopyDownloadOnCodeBlock?: boolean;
803
+ }
804
+
805
+ interface MessageGroupProps {
806
+ showAuthorName?: boolean;
807
+ showAvatar?: boolean;
808
+ showTimestamp?: boolean;
809
+ author: any;
810
+ messages: IPost[];
811
+ currentUser: IAuthUser;
812
+ onOpen: (element?: any) => void;
813
+ onMessageClick: (msg: IPost) => void;
814
+ isDesktopView?: boolean;
815
+ isSmallScreen?: boolean;
816
+ showCopyDownloadOnCodeBlock?: boolean;
817
+ }
818
+
819
+ // Enhanced utility function to group messages by user and time
820
+ export const groupMessagesByUserAndTime = (messages: IPost[], timeThresholdMinutes = 5): IPost[][] => {
821
+ if (!messages || messages.length === 0) return [];
822
+
823
+ const groups: IPost[][] = [];
824
+ let currentGroup: IPost[] = [];
825
+ let lastMessage: IPost | null = null;
826
+
827
+ for (const message of messages) {
828
+ if (typeof message === 'string') continue; // Skip date separators
829
+
830
+ const shouldStartNewGroup =
831
+ !lastMessage ||
832
+ lastMessage.author?.id !== message.author?.id ||
833
+ differenceInMinutes(new Date(message.createdAt), new Date(lastMessage.createdAt)) > timeThresholdMinutes;
834
+
835
+ if (shouldStartNewGroup) {
836
+ if (currentGroup.length > 0) {
837
+ groups.push(currentGroup);
838
+ }
839
+ currentGroup = [message];
840
+ } else {
841
+ currentGroup.push(message);
842
+ }
843
+
844
+ lastMessage = message;
845
+ }
846
+
847
+ if (currentGroup.length > 0) {
848
+ groups.push(currentGroup);
849
+ }
850
+
851
+ return groups;
852
+ };
853
+
854
+ // Enhanced content detection that properly identifies different content types
855
+ export const detectContentType = (value: string): 'html' | 'markdown' | 'code' | 'text' => {
856
+ if (!value) return 'text';
857
+
858
+ const trimmed = value.trim();
859
+
860
+ // Check for markdown patterns FIRST (before code, as markdown can contain code blocks)
861
+ const markdownPatterns = [
862
+ /^#{1,6}\s+.+$/gm, // Headers (must have text after #)
863
+ /^\s*[-*+]\s+.+$/gm, // Unordered lists
864
+ /^\s*\d+\.\s+.+$/gm, // Numbered lists
865
+ /\[.+?\]\(.+?\)/g, // Links
866
+ /!\[.+?\]\(.+?\)/g, // Images
867
+ /```[\s\S]*?```/g, // Fenced code blocks (markdown feature)
868
+ /^\s*>\s+/gm, // Blockquotes
869
+ /\*\*.*?\*\*/g, // Bold
870
+ /\*[^*].*?\*/g, // Italic (not just asterisks)
871
+ /^\s*\|.+\|/gm, // Tables
872
+ ];
873
+
874
+ // Count markdown patterns - if we have multiple, it's likely markdown
875
+ const markdownMatches = markdownPatterns.filter((pattern) => {
876
+ const matches = trimmed.match(pattern);
877
+ return matches && matches.length > 0;
878
+ }).length;
879
+
880
+ // If we have 2+ markdown patterns, it's definitely markdown
881
+ if (markdownMatches >= 2) return 'markdown';
882
+
883
+ // If we have headers or lists, it's likely markdown
884
+ if (/^#{1,6}\s+/gm.test(trimmed) || /^\s*[-*+]\s+/gm.test(trimmed) || /^\s*\d+\.\s+/gm.test(trimmed)) {
885
+ return 'markdown';
886
+ }
887
+
888
+ // Check for pure code patterns (without markdown context)
889
+ const pureCodePatterns = [
890
+ /^---\s*\w+\.\w+\s*---$/gm, // File separators (standalone)
891
+ /^@tailwind\s+\w+/gm, // Tailwind directives at start
892
+ /^export\s+default\s*\{/gm, // JS/TS exports at start
893
+ /^import\s+.*\s+from\s+['"]/gm, // Import statements at start
894
+ ];
895
+
896
+ const hasPureCodePatterns = pureCodePatterns.some((pattern) => pattern.test(trimmed));
897
+ if (hasPureCodePatterns && markdownMatches === 0) return 'code';
898
+
899
+ // Check for HTML patterns
900
+ const htmlPatterns = [
901
+ /<[a-z][\s\S]*?>/i, // HTML tags
902
+ /&[a-z0-9#]+;/i, // HTML entities
903
+ /<[^>]+>[^<]*<\/[^>]+>/i, // HTML structure
904
+ ];
905
+
906
+ const hasHtmlPatterns = htmlPatterns.some((pattern) => pattern.test(trimmed));
907
+ // Only return HTML if it's clearly HTML and not markdown
908
+ if (hasHtmlPatterns && markdownMatches === 0) return 'html';
909
+
910
+ // Default to text, but FormattedMessageContent will still try to parse as markdown
911
+ return 'text';
912
+ };
913
+
914
+ // Legacy function for backward compatibility
915
+ const isProbablyHTML = (value: string): boolean => {
916
+ return detectContentType(value) === 'html';
917
+ };
918
+
919
+ /** Map message type (THINKING, TOOLUSE, etc.) to slot fill name for Kimi-like streaming UI */
920
+ const getMessageSlotName = (type: string | undefined): string | null => {
921
+ if (!type) return null;
922
+ const t = String(type).toUpperCase();
923
+ if (t === 'THINKING') return MessengerSlotFillNameEnum.THINKING_NAME;
924
+ if (t === 'TOOLUSE') return MessengerSlotFillNameEnum.TOOLUSE_NAME;
925
+ if (t === 'CODEBLOCK') return MessengerSlotFillNameEnum.CODEBLOCK_NAME;
926
+ if (t === 'SERVICE') return MessengerSlotFillNameEnum.SERVICE_NAME;
927
+ if (t === 'AIASSISTANT' || t === 'ASSISTANT') return MessengerSlotFillNameEnum.AIASSISTANT_NAME;
928
+ if (t === 'TRAVEL') return MessengerSlotFillNameEnum.TRAVEL_NAME;
929
+ if (t === 'MEDIA') return MessengerSlotFillNameEnum.MEDIA_NAME;
930
+ if (t === 'WORKFLOW') return MessengerSlotFillNameEnum.WORKFLOW_NAME;
931
+ if (t === 'CONNECTOR') return MessengerSlotFillNameEnum.CONNECTOR_NAME;
932
+ return null;
933
+ };
934
+
935
+ // Enhanced sanitizer that allows most HTML tags while maintaining security
936
+ const sanitizeHtml = (html: string): string => {
937
+ try {
938
+ if (typeof window === 'undefined' || typeof window.DOMParser === 'undefined') {
939
+ return html
940
+ .replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '')
941
+ .replace(/<style[\s\S]*?>[\s\S]*?<\/style>/gi, '')
942
+ .replace(/on\w+\s*=\s*"[^"]*"/gi, '')
943
+ .replace(/on\w+\s*=\s*'[^']*'/gi, '')
944
+ .replace(/on\w+\s*=\s*[^\s>]+/gi, '')
945
+ .replace(/javascript:/gi, '')
946
+ .replace(/data:text\/html/gi, '');
947
+ }
948
+
949
+ const parser = new window.DOMParser();
950
+ const doc = parser.parseFromString(html, 'text/html');
951
+
952
+ // Only remove the most dangerous tags
953
+ const dangerousTags = ['script', 'style'];
954
+ dangerousTags.forEach((tag) => {
955
+ doc.querySelectorAll(tag).forEach((el) => el.remove());
956
+ });
957
+
958
+ const walker = doc.createTreeWalker(doc.body, (NodeFilter as any).SHOW_ELEMENT);
959
+ let node = walker.nextNode() as Element | null;
960
+ while (node) {
961
+ Array.from(node.attributes).forEach((attr) => {
962
+ const name = attr.name.toLowerCase();
963
+ const value = (attr.value || '').toLowerCase();
964
+
965
+ // Remove event handlers and javascript protocols
966
+ if (name.startsWith('on') || value.startsWith('javascript:')) {
967
+ node?.removeAttribute(attr.name);
968
+ }
969
+
970
+ // Allow most attributes but validate URLs for src/href
971
+ if ((name === 'src' || name === 'href') && !/^https?:|^\/.*/.test(attr.value)) {
972
+ node?.removeAttribute(attr.name);
973
+ }
974
+ });
975
+ node = walker.nextNode() as Element | null;
976
+ }
977
+
978
+ return doc.body.innerHTML;
979
+ } catch (e) {
980
+ return html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '');
981
+ }
982
+ };
983
+
984
+ // Function to prettify HTML content
985
+ const prettifyHtmlContent = (htmlContent: string): string => {
986
+ // Add semantic classes for better styling
987
+ let prettified = htmlContent;
988
+
989
+ // Enhance headings with better styling
990
+ prettified = prettified.replace(/<h([1-6])>/g, '<h$1 class="html-heading html-heading-$1">');
991
+
992
+ // Enhance lists with better styling
993
+ prettified = prettified.replace(/<ul>/g, '<ul class="html-list html-list-unordered">');
994
+ prettified = prettified.replace(/<ol>/g, '<ol class="html-list html-list-ordered">');
995
+
996
+ // Enhance paragraphs with better spacing
997
+ prettified = prettified.replace(/<p>/g, '<p class="html-paragraph">');
998
+
999
+ // Enhance code blocks
1000
+ prettified = prettified.replace(/<code>/g, '<code class="html-inline-code">');
1001
+ prettified = prettified.replace(/<pre>/g, '<pre class="html-code-block">');
1002
+
1003
+ // Enhance blockquotes
1004
+ prettified = prettified.replace(/<blockquote>/g, '<blockquote class="html-blockquote">');
1005
+
1006
+ // Enhance tables
1007
+ prettified = prettified.replace(/<table>/g, '<table class="html-table">');
1008
+ prettified = prettified.replace(/<th>/g, '<th class="html-table-header">');
1009
+ prettified = prettified.replace(/<td>/g, '<td class="html-table-cell">');
1010
+
1011
+ // Add info boxes for certain content patterns
1012
+ prettified = prettified.replace(
1013
+ /<p>(Note|Tip|Info|Warning|Error):\s*(.*?)<\/p>/gi,
1014
+ '<div class="html-info-box"><strong>$1:</strong> $2</div>',
1015
+ );
1016
+
1017
+ return prettified;
1018
+ };
1019
+
1020
+ // Minimal Builder-like blocks renderer (text/image/button/columns)
1021
+ const BuilderLikeRenderer: React.FC<{ blocks?: any[] }> = ({ blocks }) => {
1022
+ if (!blocks || !Array.isArray(blocks) || blocks.length === 0) return null;
1023
+
1024
+ return (
1025
+ <div className="space-y-3">
1026
+ {blocks.map((block, idx) => {
1027
+ const type = block?.['@type'] || block?.type;
1028
+ if (!type) return null;
1029
+
1030
+ if (/text/i.test(type)) {
1031
+ const html = sanitizeHtml(block?.text || block?.data?.text || '');
1032
+ return (
1033
+ <div
1034
+ key={idx}
1035
+ className="max-w-none text-gray-800"
1036
+ dangerouslySetInnerHTML={{ __html: html }}
1037
+ />
1038
+ );
1039
+ }
1040
+
1041
+ if (/image/i.test(type)) {
1042
+ const src = block?.src || block?.image || block?.data?.src;
1043
+ const alt = block?.alt || block?.data?.alt || '';
1044
+ if (!src) return null;
1045
+ return (
1046
+ <div key={idx} className="rounded-lg overflow-hidden border border-gray-200">
1047
+ <img className="w-full h-auto" src={src} alt={alt} />
1048
+ </div>
1049
+ );
1050
+ }
1051
+
1052
+ if (/button/i.test(type)) {
1053
+ const text = block?.text || block?.data?.text || 'Button';
1054
+ const href = block?.href || block?.link || block?.data?.href || '#';
1055
+ return (
1056
+ <a
1057
+ key={idx}
1058
+ href={href}
1059
+ target="_blank"
1060
+ rel="noopener noreferrer"
1061
+ className="inline-flex items-center px-3 py-1.5 rounded-md bg-blue-600 text-white text-sm hover:bg-blue-700"
1062
+ >
1063
+ {text}
1064
+ </a>
1065
+ );
1066
+ }
1067
+
1068
+ if (/columns?/i.test(type)) {
1069
+ const cols = block?.columns || block?.data?.columns || [];
1070
+ return (
1071
+ <div key={idx} className="grid grid-cols-1 sm:grid-cols-2 gap-3">
1072
+ {cols.map((col: any, colIdx: number) => (
1073
+ <div key={colIdx} className="space-y-2">
1074
+ <BuilderLikeRenderer blocks={col?.blocks || col?.children} />
1075
+ </div>
1076
+ ))}
1077
+ </div>
1078
+ );
1079
+ }
1080
+
1081
+ return (
1082
+ <div key={idx} className="message-container">
1083
+ <pre className="text-xs text-gray-600 overflow-x-auto">{JSON.stringify(block, null, 2)}</pre>
1084
+ </div>
1085
+ );
1086
+ })}
1087
+ </div>
1088
+ );
1089
+ };
1090
+
1091
+ // Code block with optional header (language + copy) and footer (copy + download) per Image 2
1092
+ const CodeBlockWithActions: React.FC<{
1093
+ content: string;
1094
+ language?: string;
1095
+ showCopyDownload?: boolean;
1096
+ }> = ({ content, language, showCopyDownload = true }) => {
1097
+ const [copied, setCopied] = React.useState(false);
1098
+
1099
+ const handleCopy = React.useCallback(() => {
1100
+ navigator.clipboard.writeText(content).then(() => {
1101
+ setCopied(true);
1102
+ setTimeout(() => setCopied(false), 2000);
1103
+ });
1104
+ }, [content]);
1105
+
1106
+ const handleDownload = React.useCallback(() => {
1107
+ const ext =
1108
+ language === 'jsx' || language === 'tsx' ? '.tsx' : language === 'js' || language === 'ts' ? '.ts' : '.txt';
1109
+ const blob = new Blob([content], { type: 'text/plain' });
1110
+ const url = URL.createObjectURL(blob);
1111
+ const a = document.createElement('a');
1112
+ a.href = url;
1113
+ a.download = `snippet${ext}`;
1114
+ a.click();
1115
+ URL.revokeObjectURL(url);
1116
+ }, [content, language]);
1117
+
1118
+ const langLabel = (language || 'code').toUpperCase();
1119
+
1120
+ /**
1121
+ * Duplicate / copy: back square offset top-left (only its top-left shows behind),
1122
+ * front square on top toward bottom-right — matches typical “two sheets” reference art.
1123
+ */
1124
+ const IconCopy = ({ className }: { className?: string }) => (
1125
+ <svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden>
1126
+ <rect
1127
+ x="3.25"
1128
+ y="3.25"
1129
+ width="10.5"
1130
+ height="10.5"
1131
+ rx="2.25"
1132
+ ry="2.25"
1133
+ stroke="currentColor"
1134
+ strokeWidth="1.5"
1135
+ strokeLinecap="round"
1136
+ strokeLinejoin="round"
1137
+ />
1138
+ <rect
1139
+ x="8.75"
1140
+ y="8.75"
1141
+ width="10.5"
1142
+ height="10.5"
1143
+ rx="2.25"
1144
+ ry="2.25"
1145
+ stroke="currentColor"
1146
+ strokeWidth="1.5"
1147
+ strokeLinecap="round"
1148
+ strokeLinejoin="round"
1149
+ />
1150
+ </svg>
1151
+ );
1152
+
1153
+ /** Arrow into tray (download) */
1154
+ const IconDownload = ({ className }: { className?: string }) => (
1155
+ <svg
1156
+ className={className}
1157
+ viewBox="0 0 24 24"
1158
+ fill="none"
1159
+ stroke="currentColor"
1160
+ strokeWidth={1.5}
1161
+ strokeLinecap="round"
1162
+ strokeLinejoin="round"
1163
+ aria-hidden
1164
+ >
1165
+ <path d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
1166
+ </svg>
1167
+ );
1168
+
1169
+ const IconCheck = ({ className }: { className?: string }) => (
1170
+ <svg
1171
+ className={className}
1172
+ viewBox="0 0 24 24"
1173
+ fill="none"
1174
+ stroke="currentColor"
1175
+ strokeWidth={1.75}
1176
+ strokeLinecap="round"
1177
+ strokeLinejoin="round"
1178
+ aria-hidden
1179
+ >
1180
+ <path d="M5 13l4 4L19 7" />
1181
+ </svg>
1182
+ );
1183
+
1184
+ const iconMuted = 'text-slate-500 hover:text-slate-600';
1185
+
1186
+ return (
1187
+ <div className="rounded-xl bg-white overflow-hidden my-3 shadow-sm ring-1 ring-slate-200/60">
1188
+ {/* Header: label + copy — padding aligned with inner code gutter (image 3) */}
1189
+ <div className="flex items-center justify-between px-4 pt-4 pb-3 bg-white">
1190
+ <span className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">{langLabel}</span>
1191
+ <button
1192
+ type="button"
1193
+ onClick={handleCopy}
1194
+ className={`p-1 rounded-md hover:bg-slate-100/80 transition-colors ${iconMuted}`}
1195
+ title="Copy"
1196
+ aria-label="Copy to clipboard"
1197
+ >
1198
+ <IconCopy className="w-[15px] h-[15px] shrink-0" />
1199
+ </button>
1200
+ </div>
1201
+ {/* Code body: white, no inner border (single flat surface inside the card) */}
1202
+ <div className="px-4 pb-4">
1203
+ <div className="overflow-x-auto bg-white py-4">
1204
+ <pre className="text-sm text-slate-900 font-mono leading-relaxed whitespace-pre-wrap break-words m-0 p-0 border-0 bg-transparent">
1205
+ <code className="bg-transparent">{content}</code>
1206
+ </pre>
1207
+ </div>
1208
+ </div>
1209
+ {showCopyDownload && (
1210
+ <div className="flex items-center justify-start gap-0.5 px-4 pb-3 pt-0 mt-2 bg-white">
1211
+ <button
1212
+ type="button"
1213
+ onClick={handleCopy}
1214
+ className={`p-1 rounded-md hover:bg-slate-100/80 transition-colors ${iconMuted}`}
1215
+ title={copied ? 'Copied' : 'Copy'}
1216
+ aria-label={copied ? 'Copied' : 'Copy to clipboard'}
1217
+ >
1218
+ {copied ? (
1219
+ <IconCheck className="w-[15px] h-[15px] text-emerald-600" />
1220
+ ) : (
1221
+ <IconCopy className="w-[15px] h-[15px] shrink-0" />
1222
+ )}
1223
+ </button>
1224
+ <button
1225
+ type="button"
1226
+ onClick={handleDownload}
1227
+ className={`p-1 rounded-md hover:bg-slate-100/80 transition-colors ${iconMuted}`}
1228
+ title="Download"
1229
+ aria-label="Download"
1230
+ >
1231
+ <IconDownload className="w-[15px] h-[15px] shrink-0" />
1232
+ </button>
1233
+ </div>
1234
+ )}
1235
+ </div>
1236
+ );
1237
+ };
1238
+
1239
+ // Infer language from file extension or content
1240
+ const inferCodeLanguage = (section: string, filename?: string): string => {
1241
+ if (filename) {
1242
+ const ext = filename.split('.').pop()?.toLowerCase();
1243
+ const map: Record<string, string> = {
1244
+ tsx: 'tsx',
1245
+ jsx: 'jsx',
1246
+ ts: 'ts',
1247
+ js: 'js',
1248
+ css: 'css',
1249
+ html: 'html',
1250
+ json: 'json',
1251
+ };
1252
+ return map[ext ?? ''] ?? ext ?? 'code';
1253
+ }
1254
+ if (/\bimport\b.*from\s+['"]react['"]/i.test(section) || /<\w+[\s>]/.test(section)) return 'jsx';
1255
+ if (/\bimport\b/.test(section) && /\.(ts|tsx)\b/.test(section)) return 'ts';
1256
+ return 'code';
1257
+ };
1258
+
1259
+ // Enhanced code formatter for raw code content
1260
+ export const CodeFormatter: React.FC<{
1261
+ content: string;
1262
+ showCopyDownload?: boolean;
1263
+ variant?: 'default' | 'user';
1264
+ /** When variant is `user`, use primary-foreground only on plan-mode bubbles (bg-primary). */
1265
+ planMode?: boolean;
1266
+ }> = ({ content, showCopyDownload = true, variant = 'default', planMode = false }) => {
1267
+ if (!content) return null;
1268
+
1269
+ // Split content by file separators and format each section
1270
+ const sections = content.split(/(---\s*\w+\.\w+\s*---)/g);
1271
+
1272
+ return (
1273
+ <div className={variant === 'user' ? '' : 'message-container'}>
1274
+ <div className="space-y-4">
1275
+ {sections.map((section, index) => {
1276
+ if (!section.trim()) return null;
1277
+
1278
+ // Check if this is a file separator
1279
+ const fileMatch = section.match(/---\s*(\w+\.\w+)\s*---/);
1280
+ if (fileMatch) {
1281
+ return (
1282
+ <div key={index} className="code-block-container">
1283
+ <div className="code-block-header">📄 {fileMatch[1]}</div>
1284
+ </div>
1285
+ );
1286
+ }
1287
+
1288
+ // Format the code content
1289
+ const lines = section.trim().split('\n');
1290
+ const isCode = lines.some(
1291
+ (line) =>
1292
+ line.includes('@tailwind') ||
1293
+ line.includes('export default') ||
1294
+ line.includes('import ') ||
1295
+ line.includes('{') ||
1296
+ line.includes('}') ||
1297
+ line.includes('//') ||
1298
+ line.includes('/*'),
1299
+ );
1300
+
1301
+ if (isCode) {
1302
+ const lang = inferCodeLanguage(section);
1303
+ return (
1304
+ <CodeBlockWithActions
1305
+ key={index}
1306
+ content={section.trim()}
1307
+ language={lang}
1308
+ showCopyDownload={showCopyDownload}
1309
+ />
1310
+ );
1311
+ }
1312
+
1313
+ // Regular text content
1314
+ return (
1315
+ <div
1316
+ key={index}
1317
+ className={`${variant === 'user' ? USER_MESSAGE_TEXT_SIZE_CLASS : 'text-sm'} ${
1318
+ variant === 'user'
1319
+ ? planMode
1320
+ ? 'text-primary-foreground'
1321
+ : 'text-gray-900'
1322
+ : 'text-gray-700'
1323
+ } leading-relaxed whitespace-pre-wrap break-words`}
1324
+ >
1325
+ {section.trim()}
1326
+ </div>
1327
+ );
1328
+ })}
1329
+ </div>
1330
+ </div>
1331
+ );
1332
+ };
1333
+
1334
+ // Enhanced markdown renderer using marked (similar to MessageBubble.tsx)
1335
+ export const FormattedMessageContent: React.FC<{
1336
+ content: string;
1337
+ variant?: 'default' | 'user';
1338
+ /** When variant is `user`, use primary-foreground only on plan-mode bubbles (bg-primary). */
1339
+ planMode?: boolean;
1340
+ /**
1341
+ * When true, fenced code blocks use the same copy/download UI as elsewhere.
1342
+ * Implemented via react-markdown (safe) instead of raw HTML from marked.
1343
+ */
1344
+ showCopyDownloadOnCodeBlock?: boolean;
1345
+ }> = ({ content, variant = 'default', planMode = false, showCopyDownloadOnCodeBlock = false }) => {
1346
+ if (!content) return null;
1347
+
1348
+ // Parse markdown using marked - be more aggressive in detection
1349
+ const { htmlContent, markdownSource } = useMemo(() => {
1350
+ try {
1351
+ const trimmed = content.trim();
1352
+
1353
+ // Comprehensive markdown detection patterns
1354
+ const markdownIndicators = [
1355
+ /^#{1,6}\s+.+$/gm, // Headers with text
1356
+ /^\s*[-*+]\s+.+$/gm, // Unordered lists
1357
+ /^\s*\d+\.\s+.+$/gm, // Numbered lists
1358
+ /\[.+?\]\(.+?\)/g, // Links
1359
+ /!\[.+?\]\(.+?\)/g, // Images
1360
+ /```[\s\S]*?```/g, // Fenced code blocks
1361
+ /^\s*>\s+/gm, // Blockquotes
1362
+ /\*\*[^*]+\*\*/g, // Bold text
1363
+ /\*[^*\n]+\*/g, // Italic text (not just asterisks)
1364
+ /^\s*\|.+\|/gm, // Tables
1365
+ /~~.+~~/g, // Strikethrough
1366
+ ];
1367
+
1368
+ // Check if content has markdown patterns
1369
+ const hasMarkdown = markdownIndicators.some((pattern) => {
1370
+ const matches = trimmed.match(pattern);
1371
+ return matches && matches.length > 0;
1372
+ });
1373
+
1374
+ // Also check for common markdown characters that suggest markdown content
1375
+ const hasMarkdownChars = /[#*`>|\[\]()]/.test(trimmed) && trimmed.length > 10;
1376
+
1377
+ // If we detect markdown, parse it
1378
+ if (hasMarkdown || hasMarkdownChars) {
1379
+ const parsed = marked.parse(trimmed, { async: false }) as string;
1380
+ // Only return if parsing actually produced HTML (not just the same text)
1381
+ if (parsed && parsed.trim() !== trimmed && parsed.includes('<')) {
1382
+ return { htmlContent: parsed, markdownSource: trimmed as string };
1383
+ }
1384
+ }
1385
+
1386
+ return { htmlContent: null as string | null, markdownSource: null as string | null };
1387
+ } catch (error) {
1388
+ console.error('Error parsing markdown:', error);
1389
+ return { htmlContent: null as string | null, markdownSource: null as string | null };
1390
+ }
1391
+ }, [content]);
1392
+
1393
+ const userMarkdownClasses = `${USER_MESSAGE_TEXT_SIZE_CLASS} prose ${USER_MESSAGE_PROSE_SIZE_CLASS} max-w-none leading-relaxed break-words
1394
+ prose-p:my-2 prose-p:text-gray-700
1395
+ prose-pre:my-2 prose-pre:bg-transparent prose-pre:border-0 prose-pre:p-0
1396
+ prose-ul:my-2 prose-ol:my-2
1397
+ prose-code:break-words prose-code:bg-gray-50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono
1398
+ prose-headings:font-semibold prose-headings:text-gray-900 prose-headings:mt-4 prose-headings:mb-2
1399
+ prose-h1:text-2xl prose-h1:border-b prose-h1:border-gray-200 prose-h1:pb-2
1400
+ prose-h2:text-xl prose-h2:border-b prose-h2:border-gray-200 prose-h2:pb-1
1401
+ prose-h3:text-lg
1402
+ prose-h4:text-base
1403
+ prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline
1404
+ prose-strong:font-semibold prose-strong:text-gray-900
1405
+ prose-em:text-gray-600
1406
+ prose-blockquote:border-l-4 prose-blockquote:border-blue-400 prose-blockquote:pl-4 prose-blockquote:py-2 prose-blockquote:bg-blue-50 prose-blockquote:rounded-r-md prose-blockquote:my-3 prose-blockquote:italic
1407
+ prose-table:w-full prose-table:border prose-table:border-gray-200 prose-table:rounded-md prose-table:my-4
1408
+ prose-th:px-3 prose-th:py-2 prose-th:text-left prose-th:font-semibold prose-th:bg-gray-50 prose-th:border prose-th:border-gray-200
1409
+ prose-td:px-3 prose-td:py-2 prose-td:border prose-td:border-gray-200 prose-td:text-gray-700
1410
+ prose-li:my-1 prose-li:text-gray-700
1411
+ prose-hr:my-4 prose-hr:border-gray-200
1412
+ ${
1413
+ planMode
1414
+ ? '!prose-p:text-primary-foreground !prose-li:text-primary-foreground !prose-strong:text-primary-foreground !prose-em:text-primary-foreground !prose-a:text-primary-foreground'
1415
+ : '!prose-p:text-gray-900 !prose-li:text-gray-900 !prose-strong:text-gray-900 !prose-em:text-gray-700 !prose-a:text-blue-600'
1416
+ }`;
1417
+
1418
+ const userHtmlClasses = `${USER_MESSAGE_TEXT_SIZE_CLASS} prose ${USER_MESSAGE_PROSE_SIZE_CLASS} max-w-none leading-relaxed break-words
1419
+ prose-p:my-2 prose-p:text-gray-700
1420
+ prose-pre:my-2 prose-pre:bg-gray-50 prose-pre:border prose-pre:border-gray-200 prose-pre:rounded prose-pre:p-3
1421
+ prose-ul:my-2 prose-ol:my-2
1422
+ prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:overflow-x-auto
1423
+ prose-code:break-words prose-code:bg-gray-50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono
1424
+ prose-headings:font-semibold prose-headings:text-gray-900 prose-headings:mt-4 prose-headings:mb-2
1425
+ prose-h1:text-2xl prose-h1:border-b prose-h1:border-gray-200 prose-h1:pb-2
1426
+ prose-h2:text-xl prose-h2:border-b prose-h2:border-gray-200 prose-h2:pb-1
1427
+ prose-h3:text-lg
1428
+ prose-h4:text-base
1429
+ prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline
1430
+ prose-strong:font-semibold prose-strong:text-gray-900
1431
+ prose-em:text-gray-600
1432
+ prose-blockquote:border-l-4 prose-blockquote:border-blue-400 prose-blockquote:pl-4 prose-blockquote:py-2 prose-blockquote:bg-blue-50 prose-blockquote:rounded-r-md prose-blockquote:my-3 prose-blockquote:italic
1433
+ prose-table:w-full prose-table:border prose-table:border-gray-200 prose-table:rounded-md prose-table:my-4
1434
+ prose-th:px-3 prose-th:py-2 prose-th:text-left prose-th:font-semibold prose-th:bg-gray-50 prose-th:border prose-th:border-gray-200
1435
+ prose-td:px-3 prose-td:py-2 prose-td:border prose-td:border-gray-200 prose-td:text-gray-700
1436
+ prose-li:my-1 prose-li:text-gray-700
1437
+ prose-hr:my-4 prose-hr:border-gray-200
1438
+ ${
1439
+ planMode
1440
+ ? '!prose-p:text-primary-foreground !prose-li:text-primary-foreground !prose-strong:text-primary-foreground !prose-em:text-primary-foreground !prose-a:text-primary-foreground'
1441
+ : '!prose-p:text-gray-900 !prose-li:text-gray-900 !prose-strong:text-gray-900 !prose-em:text-gray-700 !prose-a:text-blue-600'
1442
+ }`;
1443
+
1444
+ const markdownComponents = useMemo(() => {
1445
+ const isUserVariant = variant === 'user';
1446
+ const userTextClass = planMode ? 'text-primary-foreground' : 'text-gray-900';
1447
+ const userMutedTextClass = planMode ? 'text-primary-foreground' : 'text-gray-700';
1448
+ const userLinkClass = planMode
1449
+ ? 'text-primary-foreground underline underline-offset-2'
1450
+ : 'text-blue-600 hover:underline underline-offset-2';
1451
+
1452
+ return {
1453
+ p: ({ children }: { children?: React.ReactNode }) =>
1454
+ isUserVariant ? (
1455
+ <p className={`break-words mb-4 last:mb-0 leading-relaxed ${userTextClass}`}>{children}</p>
1456
+ ) : (
1457
+ <p>{children}</p>
1458
+ ),
1459
+ h1: ({ children }: { children?: React.ReactNode }) =>
1460
+ isUserVariant ? (
1461
+ <h1 className={`text-2xl font-semibold mt-6 mb-3 first:mt-0 ${userTextClass}`}>{children}</h1>
1462
+ ) : (
1463
+ <h1>{children}</h1>
1464
+ ),
1465
+ h2: ({ children }: { children?: React.ReactNode }) =>
1466
+ isUserVariant ? (
1467
+ <h2 className={`text-xl font-semibold mt-5 mb-3 first:mt-0 ${userTextClass}`}>{children}</h2>
1468
+ ) : (
1469
+ <h2>{children}</h2>
1470
+ ),
1471
+ h3: ({ children }: { children?: React.ReactNode }) =>
1472
+ isUserVariant ? (
1473
+ <h3 className={`text-lg font-semibold mt-4 mb-2 first:mt-0 ${userTextClass}`}>{children}</h3>
1474
+ ) : (
1475
+ <h3>{children}</h3>
1476
+ ),
1477
+ h4: ({ children }: { children?: React.ReactNode }) =>
1478
+ isUserVariant ? (
1479
+ <h4 className={`text-base font-semibold mt-4 mb-2 first:mt-0 ${userTextClass}`}>{children}</h4>
1480
+ ) : (
1481
+ <h4>{children}</h4>
1482
+ ),
1483
+ blockquote: ({ children }: { children?: React.ReactNode }) =>
1484
+ isUserVariant ? (
1485
+ <blockquote
1486
+ className={`border-l-4 border-blue-400 pl-4 py-2 bg-blue-50 rounded-r-md my-3 italic ${userMutedTextClass}`}
1487
+ >
1488
+ {children}
1489
+ </blockquote>
1490
+ ) : (
1491
+ <blockquote>{children}</blockquote>
1492
+ ),
1493
+ hr: () => (isUserVariant ? <hr className="my-4 border-gray-200" /> : <hr />),
1494
+ ul: ({ children }: { children?: React.ReactNode }) => (
1495
+ <ul
1496
+ className={
1497
+ isUserVariant
1498
+ ? `list-disc pl-5 mb-4 space-y-1.5 marker:text-gray-500 [&_ul]:mt-1.5 [&_ol]:mt-1.5 ${userTextClass}`
1499
+ : 'list-disc pl-5'
1500
+ }
1501
+ >
1502
+ {children}
1503
+ </ul>
1504
+ ),
1505
+ ol: ({ children }: { children?: React.ReactNode }) => (
1506
+ <ol
1507
+ className={
1508
+ isUserVariant
1509
+ ? `list-decimal pl-5 mb-4 space-y-1.5 marker:text-gray-500 [&_ul]:mt-1.5 [&_ol]:mt-1.5 ${userTextClass}`
1510
+ : 'list-decimal pl-5'
1511
+ }
1512
+ >
1513
+ {children}
1514
+ </ol>
1515
+ ),
1516
+ li: ({ children }: { children?: React.ReactNode }) => (
1517
+ <li
1518
+ className={
1519
+ isUserVariant
1520
+ ? `break-words whitespace-pre-wrap leading-relaxed [&>ul]:whitespace-normal [&>ol]:whitespace-normal ${userMutedTextClass}`
1521
+ : 'break-words whitespace-pre-wrap leading-relaxed [&>ul]:whitespace-normal [&>ol]:whitespace-normal'
1522
+ }
1523
+ >
1524
+ {children}
1525
+ </li>
1526
+ ),
1527
+ a: ({ href, children }: { href?: string; children?: React.ReactNode }) =>
1528
+ isUserVariant ? (
1529
+ <a href={href} className={userLinkClass}>
1530
+ {children}
1531
+ </a>
1532
+ ) : (
1533
+ <a href={href}>{children}</a>
1534
+ ),
1535
+ table: ({
1536
+ children,
1537
+ ...props
1538
+ }: React.TableHTMLAttributes<HTMLTableElement> & { children?: React.ReactNode }) =>
1539
+ isUserVariant ? (
1540
+ <div className="overflow-x-auto my-4 w-full rounded-md border border-gray-200">
1541
+ <table className="w-full text-sm" {...props}>
1542
+ {children}
1543
+ </table>
1544
+ </div>
1545
+ ) : (
1546
+ <div className="overflow-x-auto my-4 w-full">
1547
+ <table {...props}>{children}</table>
1548
+ </div>
1549
+ ),
1550
+ thead: ({ children }: { children?: React.ReactNode }) =>
1551
+ isUserVariant ? <thead className="bg-gray-50 text-left">{children}</thead> : <thead>{children}</thead>,
1552
+ th: ({ children }: { children?: React.ReactNode }) =>
1553
+ isUserVariant ? (
1554
+ <th className={`px-3 py-2 text-left font-semibold ${userTextClass}`}>{children}</th>
1555
+ ) : (
1556
+ <th>{children}</th>
1557
+ ),
1558
+ td: ({ children }: { children?: React.ReactNode }) =>
1559
+ isUserVariant ? <td className={`px-3 py-2 ${userMutedTextClass}`}>{children}</td> : <td>{children}</td>,
1560
+ pre: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
1561
+ code: ({ className, children, ...rest }: { className?: string; children?: React.ReactNode }) => {
1562
+ const match = /language-(\w+)/.exec(className || '');
1563
+ const text = String(children).replace(/\n$/, '');
1564
+ if (match) {
1565
+ return (
1566
+ <div className="not-prose max-w-none">
1567
+ <CodeBlockWithActions
1568
+ content={text}
1569
+ language={match[1]}
1570
+ showCopyDownload={showCopyDownloadOnCodeBlock}
1571
+ />
1572
+ </div>
1573
+ );
1574
+ }
1575
+ if (text.includes('\n')) {
1576
+ return (
1577
+ <div className="not-prose max-w-none">
1578
+ <CodeBlockWithActions
1579
+ content={text}
1580
+ language="code"
1581
+ showCopyDownload={showCopyDownloadOnCodeBlock}
1582
+ />
1583
+ </div>
1584
+ );
1585
+ }
1586
+ return (
1587
+ <code
1588
+ className={
1589
+ isUserVariant
1590
+ ? `rounded px-1.5 py-0.5 text-sm font-mono bg-gray-50 break-words ${className || ''}`
1591
+ : `llm-inline-code break-words ${className || ''}`
1592
+ }
1593
+ {...rest}
1594
+ >
1595
+ {children}
1596
+ </code>
1597
+ );
1598
+ },
1599
+ };
1600
+ }, [planMode, showCopyDownloadOnCodeBlock, variant]);
1601
+
1602
+ // Fenced code copy/download needs React tree (not marked HTML)
1603
+ if (showCopyDownloadOnCodeBlock && markdownSource) {
1604
+ if (variant === 'user') {
1605
+ return (
1606
+ <div className={userMarkdownClasses}>
1607
+ <ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]} components={markdownComponents}>
1608
+ {markdownSource}
1609
+ </ReactMarkdown>
1610
+ </div>
1611
+ );
1612
+ }
1613
+
1614
+ return (
1615
+ <AssistantResponseWrapper>
1616
+ <div className={getAssistantResponseClasses('min-w-0')}>
1617
+ <ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]} components={markdownComponents}>
1618
+ {markdownSource}
1619
+ </ReactMarkdown>
1620
+ </div>
1621
+ </AssistantResponseWrapper>
1622
+ );
1623
+ }
1624
+
1625
+ // If markdown was parsed, render as HTML with prose styling
1626
+ if (htmlContent) {
1627
+ if (variant === 'user') {
1628
+ return <div className={userHtmlClasses} dangerouslySetInnerHTML={{ __html: htmlContent }} />;
1629
+ }
1630
+
1631
+ return (
1632
+ <AssistantResponseWrapper>
1633
+ <div
1634
+ className={getAssistantResponseClasses('min-w-0')}
1635
+ dangerouslySetInnerHTML={{ __html: htmlContent }}
1636
+ />
1637
+ </AssistantResponseWrapper>
1638
+ );
1639
+ }
1640
+
1641
+ // For plain text content
1642
+ return variant === 'user' ? (
1643
+ <p
1644
+ className={`${USER_MESSAGE_TEXT_SIZE_CLASS} leading-relaxed whitespace-pre-wrap break-words ${
1645
+ planMode ? 'text-primary-foreground' : 'text-gray-900'
1646
+ }`}
1647
+ >
1648
+ {content}
1649
+ </p>
1650
+ ) : (
1651
+ <AssistantResponseWrapper>
1652
+ <div className={getAssistantResponseClasses()}>
1653
+ <p className="leading-relaxed whitespace-pre-wrap break-words">{content}</p>
1654
+ </div>
1655
+ </AssistantResponseWrapper>
1656
+ );
1657
+ };
1658
+
1659
+ // Detect predominant text direction (RTL/LTR) for international content
1660
+ const detectTextDirection = (text: string): 'rtl' | 'ltr' => {
1661
+ if (!text) return 'ltr';
1662
+ const rtlRanges = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/g; // Hebrew, Arabic, etc.
1663
+ const ltrRanges = /[A-Za-z\u00C0-\u024F\u1E00-\u1EFF]/g;
1664
+ const rtlCount = (text.match(rtlRanges) || []).length;
1665
+ const ltrCount = (text.match(ltrRanges) || []).length;
1666
+ return rtlCount > ltrCount ? 'rtl' : 'ltr';
1667
+ };
1668
+
1669
+ // Lightweight language heuristic for lang attribute
1670
+ const detectLanguageTag = (text: string): string => {
1671
+ if (!text) return 'en';
1672
+ if (/\p{Script=Arabic}/u.test(text)) return 'ar';
1673
+ if (/[\u0590-\u05FF]/.test(text)) return 'he';
1674
+ if (/[\u0600-\u06FF]/.test(text)) return 'fa';
1675
+ if (/[\u0900-\u097F]/.test(text)) return 'hi';
1676
+ if (/[\u4E00-\u9FFF]/.test(text)) return 'zh';
1677
+ if (/[\u3040-\u309F\u30A0-\u30FF]/.test(text)) return 'ja';
1678
+ if (/[\uAC00-\uD7AF]/.test(text)) return 'ko';
1679
+ if (/[\u0400-\u04FF]/.test(text)) return 'ru';
1680
+ if (/[\u0E00-\u0E7F]/.test(text)) return 'th';
1681
+ return 'en';
1682
+ };
1683
+
1684
+ // Attempt to parse Builder.io-like JSON blocks embedded as message string
1685
+ const tryExtractBuilderBlocks = (content?: string): any[] | null => {
1686
+ if (!content || content.length < 2) return null;
1687
+ try {
1688
+ const trimmed = content.trim();
1689
+ const jsonMatch = trimmed.match(/([\[{][\s\S]*[\]}])/);
1690
+ const jsonText = jsonMatch ? jsonMatch[1] : trimmed;
1691
+ const parsed = JSON.parse(jsonText);
1692
+ if (Array.isArray(parsed)) {
1693
+ if (parsed.length && (parsed[0]['@type'] || parsed[0].type)) return parsed;
1694
+ } else if (parsed && typeof parsed === 'object') {
1695
+ if (Array.isArray(parsed.blocks)) return parsed.blocks;
1696
+ if (Array.isArray(parsed?.data?.blocks)) return parsed.data.blocks;
1697
+ }
1698
+ return null;
1699
+ } catch {
1700
+ return null;
1701
+ }
1702
+ };
1703
+
1704
+ const ModernMessageGroup: React.FC<MessageGroupProps> = ({
1705
+ author,
1706
+ messages,
1707
+ currentUser,
1708
+ showAuthorName = false,
1709
+ showAvatar = false,
1710
+ showTimestamp = false,
1711
+ onOpen,
1712
+ onMessageClick,
1713
+ isDesktopView = false,
1714
+ isSmallScreen = false,
1715
+ showCopyDownloadOnCodeBlock = false,
1716
+ }) => {
1717
+ // Inject CSS styles for HTML content
1718
+ useInjectStyles();
1719
+
1720
+ // Preserve server / store order for posts (TOOLUSE, THINKING, etc.); no client-side reordering
1721
+ const displayMessages = messages;
1722
+
1723
+ //const isOwnMessage = author?.id === currentUser?.id;
1724
+ //const isOwnMessage = messages.some((message: any) => (message as any)?.propsConfiguration?.contents?.role === AiAgentMessageRole.User);
1725
+ const isOwnMessage =
1726
+ displayMessages.some(
1727
+ (message: any) => (message as any)?.propsConfiguration?.contents?.role === AiAgentMessageRole.User,
1728
+ ) ||
1729
+ (Array.isArray(author?.alias) &&
1730
+ typeof currentUser?.authUserId === 'string' &&
1731
+ author.alias.some((alias: string) => alias?.toLowerCase() === currentUser.authUserId.toLowerCase()));
1732
+
1733
+ const authorName =
1734
+ author?.givenName && author?.familyName
1735
+ ? `${author.givenName} ${author.familyName}`
1736
+ : author?.username || 'Unknown User';
1737
+
1738
+ const firstMessage = displayMessages[0];
1739
+ const formatTime = (timestamp: string) => {
1740
+ const date = new Date(timestamp);
1741
+ return format(date, 'h:mm a');
1742
+ };
1743
+
1744
+ // Determine if this is an AI/system message for special styling
1745
+ const isSystemMessage =
1746
+ author?.username?.toLowerCase().includes('ai') ||
1747
+ author?.username?.toLowerCase().includes('assistant') ||
1748
+ author?.username?.toLowerCase().includes('system');
1749
+ // const isSystemMessage = messages.some(
1750
+ // (message: any) => (message as any)?.propsConfiguration?.contents?.role === AiAgentMessageRole.Assistant,
1751
+ // );
1752
+
1753
+ // For user messages, don't show group header, just individual messages with avatars
1754
+ if (isOwnMessage) {
1755
+ return (
1756
+ <div
1757
+ className={`min-h-fit ${
1758
+ isDesktopView ? 'space-y-1 mb-3' : isSmallScreen ? 'space-y-0.5 mb-2' : 'space-y-0.5 mb-2'
1759
+ }`}
1760
+ >
1761
+ {displayMessages.map((message, index) => (
1762
+ <ModernMessageBubble
1763
+ key={message.id}
1764
+ message={message}
1765
+ isOwnMessage={isOwnMessage}
1766
+ isSystemMessage={isSystemMessage}
1767
+ isFirstInGroup={index === 0}
1768
+ isLastInGroup={index === displayMessages.length - 1}
1769
+ onMessageClick={onMessageClick}
1770
+ formatTime={formatTime}
1771
+ showAuthorName={showAuthorName}
1772
+ showAvatar={showAvatar}
1773
+ showTimestamp={showTimestamp}
1774
+ showCopyDownloadOnCodeBlock={showCopyDownloadOnCodeBlock}
1775
+ />
1776
+ ))}
1777
+ </div>
1778
+ );
1779
+ }
1780
+
1781
+ // For other messages (non-user), show the traditional group layout
1782
+ return (
1783
+ <div
1784
+ className={`group transition-all duration-300 ease-in-out ${
1785
+ isDesktopView ? 'mb-4 px-4 py-3' : isSmallScreen ? 'mb-2 px-2 py-1' : 'mb-3 px-3 py-1'
1786
+ } ${isSystemMessage ? 'bg-white rounded-lg' : ' rounded-lg'}`}
1787
+ >
1788
+ <div
1789
+ className={`flex items-start ${
1790
+ isDesktopView ? 'space-x-4' : isSmallScreen ? 'space-x-2' : 'space-x-3'
1791
+ }`}
1792
+ >
1793
+ {/* Enhanced Avatar - Only show for non-system messages */}
1794
+ {/* {!isSystemMessage && (
1795
+ <div className="flex-shrink-0 mt-1">
1796
+ <div className="relative rounded-lg overflow-hidden">
1797
+ <img
1798
+ className={`cursor-pointer hover:opacity-80 transition-opacity ${
1799
+ isDesktopView ? 'w-12 h-12' : isSmallScreen ? 'w-8 h-8' : 'w-10 h-10'
1800
+ } rounded-lg object-cover`}
1801
+ src={author?.picture || '/default-avatar.svg'}
1802
+ alt={authorName}
1803
+ onClick={() => onOpen(firstMessage)}
1804
+ onError={(e) => {
1805
+ e.currentTarget.src = '/default-avatar.svg';
1806
+ }}
1807
+ />
1808
+ </div>
1809
+ </div>
1810
+ )} */}
1811
+
1812
+ <div className="flex-1 min-w-0 overflow-hidden">
1813
+ {/* Enhanced author header - Only show for non-system messages */}
1814
+ {/* {!isSystemMessage && (
1815
+ <div className="flex items-center space-x-3 mb-2">
1816
+ <span className="font-semibold truncate text-gray-900">{authorName}</span>
1817
+ </div>
1818
+ )} */}
1819
+
1820
+ {/* Enhanced Messages with rich formatting */}
1821
+ <div className="space-y-1">
1822
+ {displayMessages.map((message, index) => (
1823
+ <ModernMessageBubble
1824
+ key={message.id}
1825
+ message={message}
1826
+ isOwnMessage={isOwnMessage}
1827
+ isSystemMessage={isSystemMessage}
1828
+ isFirstInGroup={index === 0}
1829
+ isLastInGroup={index === displayMessages.length - 1}
1830
+ onMessageClick={onMessageClick}
1831
+ formatTime={formatTime}
1832
+ showTimestamp={showTimestamp}
1833
+ showCopyDownloadOnCodeBlock={showCopyDownloadOnCodeBlock}
1834
+ />
1835
+ ))}
1836
+ </div>
1837
+ </div>
1838
+ </div>
1839
+ </div>
1840
+ );
1841
+ };
1842
+
1843
+ interface ModernMessageBubbleProps {
1844
+ showAuthorName?: boolean;
1845
+ showAvatar?: boolean;
1846
+ showTimestamp?: boolean;
1847
+ message: IPost;
1848
+ isOwnMessage: boolean;
1849
+ isSystemMessage: boolean;
1850
+ isFirstInGroup: boolean;
1851
+ isLastInGroup: boolean;
1852
+ onMessageClick: (msg: IPost) => void;
1853
+ formatTime: (timestamp: string) => string;
1854
+ showCopyDownloadOnCodeBlock?: boolean;
1855
+ }
1856
+
1857
+ const ModernMessageBubble: React.FC<ModernMessageBubbleProps> = ({
1858
+ showAuthorName = false,
1859
+ showAvatar = false,
1860
+ showTimestamp = false,
1861
+ message,
1862
+ isOwnMessage,
1863
+ isSystemMessage,
1864
+ isFirstInGroup,
1865
+ isLastInGroup,
1866
+ onMessageClick,
1867
+ formatTime,
1868
+ showCopyDownloadOnCodeBlock = false,
1869
+ }) => {
1870
+ const handleClick = () => {
1871
+ onMessageClick?.(message);
1872
+ };
1873
+
1874
+ const isAssistantRole = (message as any)?.propsConfiguration?.contents?.role === AiAgentMessageRole.Assistant;
1875
+ const messageMode =
1876
+ (message as any)?.propsConfiguration?.contents?.mode ??
1877
+ (message as any)?.propsConfiguration?.content?.mode ??
1878
+ (message as any)?.props?.mode;
1879
+ const isPlanMode = messageMode === 'plan';
1880
+ // For user messages, create a right-aligned layout with avatar and name
1881
+ if (isOwnMessage) {
1882
+ const authorName =
1883
+ message.author?.givenName && message.author?.familyName
1884
+ ? `${message.author.givenName} ${message.author.familyName}`
1885
+ : message.author?.username || 'You';
1886
+
1887
+ return (
1888
+ <div className="py-1 px-1 sm:px-2 -mx-1 sm:-mx-2 group">
1889
+ <div className="flex items-start justify-end gap-2">
1890
+ <div className="flex flex-col items-end max-w-[85%]">
1891
+ {showAuthorName && (
1892
+ <div className="flex items-end space-x-2 mb-0.5">
1893
+ <span className="text-sm font-semibold text-gray-900">{authorName}</span>
1894
+ </div>
1895
+ )}
1896
+
1897
+ {/* User message bubble (Image 2) */}
1898
+ <div
1899
+ className={`${isAssistantRole ? 'text-sm' : USER_MESSAGE_TEXT_SIZE_CLASS} ${
1900
+ isAssistantRole
1901
+ ? 'cursor-pointer rounded-2xl border border-gray-200 bg-gray-50 text-gray-900 px-4 py-2.5'
1902
+ : // : 'user-message rounded-[22px] rounded-br-[4px] px-4 py-3 bg-primary text-primary-foreground shadow-sm'
1903
+ isPlanMode
1904
+ ? 'user-message rounded-[22px] rounded-br-[4px] px-4 py-3 bg-primary text-primary-foreground shadow-sm'
1905
+ : 'user-message rounded-[22px] rounded-br-[4px] px-4 py-3 '
1906
+ }`}
1907
+ onClick={isAssistantRole ? handleClick : undefined}
1908
+ dir={detectTextDirection((message as any)?.message || '')}
1909
+ lang={detectLanguageTag((message as any)?.message || '')}
1910
+ >
1911
+ {message.message && (
1912
+ <div className="max-w-none">
1913
+ {(() => {
1914
+ const contentType = detectContentType(message.message);
1915
+
1916
+ if (contentType === 'code') {
1917
+ return (
1918
+ <CodeFormatter
1919
+ content={message.message}
1920
+ showCopyDownload={showCopyDownloadOnCodeBlock}
1921
+ variant={isAssistantRole ? 'default' : 'user'}
1922
+ planMode={!isAssistantRole && isPlanMode}
1923
+ />
1924
+ );
1925
+ } else if (contentType === 'html') {
1926
+ if (isAssistantRole) {
1927
+ return (
1928
+ <AssistantResponseWrapper>
1929
+ <div
1930
+ className={getAssistantResponseClasses('min-w-0')}
1931
+ dangerouslySetInnerHTML={{
1932
+ __html: prettifyHtmlContent(
1933
+ sanitizeHtml(message.message),
1934
+ ),
1935
+ }}
1936
+ />
1937
+ </AssistantResponseWrapper>
1938
+ );
1939
+ }
1940
+
1941
+ return (
1942
+ <div
1943
+ className={
1944
+ isPlanMode
1945
+ ? '!text-primary-foreground html-content'
1946
+ : 'text-gray-900 html-content'
1947
+ }
1948
+ dangerouslySetInnerHTML={{
1949
+ __html: prettifyHtmlContent(sanitizeHtml(message.message)),
1950
+ }}
1951
+ />
1952
+ );
1953
+ } else {
1954
+ return (
1955
+ <FormattedMessageContent
1956
+ content={message.message}
1957
+ variant={isAssistantRole ? 'default' : 'user'}
1958
+ planMode={!isAssistantRole && isPlanMode}
1959
+ showCopyDownloadOnCodeBlock={showCopyDownloadOnCodeBlock}
1960
+ />
1961
+ );
1962
+ }
1963
+ })()}
1964
+ </div>
1965
+ )}
1966
+
1967
+ {tryExtractBuilderBlocks((message as any)?.message)?.length ? (
1968
+ <div className="mt-2">
1969
+ <BuilderLikeRenderer blocks={tryExtractBuilderBlocks((message as any)?.message)} />
1970
+ </div>
1971
+ ) : null}
1972
+
1973
+ {(message as any)?.props?.blocks?.length ? (
1974
+ <div className="mt-2">
1975
+ <BuilderLikeRenderer blocks={(message as any).props.blocks} />
1976
+ </div>
1977
+ ) : null}
1978
+
1979
+ {message.files?.totalCount > 0 && (
1980
+ <div className="mt-2">
1981
+ <FilesList uploaded files={message.files.data} />
1982
+ </div>
1983
+ )}
1984
+ {showTimestamp ? (
1985
+ <div
1986
+ className={`text-[10px] mt-1 text-right ${
1987
+ isAssistantRole
1988
+ ? 'text-gray-500'
1989
+ : isPlanMode
1990
+ ? 'text-primary-foreground/80'
1991
+ : 'text-gray-500'
1992
+ }`}
1993
+ >
1994
+ {formatTime(message.createdAt)}
1995
+ </div>
1996
+ ) : null}
1997
+ </div>
1998
+ </div>
1999
+
2000
+ {/* User avatar on the right */}
2001
+ {showAvatar && (
2002
+ <div className="flex-shrink-0 mt-0.5">
2003
+ <img
2004
+ className={`w-8 h-8 sm:w-10 sm:h-10 rounded-lg ${
2005
+ isAssistantRole ? '' : 'cursor-pointer hover:opacity-80'
2006
+ } transition-opacity object-cover`}
2007
+ src={message.author?.picture || '/default-avatar.svg'}
2008
+ alt={authorName}
2009
+ //onClick={isAssistantRole ? undefined : handleClick}
2010
+ onClick={isAssistantRole ? handleClick : undefined}
2011
+ onError={(e) => {
2012
+ e.currentTarget.src = '/default-avatar.svg';
2013
+ }}
2014
+ />
2015
+ </div>
2016
+ )}
2017
+ </div>
2018
+ </div>
2019
+ );
2020
+ }
2021
+
2022
+ // For other messages (left-aligned) — use Slot for streaming types (THINKING, TOOLUSE, etc.) like Kimi
2023
+ const attachment =
2024
+ (message as any)?.props?.attachment ?? (message as any)?.propsConfiguration?.contents?.attachment;
2025
+ const slotName = getMessageSlotName((message as any)?.type);
2026
+ // Don't show THINKING slot when attachment has no meaningful content or when dismissed
2027
+ const isThinkingSlot = slotName === MessengerSlotFillNameEnum.THINKING_NAME;
2028
+ const shouldShowThinkingSlot =
2029
+ !!attachment &&
2030
+ attachment.dismissed !== true &&
2031
+ (attachment.isThinking === true || (attachment.thought != null && String(attachment.thought).trim() !== ''));
2032
+ const shouldShowSlot = isThinkingSlot ? shouldShowThinkingSlot : !!attachment;
2033
+ const hasStreamingSlot = !!slotName && shouldShowSlot;
2034
+ return (
2035
+ <div
2036
+ className={`group/message w-full min-w-0 px-0 py-2 ${
2037
+ isSystemMessage || isAssistantRole ? 'cursor-pointer' : ''
2038
+ }`}
2039
+ onClick={isSystemMessage || isAssistantRole ? handleClick : undefined}
2040
+ >
2041
+ <div className="flex items-start justify-between gap-3">
2042
+ <div className="flex-1 min-w-0">
2043
+ {/* Slot fill for streaming message types (THINKING, TOOLUSE, CODEBLOCK, SERVICE, AIASSISTANT) */}
2044
+ {slotName && shouldShowSlot && (
2045
+ <Slot
2046
+ name={slotName}
2047
+ fillProps={{
2048
+ active: true,
2049
+ message,
2050
+ index: 0,
2051
+ onMessageClick,
2052
+ }}
2053
+ />
2054
+ )}
2055
+ {/* Default content when no slot or attachment (slot returns null when active: false) */}
2056
+ {!hasStreamingSlot && message.message && (
2057
+ <div
2058
+ className={`min-w-0 relative max-w-full w-full ${
2059
+ isSystemMessage ? 'text-gray-800' : 'text-gray-900'
2060
+ }`}
2061
+ dir={detectTextDirection((message as any)?.message || '')}
2062
+ lang={detectLanguageTag((message as any)?.message || '')}
2063
+ >
2064
+ {(() => {
2065
+ const contentType = detectContentType(message.message);
2066
+
2067
+ // Markdown and text both go through FormattedMessageContent
2068
+ // which will intelligently detect and render markdown
2069
+ if (contentType === 'code') {
2070
+ return (
2071
+ <CodeFormatter
2072
+ content={message.message}
2073
+ showCopyDownload={showCopyDownloadOnCodeBlock}
2074
+ />
2075
+ );
2076
+ } else if (contentType === 'html') {
2077
+ return (
2078
+ <AssistantResponseWrapper>
2079
+ <div
2080
+ className={getAssistantResponseClasses(
2081
+ 'min-w-0 relative max-w-full w-full',
2082
+ )}
2083
+ dangerouslySetInnerHTML={{
2084
+ __html: prettifyHtmlContent(sanitizeHtml(message.message)),
2085
+ }}
2086
+ />
2087
+ </AssistantResponseWrapper>
2088
+ );
2089
+ } else {
2090
+ // Handle both 'markdown' and 'text' - FormattedMessageContent
2091
+ // will detect markdown patterns and render accordingly
2092
+ return (
2093
+ <FormattedMessageContent
2094
+ content={message.message}
2095
+ showCopyDownloadOnCodeBlock={showCopyDownloadOnCodeBlock}
2096
+ />
2097
+ );
2098
+ }
2099
+ })()}
2100
+ </div>
2101
+ )}
2102
+
2103
+ {!hasStreamingSlot && tryExtractBuilderBlocks((message as any)?.message)?.length ? (
2104
+ <div className="mt-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
2105
+ <BuilderLikeRenderer blocks={tryExtractBuilderBlocks((message as any)?.message)} />
2106
+ </div>
2107
+ ) : null}
2108
+
2109
+ {!hasStreamingSlot && (message as any)?.props?.blocks?.length ? (
2110
+ <div className="mt-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
2111
+ <BuilderLikeRenderer blocks={(message as any).props.blocks} />
2112
+ </div>
2113
+ ) : null}
2114
+
2115
+ {!hasStreamingSlot && message.files?.totalCount > 0 ? (
2116
+ <div className="mt-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
2117
+ <FilesList uploaded files={message.files.data} />
2118
+ </div>
2119
+ ) : null}
2120
+ </div>
2121
+
2122
+ {/* Action buttons on hover - Only show for non-system messages */}
2123
+ {/* {!isSystemMessage && (
2124
+ <div className="flex-shrink-0 mt-1">
2125
+ <div className="opacity-0 group-hover/message:opacity-100 transition-opacity duration-200 flex space-x-1">
2126
+ <button className="p-1 hover:bg-gray-200 rounded transition-colors">
2127
+ <svg
2128
+ className="w-3 h-3 text-gray-400"
2129
+ fill="none"
2130
+ stroke="currentColor"
2131
+ viewBox="0 0 24 24"
2132
+ >
2133
+ <path
2134
+ strokeLinecap="round"
2135
+ strokeLinejoin="round"
2136
+ strokeWidth={2}
2137
+ d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z"
2138
+ />
2139
+ </svg>
2140
+ </button>
2141
+ <button className="p-1 hover:bg-gray-200 rounded transition-colors">
2142
+ <svg
2143
+ className="w-3 h-3 text-gray-400"
2144
+ fill="none"
2145
+ stroke="currentColor"
2146
+ viewBox="0 0 24 24"
2147
+ >
2148
+ <path
2149
+ strokeLinecap="round"
2150
+ strokeLinejoin="round"
2151
+ strokeWidth={2}
2152
+ d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
2153
+ />
2154
+ </svg>
2155
+ </button>
2156
+ </div>
2157
+ </div>
2158
+ )} */}
2159
+ </div>
2160
+ {showTimestamp ? (
2161
+ <div className="text-[10px] text-gray-500 mt-1 text-right">{formatTime(message.createdAt)}</div>
2162
+ ) : null}
2163
+ </div>
2164
+ );
2165
+ };
2166
+
2167
+ export const ModernMessageGroupComponent: React.FC<ModernMessageGroupProps> = ({
2168
+ messages,
2169
+ currentUser,
2170
+ showAuthorName = false,
2171
+ showAvatar = false,
2172
+ showTimestamp = false,
2173
+ onOpen,
2174
+ onMessageClick,
2175
+ isDesktopView = false,
2176
+ isSmallScreen = false,
2177
+ sandboxErrors = [],
2178
+ currentFiles = {},
2179
+ onFixError,
2180
+ showCopyDownloadOnCodeBlock = false,
2181
+ }) => {
2182
+ // Inject CSS styles for HTML content
2183
+ useInjectStyles();
2184
+
2185
+ // Filter out non-message items (like date strings)
2186
+ const actualMessages = messages.filter((msg) => typeof msg !== 'string') as IPost[];
2187
+
2188
+ // Group messages by user and time
2189
+ const messageGroups = groupMessagesByUserAndTime(actualMessages);
2190
+
2191
+ return (
2192
+ <div className={`min-h-fit ${isDesktopView ? 'space-y-6' : isSmallScreen ? 'space-y-4' : 'space-y-5'}`}>
2193
+ {messageGroups.map((group, groupIndex) => {
2194
+ const author = group[0]?.author;
2195
+ return (
2196
+ <ModernMessageGroup
2197
+ key={`group-${groupIndex}-${group[0]?.id}`}
2198
+ author={author}
2199
+ messages={group}
2200
+ currentUser={currentUser}
2201
+ onOpen={onOpen}
2202
+ onMessageClick={onMessageClick}
2203
+ isDesktopView={isDesktopView}
2204
+ isSmallScreen={isSmallScreen}
2205
+ showAuthorName={showAuthorName}
2206
+ showAvatar={showAvatar}
2207
+ showTimestamp={showTimestamp}
2208
+ showCopyDownloadOnCodeBlock={showCopyDownloadOnCodeBlock}
2209
+ />
2210
+ );
2211
+ })}
2212
+
2213
+ {sandboxErrors?.map((error) => (
2214
+ <div key={error.id} className="px-2">
2215
+ <ErrorFixCard
2216
+ error={error}
2217
+ onFixError={onFixError || (() => Promise.resolve())}
2218
+ currentFiles={currentFiles}
2219
+ isFixing={false}
2220
+ />
2221
+ </div>
2222
+ ))}
2223
+ </div>
2224
+ );
2225
+ };