@messenger-box/tailwind-ui-inbox 10.0.3-alpha.71 → 10.0.3-alpha.72

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 (148) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/components/AIAgent/AIAgent.d.ts +14 -0
  3. package/lib/components/AIAgent/AIAgent.d.ts.map +1 -0
  4. package/lib/components/AIAgent/AIAgent.js +1148 -0
  5. package/lib/components/AIAgent/AIAgent.js.map +1 -0
  6. package/lib/components/AIAgent/index.d.ts +2 -0
  7. package/lib/components/AIAgent/index.d.ts.map +1 -0
  8. package/lib/components/InboxMessage/InputComponent.d.ts +9 -0
  9. package/lib/components/InboxMessage/InputComponent.d.ts.map +1 -0
  10. package/lib/components/InboxMessage/InputComponent.js +210 -0
  11. package/lib/components/InboxMessage/InputComponent.js.map +1 -0
  12. package/lib/components/InboxMessage/MessageInput.d.ts.map +1 -1
  13. package/lib/components/InboxMessage/MessageInput.js +14 -10
  14. package/lib/components/InboxMessage/MessageInput.js.map +1 -1
  15. package/lib/components/InboxMessage/MessageInputComponent.d.ts +9 -0
  16. package/lib/components/InboxMessage/MessageInputComponent.d.ts.map +1 -0
  17. package/lib/components/InboxMessage/MessageInputComponent.js +173 -0
  18. package/lib/components/InboxMessage/MessageInputComponent.js.map +1 -0
  19. package/lib/components/InboxMessage/Messages.d.ts.map +1 -1
  20. package/lib/components/InboxMessage/Messages.js +4 -54
  21. package/lib/components/InboxMessage/Messages.js.map +1 -1
  22. package/lib/components/InboxMessage/MessagesBuilderUi.d.ts +17 -0
  23. package/lib/components/InboxMessage/MessagesBuilderUi.d.ts.map +1 -0
  24. package/lib/components/InboxMessage/MessagesBuilderUi.js +162 -0
  25. package/lib/components/InboxMessage/MessagesBuilderUi.js.map +1 -0
  26. package/lib/components/InboxMessage/UploadImageButton.d.ts +1 -0
  27. package/lib/components/InboxMessage/UploadImageButton.d.ts.map +1 -1
  28. package/lib/components/InboxMessage/UploadImageButton.js +3 -3
  29. package/lib/components/InboxMessage/UploadImageButton.js.map +1 -1
  30. package/lib/components/InboxMessage/index.d.ts +3 -0
  31. package/lib/components/InboxMessage/index.d.ts.map +1 -1
  32. package/lib/components/InboxMessage/message-widgets/CommonMessage.d.ts.map +1 -1
  33. package/lib/components/InboxMessage/message-widgets/CommonMessage.js +11 -6
  34. package/lib/components/InboxMessage/message-widgets/CommonMessage.js.map +1 -1
  35. package/lib/components/InboxMessage/message-widgets/ModernMessageGroup.d.ts +14 -0
  36. package/lib/components/InboxMessage/message-widgets/ModernMessageGroup.d.ts.map +1 -0
  37. package/lib/components/InboxMessage/message-widgets/ModernMessageGroup.js +1525 -0
  38. package/lib/components/InboxMessage/message-widgets/ModernMessageGroup.js.map +1 -0
  39. package/lib/components/InboxMessage/message-widgets/PlainMessage.d.ts.map +1 -1
  40. package/lib/components/InboxMessage/message-widgets/PlainMessage.js +6 -3
  41. package/lib/components/InboxMessage/message-widgets/PlainMessage.js.map +1 -1
  42. package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.d.ts.map +1 -1
  43. package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.js +207 -12
  44. package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.js.map +1 -1
  45. package/lib/components/InboxMessage/message-widgets/index.d.ts +1 -0
  46. package/lib/components/InboxMessage/message-widgets/index.d.ts.map +1 -1
  47. package/lib/components/index.d.ts +2 -1
  48. package/lib/components/index.d.ts.map +1 -1
  49. package/lib/compute.d.ts.map +1 -1
  50. package/lib/compute.js +79 -3
  51. package/lib/compute.js.map +1 -1
  52. package/lib/config/env-config.d.ts +6 -0
  53. package/lib/config/env-config.d.ts.map +1 -1
  54. package/lib/config/env-config.js +19 -1
  55. package/lib/config/env-config.js.map +1 -1
  56. package/lib/container/AiInbox.d.ts +15 -0
  57. package/lib/container/AiInbox.d.ts.map +1 -0
  58. package/lib/container/AiInbox.js +1520 -0
  59. package/lib/container/AiInbox.js.map +1 -0
  60. package/lib/container/AiInboxWithLoader.d.ts +36 -0
  61. package/lib/container/AiInboxWithLoader.d.ts.map +1 -0
  62. package/lib/container/AiInboxWithLoader.js +300 -0
  63. package/lib/container/AiInboxWithLoader.js.map +1 -0
  64. package/lib/container/AiLandingInput.d.ts +4 -0
  65. package/lib/container/AiLandingInput.d.ts.map +1 -0
  66. package/lib/container/AiLandingInput.js +164 -0
  67. package/lib/container/AiLandingInput.js.map +1 -0
  68. package/lib/container/Inbox.d.ts.map +1 -1
  69. package/lib/container/Inbox.js +6 -4
  70. package/lib/container/Inbox.js.map +1 -1
  71. package/lib/container/InboxAiMessagesLoader.d.ts +36 -0
  72. package/lib/container/InboxAiMessagesLoader.d.ts.map +1 -0
  73. package/lib/container/InboxAiMessagesLoader.js +47 -0
  74. package/lib/container/InboxAiMessagesLoader.js.map +1 -0
  75. package/lib/container/InboxContainer.d.ts +12 -0
  76. package/lib/container/InboxContainer.d.ts.map +1 -0
  77. package/lib/container/InboxContainer.js +31 -0
  78. package/lib/container/InboxContainer.js.map +1 -0
  79. package/lib/container/InboxTemplate1.d.ts +15 -0
  80. package/lib/container/InboxTemplate1.d.ts.map +1 -0
  81. package/lib/container/InboxTemplate1.js +1375 -0
  82. package/lib/container/InboxTemplate1.js.map +1 -0
  83. package/lib/container/InboxTemplate1WithLoader.d.ts +36 -0
  84. package/lib/container/InboxTemplate1WithLoader.d.ts.map +1 -0
  85. package/lib/container/InboxTemplate2.d.ts +15 -0
  86. package/lib/container/InboxTemplate2.d.ts.map +1 -0
  87. package/lib/container/InboxTemplate2.js +1426 -0
  88. package/lib/container/InboxTemplate2.js.map +1 -0
  89. package/lib/container/InboxWithAiLoader.d.ts +15 -0
  90. package/lib/container/InboxWithAiLoader.d.ts.map +1 -0
  91. package/lib/container/InboxWithAiLoader.js +56 -0
  92. package/lib/container/InboxWithAiLoader.js.map +1 -0
  93. package/lib/container/ServiceInbox.js +1 -1
  94. package/lib/container/ServiceInbox.js.map +1 -1
  95. package/lib/container/ThreadMessages.js +1 -1
  96. package/lib/container/ThreadMessages.js.map +1 -1
  97. package/lib/container/ThreadMessagesInbox.js +1 -1
  98. package/lib/container/ThreadMessagesInbox.js.map +1 -1
  99. package/lib/container/Threads.js +1 -1
  100. package/lib/container/Threads.js.map +1 -1
  101. package/lib/container/index.d.ts +4 -1
  102. package/lib/container/index.d.ts.map +1 -1
  103. package/lib/index.js +1 -1
  104. package/lib/machines/aiAgentMachine.d.ts +3 -0
  105. package/lib/machines/aiAgentMachine.d.ts.map +1 -0
  106. package/lib/machines/aiAgentMachine.js +1040 -0
  107. package/lib/machines/aiAgentMachine.js.map +1 -0
  108. package/lib/machines/types.d.ts +77 -0
  109. package/lib/machines/types.d.ts.map +1 -0
  110. package/lib/routes.json +40 -0
  111. package/lib/templates/InboxWithAi.d.ts +15 -0
  112. package/lib/templates/InboxWithAi.d.ts.map +1 -0
  113. package/lib/templates/InboxWithAi.js +405 -0
  114. package/lib/templates/InboxWithAi.js.map +1 -0
  115. package/lib/templates/InboxWithAi.tsx +502 -0
  116. package/package.json +7 -5
  117. package/src/components/AIAgent/AIAgent.tsx +1351 -0
  118. package/src/components/AIAgent/README.md +82 -0
  119. package/src/components/AIAgent/index.ts +1 -0
  120. package/src/components/InboxMessage/InputComponent.tsx +263 -0
  121. package/src/components/InboxMessage/MessageInput.tsx +73 -66
  122. package/src/components/InboxMessage/MessageInputComponent.tsx +245 -0
  123. package/src/components/InboxMessage/Messages.tsx +2 -56
  124. package/src/components/InboxMessage/MessagesBuilderUi.tsx +205 -0
  125. package/src/components/InboxMessage/UploadImageButton.tsx +3 -2
  126. package/src/components/InboxMessage/index.ts +3 -0
  127. package/src/components/InboxMessage/message-widgets/CommonMessage.tsx +39 -21
  128. package/src/components/InboxMessage/message-widgets/ModernMessageGroup.tsx +1968 -0
  129. package/src/components/InboxMessage/message-widgets/PlainMessage.tsx +6 -2
  130. package/src/components/InboxMessage/message-widgets/SlackLikeMessageGroup.tsx +306 -54
  131. package/src/components/InboxMessage/message-widgets/index.ts +1 -0
  132. package/src/components/index.ts +4 -0
  133. package/src/compute.ts +83 -2
  134. package/src/config/env-config.ts +6 -0
  135. package/src/container/AiInbox.tsx +1796 -0
  136. package/src/container/AiInboxWithLoader.tsx +356 -0
  137. package/src/container/AiLandingInput.tsx +168 -0
  138. package/src/container/Inbox.tsx +8 -5
  139. package/src/container/InboxAiMessagesLoader.tsx +68 -0
  140. package/src/container/InboxContainer.tsx +35 -0
  141. package/src/container/InboxTemplate1.tsx +1542 -0
  142. package/src/container/InboxTemplate1WithLoader.tsx +338 -0
  143. package/src/container/InboxTemplate2.tsx +1606 -0
  144. package/src/container/InboxWithAiLoader.tsx +76 -0
  145. package/src/container/index.ts +15 -1
  146. package/src/machines/aiAgentMachine.ts +1248 -0
  147. package/src/machines/types.ts +59 -0
  148. package/src/templates/InboxWithAi.tsx +502 -0
@@ -0,0 +1,1968 @@
1
+ import React, { useEffect } from 'react';
2
+ import { format, formatDistanceToNow, differenceInMinutes } from 'date-fns';
3
+ import { IAuthUser, IPost } from 'common';
4
+ import { FilesList } from '../../inbox';
5
+
6
+ // Enhanced CSS styles for HTML content rendering with prettification
7
+ const htmlContentStyles = `
8
+ .html-content {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
10
+ line-height: 1.6;
11
+ color: #374151;
12
+ }
13
+
14
+ .html-content h1, .html-content h2, .html-content h3, .html-content h4, .html-content h5, .html-content h6 {
15
+ margin-top: 1.5rem;
16
+ margin-bottom: 1rem;
17
+ font-weight: 600;
18
+ line-height: 1.25;
19
+ color: #111827;
20
+ }
21
+
22
+ .html-content h1 {
23
+ font-size: 1.875rem;
24
+ border-bottom: 2px solid #e5e7eb;
25
+ padding-bottom: 0.5rem;
26
+ }
27
+
28
+ .html-content h2 {
29
+ font-size: 1.5rem;
30
+ border-bottom: 1px solid #e5e7eb;
31
+ padding-bottom: 0.375rem;
32
+ }
33
+
34
+ .html-content h3 {
35
+ font-size: 1.25rem;
36
+ }
37
+
38
+ .html-content p {
39
+ margin-bottom: 1rem;
40
+ line-height: 1.7;
41
+ }
42
+
43
+ .html-content ul, .html-content ol {
44
+ margin-bottom: 1rem;
45
+ padding-left: 1.5rem;
46
+ }
47
+
48
+ .html-content ul {
49
+ list-style-type: disc;
50
+ }
51
+
52
+ .html-content ol {
53
+ list-style-type: decimal;
54
+ }
55
+
56
+ .html-content li {
57
+ margin-bottom: 0.5rem;
58
+ line-height: 1.6;
59
+ }
60
+
61
+ .html-content li > ul, .html-content li > ol {
62
+ margin-top: 0.5rem;
63
+ margin-bottom: 0.5rem;
64
+ }
65
+
66
+ .html-content a {
67
+ color: #2563eb;
68
+ text-decoration: none;
69
+ border-bottom: 1px solid transparent;
70
+ transition: all 0.2s ease;
71
+ }
72
+
73
+ .html-content a:hover {
74
+ color: #1d4ed8;
75
+ border-bottom-color: #1d4ed8;
76
+ }
77
+
78
+ .html-content strong, .html-content b {
79
+ font-weight: 600;
80
+ color: #111827;
81
+ }
82
+
83
+ .html-content em, .html-content i {
84
+ font-style: italic;
85
+ color: #6b7280;
86
+ }
87
+
88
+ .html-content code {
89
+ background-color: #f3f4f6;
90
+ padding: 0.25rem 0.5rem;
91
+ border-radius: 0.375rem;
92
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
93
+ font-size: 0.875rem;
94
+ color: #dc2626;
95
+ border: 1px solid #e5e7eb;
96
+ }
97
+
98
+ .html-content pre {
99
+ background-color: #f9fafb;
100
+ padding: 1rem;
101
+ border-radius: 0.5rem;
102
+ overflow-x: auto;
103
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
104
+ font-size: 0.875rem;
105
+ line-height: 1.5;
106
+ border: 1px solid #e5e7eb;
107
+ margin: 1.5rem 0;
108
+ }
109
+
110
+ .html-content pre code {
111
+ background-color: transparent;
112
+ padding: 0;
113
+ border: none;
114
+ color: #374151;
115
+ font-size: inherit;
116
+ }
117
+
118
+ .html-content blockquote {
119
+ border-left: 4px solid #3b82f6;
120
+ padding-left: 1rem;
121
+ margin: 1.5rem 0;
122
+ font-style: italic;
123
+ color: #6b7280;
124
+ background-color: #f8fafc;
125
+ padding: 1rem;
126
+ border-radius: 0.375rem;
127
+ }
128
+
129
+ .html-content blockquote p {
130
+ margin-bottom: 0;
131
+ }
132
+
133
+ .html-content table {
134
+ border-collapse: collapse;
135
+ border: 1px solid #e5e7eb;
136
+ width: 100%;
137
+ margin: 1.5rem 0;
138
+ border-radius: 0.5rem;
139
+ overflow: hidden;
140
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
141
+ }
142
+
143
+ .html-content th,
144
+ .html-content td {
145
+ border: 1px solid #e5e7eb;
146
+ padding: 0.75rem;
147
+ text-align: left;
148
+ vertical-align: top;
149
+ }
150
+
151
+ .html-content th {
152
+ background-color: #f9fafb;
153
+ font-weight: 600;
154
+ color: #111827;
155
+ border-bottom: 2px solid #e5e7eb;
156
+ }
157
+
158
+ .html-content tr:nth-child(even) {
159
+ background-color: #f9fafb;
160
+ }
161
+
162
+ .html-content tr:hover {
163
+ background-color: #f3f4f6;
164
+ }
165
+
166
+ .html-content hr {
167
+ border: none;
168
+ border-top: 2px solid #e5e7eb;
169
+ margin: 2rem 0;
170
+ }
171
+
172
+ .html-content img {
173
+ max-width: 100%;
174
+ height: auto;
175
+ border-radius: 0.5rem;
176
+ margin: 1rem 0;
177
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
178
+ }
179
+
180
+ .html-content .highlight {
181
+ background-color: #fef3c7;
182
+ padding: 0.25rem 0.5rem;
183
+ border-radius: 0.25rem;
184
+ border: 1px solid #f59e0b;
185
+ }
186
+
187
+ .html-content .info-box {
188
+ background-color: #dbeafe;
189
+ border: 1px solid #3b82f6;
190
+ border-radius: 0.5rem;
191
+ padding: 1rem;
192
+ margin: 1rem 0;
193
+ }
194
+
195
+ .html-content .warning-box {
196
+ background-color: #fef3c7;
197
+ border: 1px solid #f59e0b;
198
+ border-radius: 0.5rem;
199
+ padding: 1rem;
200
+ margin: 1rem 0;
201
+ }
202
+
203
+ .html-content .success-box {
204
+ background-color: #d1fae5;
205
+ border: 1px solid #10b981;
206
+ border-radius: 0.5rem;
207
+ padding: 1rem;
208
+ margin: 1rem 0;
209
+ }
210
+
211
+ .html-content .error-box {
212
+ background-color: #fee2e2;
213
+ border: 1px solid #ef4444;
214
+ border-radius: 0.5rem;
215
+ padding: 1rem;
216
+ margin: 1rem 0;
217
+ }
218
+
219
+ .html-content .html-heading {
220
+ margin-top: 2rem;
221
+ margin-bottom: 1rem;
222
+ font-weight: 700;
223
+ line-height: 1.2;
224
+ color: #111827;
225
+ }
226
+
227
+ .html-content .html-heading-1 {
228
+ font-size: 2rem;
229
+ border-bottom: 3px solid #3b82f6;
230
+ padding-bottom: 0.75rem;
231
+ }
232
+
233
+ .html-content .html-heading-2 {
234
+ font-size: 1.5rem;
235
+ border-bottom: 2px solid #e5e7eb;
236
+ padding-bottom: 0.5rem;
237
+ }
238
+
239
+ .html-content .html-heading-3 {
240
+ font-size: 1.25rem;
241
+ color: #374151;
242
+ }
243
+
244
+ .html-content .html-paragraph {
245
+ margin-bottom: 1.25rem;
246
+ line-height: 1.8;
247
+ color: #4b5563;
248
+ }
249
+
250
+ .html-content .html-list {
251
+ margin: 1.5rem 0;
252
+ padding-left: 2rem;
253
+ }
254
+
255
+ .html-content .html-list li {
256
+ margin-bottom: 0.75rem;
257
+ line-height: 1.7;
258
+ position: relative;
259
+ }
260
+
261
+ .html-content .html-list-unordered li::before {
262
+ content: "•";
263
+ color: #3b82f6;
264
+ font-weight: bold;
265
+ position: absolute;
266
+ left: -1.5rem;
267
+ }
268
+
269
+ .html-content .html-list-ordered {
270
+ counter-reset: list-counter;
271
+ }
272
+
273
+ .html-content .html-list-ordered li {
274
+ counter-increment: list-counter;
275
+ }
276
+
277
+ .html-content .html-list-ordered li::before {
278
+ content: counter(list-counter) ".";
279
+ color: #3b82f6;
280
+ font-weight: 600;
281
+ position: absolute;
282
+ left: -2rem;
283
+ }
284
+
285
+ .html-content .html-inline-code {
286
+ background-color: #f3f4f6;
287
+ padding: 0.25rem 0.5rem;
288
+ border-radius: 0.375rem;
289
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
290
+ font-size: 0.875rem;
291
+ color: #dc2626;
292
+ border: 1px solid #e5e7eb;
293
+ }
294
+
295
+ .html-content .html-code-block {
296
+ background-color: #1f2937;
297
+ color: #f9fafb;
298
+ padding: 1.5rem;
299
+ border-radius: 0.75rem;
300
+ overflow-x: auto;
301
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
302
+ font-size: 0.875rem;
303
+ line-height: 1.6;
304
+ border: 1px solid #374151;
305
+ margin: 2rem 0;
306
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
307
+ }
308
+
309
+ .html-content .html-blockquote {
310
+ border-left: 4px solid #3b82f6;
311
+ background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
312
+ padding: 1.5rem;
313
+ margin: 2rem 0;
314
+ border-radius: 0.5rem;
315
+ font-style: italic;
316
+ color: #475569;
317
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
318
+ }
319
+
320
+ .html-content .html-table {
321
+ border-collapse: collapse;
322
+ border: 2px solid #e5e7eb;
323
+ width: 100%;
324
+ margin: 2rem 0;
325
+ border-radius: 0.75rem;
326
+ overflow: hidden;
327
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
328
+ }
329
+
330
+ .html-content .html-table-header {
331
+ background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
332
+ color: white;
333
+ font-weight: 700;
334
+ padding: 1rem;
335
+ text-align: left;
336
+ border-bottom: 2px solid #1d4ed8;
337
+ }
338
+
339
+ .html-content .html-table-cell {
340
+ border: 1px solid #e5e7eb;
341
+ padding: 1rem;
342
+ text-align: left;
343
+ vertical-align: top;
344
+ background-color: white;
345
+ }
346
+
347
+ .html-content .html-table tr:nth-child(even) .html-table-cell {
348
+ background-color: #f9fafb;
349
+ }
350
+
351
+ .html-content .html-table tr:hover .html-table-cell {
352
+ background-color: #f3f4f6;
353
+ }
354
+
355
+ /* Enhanced code block styling */
356
+ .html-content pre {
357
+ background-color: #1f2937;
358
+ color: #f9fafb;
359
+ padding: 1.5rem;
360
+ border-radius: 0.75rem;
361
+ overflow-x: auto;
362
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
363
+ font-size: 0.875rem;
364
+ line-height: 1.6;
365
+ border: 1px solid #374151;
366
+ margin: 1.5rem 0;
367
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
368
+ }
369
+
370
+ .html-content pre code {
371
+ background-color: transparent;
372
+ padding: 0;
373
+ border: none;
374
+ color: #f9fafb;
375
+ font-size: inherit;
376
+ }
377
+
378
+ /* Code block container styling */
379
+ .html-content .code-block-container {
380
+ border-radius: 0.75rem;
381
+ overflow: hidden;
382
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
383
+ margin: 1.5rem 0;
384
+ }
385
+
386
+ .html-content .code-block-header {
387
+ background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
388
+ padding: 0.75rem 1rem;
389
+ border-bottom: 1px solid #d1d5db;
390
+ font-weight: 600;
391
+ color: #374151;
392
+ font-size: 0.875rem;
393
+ text-transform: uppercase;
394
+ letter-spacing: 0.05em;
395
+ }
396
+
397
+ .html-content .code-block-content {
398
+ background-color: #1f2937;
399
+ padding: 1.5rem;
400
+ overflow-x: auto;
401
+ }
402
+ `;
403
+
404
+ // Function to prettify HTML content
405
+ const prettifyHtmlContent = (htmlContent: string): string => {
406
+ // Add semantic classes for better styling
407
+ let prettified = htmlContent;
408
+
409
+ // Enhance headings with better styling
410
+ prettified = prettified.replace(/<h([1-6])>/g, '<h$1 class="html-heading html-heading-$1">');
411
+
412
+ // Enhance lists with better styling
413
+ prettified = prettified.replace(/<ul>/g, '<ul class="html-list html-list-unordered">');
414
+ prettified = prettified.replace(/<ol>/g, '<ol class="html-list html-list-ordered">');
415
+
416
+ // Enhance paragraphs with better spacing
417
+ prettified = prettified.replace(/<p>/g, '<p class="html-paragraph">');
418
+
419
+ // Enhance code blocks
420
+ prettified = prettified.replace(/<code>/g, '<code class="html-inline-code">');
421
+ prettified = prettified.replace(/<pre>/g, '<pre class="html-code-block">');
422
+
423
+ // Enhance blockquotes
424
+ prettified = prettified.replace(/<blockquote>/g, '<blockquote class="html-blockquote">');
425
+
426
+ // Enhance tables
427
+ prettified = prettified.replace(/<table>/g, '<table class="html-table">');
428
+ prettified = prettified.replace(/<th>/g, '<th class="html-table-header">');
429
+ prettified = prettified.replace(/<td>/g, '<td class="html-table-cell">');
430
+
431
+ // Add info boxes for certain content patterns
432
+ prettified = prettified.replace(
433
+ /<p>(Note|Tip|Info|Warning|Error):\s*(.*?)<\/p>/gi,
434
+ '<div class="html-info-box"><strong>$1:</strong> $2</div>',
435
+ );
436
+
437
+ return prettified;
438
+ };
439
+
440
+ // Hook to inject CSS styles
441
+ const useInjectStyles = () => {
442
+ useEffect(() => {
443
+ // Check if styles are already injected
444
+ if (document.getElementById('html-content-styles')) {
445
+ return;
446
+ }
447
+
448
+ // Create and inject style element
449
+ const styleElement = document.createElement('style');
450
+ styleElement.id = 'html-content-styles';
451
+ styleElement.textContent = htmlContentStyles;
452
+ document.head.appendChild(styleElement);
453
+
454
+ // Cleanup function
455
+ return () => {
456
+ const existingStyle = document.getElementById('html-content-styles');
457
+ if (existingStyle) {
458
+ existingStyle.remove();
459
+ }
460
+ };
461
+ }, []);
462
+ };
463
+
464
+ interface ModernMessageGroupProps {
465
+ messages: IPost[];
466
+ currentUser: IAuthUser;
467
+ onOpen: (element?: any) => void;
468
+ onMessageClick: (msg: IPost) => void;
469
+ isDesktopView?: boolean;
470
+ isSmallScreen?: boolean;
471
+ }
472
+
473
+ interface MessageGroupProps {
474
+ author: any;
475
+ messages: IPost[];
476
+ currentUser: IAuthUser;
477
+ onOpen: (element?: any) => void;
478
+ onMessageClick: (msg: IPost) => void;
479
+ isDesktopView?: boolean;
480
+ isSmallScreen?: boolean;
481
+ }
482
+
483
+ // Enhanced utility function to group messages by user and time
484
+ export const groupMessagesByUserAndTime = (messages: IPost[], timeThresholdMinutes = 5): IPost[][] => {
485
+ if (!messages || messages.length === 0) return [];
486
+
487
+ const groups: IPost[][] = [];
488
+ let currentGroup: IPost[] = [];
489
+ let lastMessage: IPost | null = null;
490
+
491
+ for (const message of messages) {
492
+ if (typeof message === 'string') continue; // Skip date separators
493
+
494
+ const shouldStartNewGroup =
495
+ !lastMessage ||
496
+ lastMessage.author?.id !== message.author?.id ||
497
+ differenceInMinutes(new Date(message.createdAt), new Date(lastMessage.createdAt)) > timeThresholdMinutes;
498
+
499
+ if (shouldStartNewGroup) {
500
+ if (currentGroup.length > 0) {
501
+ groups.push(currentGroup);
502
+ }
503
+ currentGroup = [message];
504
+ } else {
505
+ currentGroup.push(message);
506
+ }
507
+
508
+ lastMessage = message;
509
+ }
510
+
511
+ if (currentGroup.length > 0) {
512
+ groups.push(currentGroup);
513
+ }
514
+
515
+ return groups;
516
+ };
517
+
518
+ // Enhanced HTML detection that recognizes more HTML patterns
519
+ const isProbablyHTML = (value: string): boolean => {
520
+ if (!value) return false;
521
+
522
+ // Check for HTML tags (including self-closing tags)
523
+ const htmlTagPattern = /<\/?[a-z][\s\S]*>/i;
524
+
525
+ // Check for HTML entities
526
+ const htmlEntityPattern = /&[a-z0-9#]+;/i;
527
+
528
+ // Check for common HTML attributes
529
+ const htmlAttrPattern = /\s+[a-z-]+\s*=\s*["'][^"']*["']/i;
530
+
531
+ // Check for HTML-like structure
532
+ const htmlStructurePattern = /<[^>]+>[^<]*<\/[^>]+>/i;
533
+
534
+ return (
535
+ htmlTagPattern.test(value.trim()) ||
536
+ htmlEntityPattern.test(value.trim()) ||
537
+ htmlAttrPattern.test(value.trim()) ||
538
+ htmlStructurePattern.test(value.trim())
539
+ );
540
+ };
541
+
542
+ // Detect if a string looks like a code block
543
+ const isCodeBlock = (text: string): boolean => {
544
+ if (!text || text.length < 10) return false;
545
+
546
+ // Check for common programming patterns
547
+ const codePatterns = [
548
+ /\b(if|else|for|while|switch|case|break|continue|return|function|class|import|export|const|let|var|default)\b/,
549
+ /\b(public|private|protected|static|final|abstract|interface|extends|implements)\b/,
550
+ /\b(try|catch|finally|throw|throws)\b/,
551
+ /\b(new|this|super|null|undefined|true|false)\b/,
552
+ // Narrow punctuation: exclude parentheses, brackets, hyphen, question/colon to reduce false positives
553
+ /[{}<>;=+*/%&|^~]/,
554
+ /\b(System\.out\.println|console\.log|print|printf)\b/,
555
+ /\b(int|string|boolean|float|double|long|char|byte|short)\b/,
556
+ /\b(void|int|main|args)\b/,
557
+ /\/\/.*$/m, // Single line comments
558
+ /\/\*[\s\S]*?\*\//m, // Multi-line comments
559
+ /^\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[=:]\s*/, // Variable assignment
560
+ /^\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(.*\)\s*{/, // Function definition
561
+ /\bexport\s+default\b/, // Export default statements
562
+ /\breturn\s+\(/, // Return with JSX
563
+ /\bJSX\.Element\b/, // JSX types
564
+ ];
565
+
566
+ // Count how many patterns match
567
+ let matchCount = 0;
568
+ codePatterns.forEach((pattern) => {
569
+ if (pattern.test(text)) {
570
+ matchCount++;
571
+ }
572
+ });
573
+
574
+ // Debug logging
575
+ console.log('Code block detection for:', text.substring(0, 100) + '...');
576
+ console.log('Match count:', matchCount);
577
+ console.log('Contains export default:', /\bexport\s+default\b/.test(text));
578
+ console.log('Contains function:', /\bfunction\b/.test(text));
579
+ console.log('Contains braces:', /[{}<>;]/.test(text));
580
+
581
+ // If multiple patterns match or if it contains specific strong indicators, consider it code
582
+ const result =
583
+ matchCount >= 2 ||
584
+ /\b(if|else|for|while|function|class|export)\b/.test(text) ||
585
+ /[{}<>;]/.test(text) ||
586
+ /\bexport\s+default\b/.test(text);
587
+
588
+ console.log('Is code block:', result);
589
+ return result;
590
+ };
591
+
592
+ // Enhanced sanitizer that allows most HTML tags while maintaining security
593
+ const sanitizeHtml = (html: string): string => {
594
+ try {
595
+ if (typeof window === 'undefined' || typeof window.DOMParser === 'undefined') {
596
+ return html
597
+ .replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '')
598
+ .replace(/<style[\s\S]*?>[\s\S]*?<\/style>/gi, '')
599
+ .replace(/on\w+\s*=\s*"[^"]*"/gi, '')
600
+ .replace(/on\w+\s*=\s*'[^']*'/gi, '')
601
+ .replace(/on\w+\s*=\s*[^\s>]+/gi, '')
602
+ .replace(/javascript:/gi, '')
603
+ .replace(/data:text\/html/gi, '');
604
+ }
605
+
606
+ const parser = new window.DOMParser();
607
+ const doc = parser.parseFromString(html, 'text/html');
608
+
609
+ // Only remove the most dangerous tags
610
+ const dangerousTags = ['script', 'style'];
611
+ dangerousTags.forEach((tag) => {
612
+ doc.querySelectorAll(tag).forEach((el) => el.remove());
613
+ });
614
+
615
+ const walker = doc.createTreeWalker(doc.body, (NodeFilter as any).SHOW_ELEMENT);
616
+ let node = walker.nextNode() as Element | null;
617
+ while (node) {
618
+ Array.from(node.attributes).forEach((attr) => {
619
+ const name = attr.name.toLowerCase();
620
+ const value = (attr.value || '').toLowerCase();
621
+
622
+ // Remove event handlers and javascript protocols
623
+ if (name.startsWith('on') || value.startsWith('javascript:')) {
624
+ node?.removeAttribute(attr.name);
625
+ }
626
+
627
+ // Allow most attributes but validate URLs for src/href
628
+ if ((name === 'src' || name === 'href') && !/^https?:|^\/.*/.test(attr.value)) {
629
+ node?.removeAttribute(attr.name);
630
+ }
631
+ });
632
+ node = walker.nextNode() as Element | null;
633
+ }
634
+
635
+ return doc.body.innerHTML;
636
+ } catch (e) {
637
+ return html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '');
638
+ }
639
+ };
640
+
641
+ // Minimal Builder-like blocks renderer (text/image/button/columns)
642
+ const BuilderLikeRenderer: React.FC<{ blocks?: any[] }> = ({ blocks }) => {
643
+ if (!blocks || !Array.isArray(blocks) || blocks.length === 0) return null;
644
+
645
+ return (
646
+ <div className="space-y-3">
647
+ {blocks.map((block, idx) => {
648
+ const type = block?.['@type'] || block?.type;
649
+ if (!type) return null;
650
+
651
+ if (/text/i.test(type)) {
652
+ const html = sanitizeHtml(block?.text || block?.data?.text || '');
653
+ return (
654
+ <div
655
+ key={idx}
656
+ className="prose prose-sm max-w-none text-gray-800"
657
+ dangerouslySetInnerHTML={{ __html: html }}
658
+ />
659
+ );
660
+ }
661
+
662
+ if (/image/i.test(type)) {
663
+ const src = block?.src || block?.image || block?.data?.src;
664
+ const alt = block?.alt || block?.data?.alt || '';
665
+ if (!src) return null;
666
+ return (
667
+ <div key={idx} className="rounded-lg overflow-hidden border border-gray-200">
668
+ <img className="w-full h-auto" src={src} alt={alt} />
669
+ </div>
670
+ );
671
+ }
672
+
673
+ if (/button/i.test(type)) {
674
+ const text = block?.text || block?.data?.text || 'Button';
675
+ const href = block?.href || block?.link || block?.data?.href || '#';
676
+ return (
677
+ <a
678
+ key={idx}
679
+ href={href}
680
+ target="_blank"
681
+ rel="noopener noreferrer"
682
+ className="inline-flex items-center px-3 py-1.5 rounded-md bg-blue-600 text-white text-sm hover:bg-blue-700"
683
+ >
684
+ {text}
685
+ </a>
686
+ );
687
+ }
688
+
689
+ if (/columns?/i.test(type)) {
690
+ const cols = block?.columns || block?.data?.columns || [];
691
+ return (
692
+ <div key={idx} className="grid grid-cols-1 sm:grid-cols-2 gap-3">
693
+ {cols.map((col: any, colIdx: number) => (
694
+ <div key={colIdx} className="space-y-2">
695
+ <BuilderLikeRenderer blocks={col?.blocks || col?.children} />
696
+ </div>
697
+ ))}
698
+ </div>
699
+ );
700
+ }
701
+
702
+ return (
703
+ <pre key={idx} className="text-xs text-gray-600 bg-gray-50 p-2 rounded overflow-x-auto not-prose">
704
+ {JSON.stringify(block, null, 2)}
705
+ </pre>
706
+ );
707
+ })}
708
+ </div>
709
+ );
710
+ };
711
+
712
+ // Enhanced markdown-like renderer for rich text content
713
+ // Helper: render multiple inline lists from a single paragraph like
714
+ // "Intro A: - item1 - item2 Intro B: - item3 - item4"
715
+ const tryRenderMultipleInlineLists = (text: string): React.ReactNode[] | null => {
716
+ if (!text || !text.includes(' - ') || !text.includes(':')) return null;
717
+ // Split while keeping the section headers (ending with ":")
718
+ const segments = text.split(/([^:]+:\s*)/).filter((s) => s.length > 0);
719
+ if (segments.length < 3) return null; // need at least header + content
720
+
721
+ const nodes: React.ReactNode[] = [];
722
+ // Handle leading text before the first header
723
+ if (segments[0] && !segments[0].endsWith(':')) {
724
+ const leading = segments[0].trim();
725
+ if (leading) {
726
+ nodes.push(
727
+ <p key={`inline-leading`} className="text-gray-700 leading-relaxed">
728
+ {leading}
729
+ </p>,
730
+ );
731
+ }
732
+ }
733
+
734
+ for (let i = 1; i < segments.length; i += 2) {
735
+ const header = (segments[i] || '').trim();
736
+ const rest = (segments[i + 1] || '').trim();
737
+ if (!header.endsWith(':')) continue;
738
+ const parts = rest.split(/\s-\s+/).filter((p) => p.trim());
739
+ // When split on " - ", the first element is the tail right after header; ignore if empty
740
+ const items = parts.length && !rest.startsWith('- ') ? parts.slice(1) : parts;
741
+ if (items.length >= 2) {
742
+ nodes.push(
743
+ <div key={`inline-group-${i}`} className="space-y-2">
744
+ <p className="text-gray-900 font-semibold leading-relaxed">{header.replace(/:$/, '')}</p>
745
+ <ul className="space-y-2">
746
+ {items.map((it, idx) => (
747
+ <li key={`inline-item-${i}-${idx}`} className="flex items-start space-x-3 items-center">
748
+ <svg
749
+ className="w-5 h-5 text-black mt-0.5 flex-shrink-0"
750
+ fill="currentColor"
751
+ viewBox="0 0 20 20"
752
+ >
753
+ <path
754
+ fillRule="evenodd"
755
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
756
+ clipRule="evenodd"
757
+ />
758
+ </svg>
759
+ <span className="text-gray-700 leading-relaxed">{it.trim()}</span>
760
+ </li>
761
+ ))}
762
+ </ul>
763
+ </div>,
764
+ );
765
+ } else {
766
+ // Not enough items; treat as plain text
767
+ const combined = `${header} ${rest}`.trim();
768
+ if (combined) {
769
+ nodes.push(
770
+ <p key={`inline-fallback-${i}`} className="text-gray-700 leading-relaxed">
771
+ {combined}
772
+ </p>,
773
+ );
774
+ }
775
+ }
776
+ }
777
+
778
+ return nodes.length ? nodes : null;
779
+ };
780
+
781
+ const FormattedMessageContent: React.FC<{ content: string }> = ({ content }) => {
782
+ if (!content) return null;
783
+
784
+ // Debug logging for content
785
+ console.log('FormattedMessageContent received:', content.substring(0, 200) + '...');
786
+
787
+ // Direct check for a code-looking message (common for AI replies with fenced blocks)
788
+ if (
789
+ content.includes('export default function Home()') ||
790
+ (content.includes('export default') && content.includes('function') && content.includes('return'))
791
+ ) {
792
+ console.log('Direct code pattern match detected!');
793
+
794
+ // If content has fenced code, extract language and code, removing the fences
795
+ const fenceMatch = content.match(/```([\w-]*)\n?([\s\S]*?)```/);
796
+ let language = 'typescript';
797
+ let codeBody = content;
798
+ if (fenceMatch) {
799
+ language = (fenceMatch[1] || 'code').trim() || 'code';
800
+ codeBody = fenceMatch[2];
801
+ }
802
+
803
+ // Clean up the code content for better display
804
+ const cleanCode = codeBody
805
+ .replace(/^\s+/, '') // Remove leading whitespace
806
+ .replace(/\s+$/, '') // Remove trailing whitespace
807
+ .split('\n')
808
+ .map((line) => line.trim())
809
+ .filter((line) => line.length > 0)
810
+ .join('\n');
811
+
812
+ return (
813
+ <div className="rounded-xl border border-gray-200 bg-gray-50 overflow-hidden">
814
+ <div className="px-4 py-2 text-sm font-bold text-gray-900">{language}</div>
815
+ <div className="px-4 pb-4 overflow-x-auto">
816
+ <pre className="text-sm text-gray-900 font-mono leading-relaxed whitespace-pre-wrap not-prose">
817
+ <code>{cleanCode}</code>
818
+ </pre>
819
+ </div>
820
+ </div>
821
+ );
822
+ }
823
+
824
+ // Normalize escaped fences (\``` -> ```)
825
+ const normalizedContent = content.replace(/\\`{3}/g, '```');
826
+
827
+ // Global fenced-code parse as an early pass (handles cases where block splitting interferes)
828
+ if (normalizedContent.includes('```')) {
829
+ const parts: React.ReactNode[] = [];
830
+ const pushTextBlock = (text: string, key: string) => {
831
+ const t = (text || '').trim();
832
+ if (!t) return;
833
+ // Try to render multiple inline lists in one paragraph
834
+ const multiInline = tryRenderMultipleInlineLists(t);
835
+ if (multiInline) {
836
+ multiInline.forEach((n, idx) =>
837
+ parts.push(<React.Fragment key={`${key}-mil-${idx}`}>{n}</React.Fragment>),
838
+ );
839
+ return;
840
+ }
841
+ if (t.startsWith('# ')) {
842
+ parts.push(
843
+ <h1
844
+ key={key}
845
+ className="text-2xl font-bold text-gray-900 mb-4 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
846
+ >
847
+ {t.substring(2)}
848
+ </h1>,
849
+ );
850
+ return;
851
+ }
852
+ if (t.startsWith('## ')) {
853
+ parts.push(
854
+ <h2 key={key} className="text-xl font-semibold text-gray-800 mb-3 border-b border-gray-200 pb-2">
855
+ {t.substring(3)}
856
+ </h2>,
857
+ );
858
+ return;
859
+ }
860
+ if (t.startsWith('### ')) {
861
+ parts.push(
862
+ <h3 key={key} className="text-lg font-medium text-gray-700 mb-2">
863
+ {t.substring(4)}
864
+ </h3>,
865
+ );
866
+ return;
867
+ }
868
+ // Inline list heuristic: bullet items written on one line separated by " - "
869
+ if (!t.includes('\n') && t.includes(' - ') && t.split(' - ').length >= 3) {
870
+ const [intro, ...rawItems] = t.split(/\s-\s+/);
871
+ parts.push(
872
+ <div key={`${key}-inline-list`} className="space-y-2">
873
+ {intro.trim() ? <p className="text-gray-700 leading-relaxed">{intro.trim()}</p> : null}
874
+ <ul className="space-y-2">
875
+ {rawItems.map((item, itemIndex) => (
876
+ <li
877
+ key={`${key}-inli-${itemIndex}`}
878
+ className="flex items-start space-x-3 items-center"
879
+ >
880
+ <svg
881
+ className="w-5 h-5 text-black mt-0.5 flex-shrink-0"
882
+ fill="currentColor"
883
+ viewBox="0 0 20 20"
884
+ >
885
+ <path
886
+ fillRule="evenodd"
887
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
888
+ clipRule="evenodd"
889
+ />
890
+ </svg>
891
+ <span className="text-gray-700 leading-relaxed">{item.trim()}</span>
892
+ </li>
893
+ ))}
894
+ </ul>
895
+ </div>,
896
+ );
897
+ return;
898
+ }
899
+ if (t.startsWith('- ') || t.startsWith('• ')) {
900
+ const items = t.split('\n').filter((line) => line.trim());
901
+ parts.push(
902
+ <div key={key} className="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-r-lg">
903
+ <ul className="space-y-2">
904
+ {items.map((item, itemIndex) => (
905
+ <li key={`${key}-li-${itemIndex}`} className="flex items-start space-x-3 items-center">
906
+ <svg
907
+ className="w-5 h-5 text-black mt-0.5 flex-shrink-0"
908
+ fill="currentColor"
909
+ viewBox="0 0 20 20"
910
+ >
911
+ <path
912
+ fillRule="evenodd"
913
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
914
+ clipRule="evenodd"
915
+ />
916
+ </svg>
917
+ <span className="text-gray-700 leading-relaxed">
918
+ {item.replace(/^[\u2212\-•]\s*/, '')}
919
+ </span>
920
+ </li>
921
+ ))}
922
+ </ul>
923
+ </div>,
924
+ );
925
+ return;
926
+ }
927
+ if (isProbablyHTML(t)) {
928
+ parts.push(
929
+ <div
930
+ key={key}
931
+ className="prose prose-sm max-w-none text-gray-800"
932
+ dangerouslySetInnerHTML={{ __html: sanitizeHtml(t) }}
933
+ />,
934
+ );
935
+ return;
936
+ }
937
+ if (t.includes('**') || t.includes('`')) {
938
+ const formatted = t
939
+ .replace(
940
+ /\*\*(.*?)\*\*/g,
941
+ '<span class="font-semibold text-gray-900 bg-yellow-100 px-1 py-0.5 rounded">$1</span>',
942
+ )
943
+ .replace(
944
+ /`([^`]+)`/g,
945
+ '<code class="bg-gray-100 px-2 py-1 rounded text-sm font-mono text-gray-800">$1</code>',
946
+ );
947
+ parts.push(
948
+ <div
949
+ key={key}
950
+ className="text-gray-700 leading-relaxed"
951
+ dangerouslySetInnerHTML={{ __html: sanitizeHtml(formatted) }}
952
+ />,
953
+ );
954
+ return;
955
+ }
956
+
957
+ // Check if the text looks like a code block (contains programming keywords, syntax, etc.)
958
+ if (isCodeBlock(t)) {
959
+ // Clean up the code content for better display
960
+ const cleanCode = t
961
+ .replace(/^\s+/, '') // Remove leading whitespace
962
+ .replace(/\s+$/, '') // Remove trailing whitespace
963
+ .split('\n')
964
+ .map((line) => line.trim())
965
+ .filter((line) => line.length > 0)
966
+ .join('\n');
967
+
968
+ parts.push(
969
+ <div key={key} className="rounded-xl border border-gray-200 bg-gray-50 overflow-hidden">
970
+ <div className="px-4 py-2 text-sm font-bold text-gray-900">typescript</div>
971
+ <div className="px-4 pb-4 overflow-x-auto">
972
+ <pre className="text-sm text-gray-900 font-mono leading-relaxed whitespace-pre-wrap not-prose">
973
+ <code>{cleanCode}</code>
974
+ </pre>
975
+ </div>
976
+ </div>,
977
+ );
978
+ return;
979
+ }
980
+ parts.push(
981
+ <p key={key} className="text-gray-700 leading-relaxed whitespace-pre-wrap">
982
+ {t}
983
+ </p>,
984
+ );
985
+ };
986
+
987
+ // Improved regex to handle multiline code blocks better
988
+ const codeRegex = /```([\w-]*)\n?([\s\S]*?)```/g;
989
+ let lastIndex = 0;
990
+ let m: RegExpExecArray | null;
991
+
992
+ let foundAnyFence = false;
993
+ while ((m = codeRegex.exec(normalizedContent)) !== null) {
994
+ foundAnyFence = true;
995
+ const [full, lang, code] = m;
996
+ const before = normalizedContent.slice(lastIndex, m.index);
997
+ pushTextBlock(before, `global-before-${lastIndex}`);
998
+
999
+ const language = (lang || '').trim() || 'code';
1000
+ // Clean up the code content properly
1001
+ const cleanCode = code
1002
+ .replace(/^\s+/, '') // Remove leading whitespace
1003
+ .replace(/\s+$/, '') // Remove trailing whitespace
1004
+ .split('\n')
1005
+ .map((line) => line.trim())
1006
+ .filter((line) => line.length > 0)
1007
+ .join('\n');
1008
+
1009
+ parts.push(
1010
+ <div
1011
+ key={`global-code-${m.index}`}
1012
+ className="rounded-xl border border-gray-200 bg-gray-50 overflow-hidden shadow-sm"
1013
+ >
1014
+ <div className="px-4 py-2 text-sm font-bold text-gray-900 bg-gray-100 border-b border-gray-200">
1015
+ {language}
1016
+ </div>
1017
+ <div className="px-4 py-4 overflow-x-auto bg-white">
1018
+ <pre className="text-sm text-gray-900 font-mono leading-relaxed whitespace-pre-wrap not-prose">
1019
+ <code>{cleanCode}</code>
1020
+ </pre>
1021
+ </div>
1022
+ </div>,
1023
+ );
1024
+ lastIndex = m.index + full.length;
1025
+ }
1026
+ const after = normalizedContent.slice(lastIndex);
1027
+
1028
+ if (!foundAnyFence) {
1029
+ // Fallback: opening fence exists but no closing fence found
1030
+ const fenceIndex = normalizedContent.indexOf('```');
1031
+ const before = normalizedContent.slice(0, fenceIndex);
1032
+ const afterFence = normalizedContent.slice(fenceIndex + 3); // skip opening backticks
1033
+ // Try to read language up to end of line. If none, treat whole remainder as code.
1034
+ const firstNewline = afterFence.indexOf('\n');
1035
+ const language = firstNewline >= 0 ? afterFence.slice(0, firstNewline).trim() || 'code' : 'code';
1036
+ const rawCodeBody = firstNewline >= 0 ? afterFence.slice(firstNewline + 1) : afterFence;
1037
+ const codeBody = rawCodeBody
1038
+ .replace(/^\s+/, '')
1039
+ .replace(/\s+$/, '')
1040
+ .split('\n')
1041
+ .map((line) => line.trimEnd())
1042
+ .join('\n');
1043
+
1044
+ pushTextBlock(before, `global-fallback-before-${fenceIndex}`);
1045
+ parts.push(
1046
+ <div
1047
+ key={`global-fallback-code-${fenceIndex}`}
1048
+ className="rounded-xl border border-gray-200 bg-gray-50 overflow-hidden shadow-sm"
1049
+ >
1050
+ <div className="px-4 py-2 text-sm font-bold text-gray-900 bg-gray-100 border-b border-gray-200">
1051
+ {language}
1052
+ </div>
1053
+ <div className="px-4 py-4 overflow-x-auto bg-white">
1054
+ <pre className="text-sm text-gray-900 font-mono leading-relaxed whitespace-pre-wrap not-prose">
1055
+ <code>{codeBody}</code>
1056
+ </pre>
1057
+ </div>
1058
+ </div>,
1059
+ );
1060
+ } else {
1061
+ // If there's a dangling opening fence in the tail, render it as a fallback code block
1062
+ if (after.includes('```')) {
1063
+ const fenceIndexTail = after.indexOf('```');
1064
+ const beforeTail = after.slice(0, fenceIndexTail);
1065
+ const afterFenceTail = after.slice(fenceIndexTail + 3);
1066
+ const firstNewlineTail = afterFenceTail.indexOf('\n');
1067
+ const languageTail =
1068
+ firstNewlineTail >= 0 ? afterFenceTail.slice(0, firstNewlineTail).trim() || 'code' : 'code';
1069
+ const rawCodeTail = firstNewlineTail >= 0 ? afterFenceTail.slice(firstNewlineTail + 1) : afterFenceTail;
1070
+ const cleanTail = rawCodeTail
1071
+ .replace(/^\s+/, '')
1072
+ .replace(/\s+$/, '')
1073
+ .split('\n')
1074
+ .map((line) => line.trimEnd())
1075
+ .join('\n');
1076
+
1077
+ pushTextBlock(beforeTail, `global-after-text-${lastIndex}`);
1078
+ parts.push(
1079
+ <div
1080
+ key={`global-after-fallback-code-${lastIndex}`}
1081
+ className="rounded-xl border border-gray-200 bg-gray-50 overflow-hidden shadow-sm"
1082
+ >
1083
+ <div className="px-4 py-2 text-sm font-bold text-gray-900 bg-gray-100 border-b border-gray-200">
1084
+ {languageTail}
1085
+ </div>
1086
+ <div className="px-4 py-4 overflow-x-auto bg-white">
1087
+ <pre className="text-sm text-gray-900 font-mono leading-relaxed whitespace-pre-wrap not-prose">
1088
+ <code>{cleanTail}</code>
1089
+ </pre>
1090
+ </div>
1091
+ </div>,
1092
+ );
1093
+ } else {
1094
+ pushTextBlock(after, `global-after-${lastIndex}`);
1095
+ }
1096
+ }
1097
+ return <div className="space-y-4">{parts}</div>;
1098
+ }
1099
+
1100
+ // Split content into blocks for better formatting
1101
+ const blocks = normalizedContent.split('\n\n').filter((block) => block.trim());
1102
+
1103
+ // Debug logging for blocks
1104
+ console.log(
1105
+ 'Content blocks:',
1106
+ blocks.map((b) => b.substring(0, 100) + '...'),
1107
+ );
1108
+
1109
+ return (
1110
+ <div className="space-y-4">
1111
+ {blocks.map((block, index) => {
1112
+ const trimmedBlock = block.trim();
1113
+
1114
+ // Debug logging for individual blocks
1115
+ console.log(`Block ${index}:`, trimmedBlock.substring(0, 100) + '...');
1116
+ console.log(`Block ${index} isCodeBlock:`, isCodeBlock(trimmedBlock));
1117
+
1118
+ // If the block contains fenced code anywhere, render text parts + code parts in order
1119
+ if (trimmedBlock.includes('```')) {
1120
+ const parts: React.ReactNode[] = [];
1121
+ const pushTextBlock = (text: string, key: string) => {
1122
+ const t = (text || '').trim();
1123
+ if (!t) return;
1124
+ const multiInlineInner = tryRenderMultipleInlineLists(t);
1125
+ if (multiInlineInner) {
1126
+ multiInlineInner.forEach((n, idx) =>
1127
+ parts.push(<React.Fragment key={`${key}-mil-${idx}`}>{n}</React.Fragment>),
1128
+ );
1129
+ return;
1130
+ }
1131
+ if (t.startsWith('# ')) {
1132
+ parts.push(
1133
+ <h1
1134
+ key={key}
1135
+ className="text-2xl font-bold text-gray-900 mb-4 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
1136
+ >
1137
+ {t.substring(2)}
1138
+ </h1>,
1139
+ );
1140
+ return;
1141
+ }
1142
+ if (t.startsWith('## ')) {
1143
+ parts.push(
1144
+ <h2
1145
+ key={key}
1146
+ className="text-xl font-semibold text-gray-800 mb-3 border-b border-gray-200 pb-2"
1147
+ >
1148
+ {t.substring(3)}
1149
+ </h2>,
1150
+ );
1151
+ return;
1152
+ }
1153
+ if (t.startsWith('### ')) {
1154
+ parts.push(
1155
+ <h3 key={key} className="text-lg font-medium text-gray-700 mb-2">
1156
+ {t.substring(4)}
1157
+ </h3>,
1158
+ );
1159
+ return;
1160
+ }
1161
+ // Inline list heuristic inside block
1162
+ if (!t.includes('\n') && t.includes(' - ') && t.split(' - ').length >= 3) {
1163
+ const [intro, ...rawItems] = t.split(/\s-\s+/);
1164
+ parts.push(
1165
+ <div key={`${key}-inline-list`} className="space-y-2">
1166
+ {intro.trim() ? (
1167
+ <p className="text-gray-700 leading-relaxed">{intro.trim()}</p>
1168
+ ) : null}
1169
+ <ul className="space-y-2">
1170
+ {rawItems.map((item, itemIndex) => (
1171
+ <li
1172
+ key={`${key}-inli-${itemIndex}`}
1173
+ className="flex items-start space-x-3 items-center"
1174
+ >
1175
+ <svg
1176
+ className="w-5 h-5 text-black mt-0.5 flex-shrink-0"
1177
+ fill="currentColor"
1178
+ viewBox="0 0 20 20"
1179
+ >
1180
+ <path
1181
+ fillRule="evenodd"
1182
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
1183
+ clipRule="evenodd"
1184
+ />
1185
+ </svg>
1186
+ <span className="text-gray-700 leading-relaxed">{item.trim()}</span>
1187
+ </li>
1188
+ ))}
1189
+ </ul>
1190
+ </div>,
1191
+ );
1192
+ return;
1193
+ }
1194
+ if (t.startsWith('- ') || t.startsWith('• ')) {
1195
+ const items = t.split('\n').filter((line) => line.trim());
1196
+ parts.push(
1197
+ // <div key={key} className="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-r-lg">
1198
+ <div key={key} className=" p-4 rounded-r-lg">
1199
+ <ul className="space-y-2">
1200
+ {items.map((item, itemIndex) => (
1201
+ <li
1202
+ key={`${key}-li-${itemIndex}`}
1203
+ className="flex items-start space-x-3 items-center"
1204
+ >
1205
+ <svg
1206
+ className="w-5 h-5 text-black mt-0.5 flex-shrink-0"
1207
+ fill="currentColor"
1208
+ viewBox="0 0 20 20"
1209
+ >
1210
+ <path
1211
+ fillRule="evenodd"
1212
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
1213
+ clipRule="evenodd"
1214
+ />
1215
+ </svg>
1216
+ <span className="text-gray-700 leading-relaxed">
1217
+ {item.replace(/^[\u2212\-•]\s*/, '')}
1218
+ </span>
1219
+ </li>
1220
+ ))}
1221
+ </ul>
1222
+ </div>,
1223
+ );
1224
+ return;
1225
+ }
1226
+ if (t.includes('**') || t.includes('`')) {
1227
+ const formatted = t
1228
+ .replace(
1229
+ /\*\*(.*?)\*\*/g,
1230
+ '<span class="font-semibold text-gray-900 bg-yellow-100 px-1 py-0.5 rounded">$1</span>',
1231
+ )
1232
+ .replace(
1233
+ /`([^`]+)`/g,
1234
+ '<code class="bg-gray-100 px-2 py-1 rounded text-sm font-mono text-gray-800">$1</code>',
1235
+ );
1236
+ parts.push(
1237
+ <div
1238
+ key={key}
1239
+ className="text-gray-700 leading-relaxed"
1240
+ dangerouslySetInnerHTML={{ __html: sanitizeHtml(formatted) }}
1241
+ />,
1242
+ );
1243
+ return;
1244
+ }
1245
+
1246
+ // Check if the text looks like a code block (contains programming keywords, syntax, etc.)
1247
+ if (isCodeBlock(t)) {
1248
+ // Clean up the code content for better display
1249
+ const cleanCode = t
1250
+ .replace(/^\s+/, '') // Remove leading whitespace
1251
+ .replace(/\s+$/, '') // Remove trailing whitespace
1252
+ .split('\n')
1253
+ .map((line) => line.trim())
1254
+ .filter((line) => line.length > 0)
1255
+ .join('\n');
1256
+
1257
+ parts.push(
1258
+ <div key={key} className="rounded-xl border border-gray-200 bg-gray-50 overflow-hidden">
1259
+ <div className="px-4 py-2 text-sm font-bold text-gray-900">typescript</div>
1260
+ <div className="px-4 pb-4 overflow-x-auto">
1261
+ <pre className="text-sm text-gray-900 font-mono leading-relaxed whitespace-pre-wrap not-prose">
1262
+ <code>{cleanCode}</code>
1263
+ </pre>
1264
+ </div>
1265
+ </div>,
1266
+ );
1267
+ return;
1268
+ }
1269
+
1270
+ parts.push(
1271
+ <p key={key} className="text-gray-700 leading-relaxed whitespace-pre-wrap">
1272
+ {t}
1273
+ </p>,
1274
+ );
1275
+ };
1276
+ const codeRegex = /```([\w-]*)\n?([\s\S]*?)```/g;
1277
+ let lastIndex = 0;
1278
+ let match: RegExpExecArray | null;
1279
+
1280
+ while ((match = codeRegex.exec(trimmedBlock)) !== null) {
1281
+ const [full, lang, code] = match;
1282
+ const before = trimmedBlock.slice(lastIndex, match.index).trim();
1283
+ pushTextBlock(before, `${index}-before-${lastIndex}`);
1284
+
1285
+ const language = (lang || '').trim() || 'code';
1286
+ // Clean up the code content properly
1287
+ const cleanCode = code
1288
+ .replace(/^\s+/, '') // Remove leading whitespace
1289
+ .replace(/\s+$/, '') // Remove trailing whitespace
1290
+ .split('\n')
1291
+ .map((line) => line.trim())
1292
+ .filter((line) => line.length > 0)
1293
+ .join('\n');
1294
+
1295
+ parts.push(
1296
+ <div
1297
+ key={`${index}-code-${match.index}`}
1298
+ className="rounded-xl border border-gray-200 bg-gray-50 overflow-hidden shadow-sm"
1299
+ >
1300
+ <div className="px-4 py-2 text-sm font-bold text-gray-900 bg-gray-100 border-b border-gray-200">
1301
+ {language}
1302
+ </div>
1303
+ <div className="px-4 py-4 overflow-x-auto bg-white">
1304
+ <pre className="text-sm text-gray-900 font-mono leading-relaxed whitespace-pre-wrap not-prose">
1305
+ <code>{cleanCode}</code>
1306
+ </pre>
1307
+ </div>
1308
+ </div>,
1309
+ );
1310
+ lastIndex = match.index + full.length;
1311
+ }
1312
+
1313
+ const after = trimmedBlock.slice(lastIndex).trim();
1314
+ pushTextBlock(after, `${index}-after`);
1315
+
1316
+ return (
1317
+ <div key={index} className="space-y-3">
1318
+ {parts}
1319
+ </div>
1320
+ );
1321
+ }
1322
+
1323
+ // Raw HTML support per block
1324
+ if (isProbablyHTML(trimmedBlock)) {
1325
+ return (
1326
+ <div
1327
+ key={index}
1328
+ className="prose prose-sm max-w-none text-gray-800"
1329
+ dangerouslySetInnerHTML={{ __html: sanitizeHtml(trimmedBlock) }}
1330
+ />
1331
+ );
1332
+ }
1333
+
1334
+ // Check for different content types
1335
+ if (trimmedBlock.startsWith('# ')) {
1336
+ // Main heading
1337
+ return (
1338
+ <h1
1339
+ key={index}
1340
+ className="text-2xl font-bold text-gray-900 mb-4 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
1341
+ >
1342
+ {trimmedBlock.substring(2)}
1343
+ </h1>
1344
+ );
1345
+ } else if (trimmedBlock.startsWith('## ')) {
1346
+ // Secondary heading
1347
+ return (
1348
+ <h2
1349
+ key={index}
1350
+ className="text-xl font-semibold text-gray-800 mb-3 border-b border-gray-200 pb-2"
1351
+ >
1352
+ {trimmedBlock.substring(3)}
1353
+ </h2>
1354
+ );
1355
+ } else if (trimmedBlock.startsWith('### ')) {
1356
+ // Tertiary heading
1357
+ return (
1358
+ <h3 key={index} className="text-lg font-medium text-gray-700 mb-2">
1359
+ {trimmedBlock.substring(4)}
1360
+ </h3>
1361
+ );
1362
+ } else if (trimmedBlock.startsWith('- ') || trimmedBlock.startsWith('• ')) {
1363
+ // Bullet points
1364
+ const items = trimmedBlock.split('\n').filter((line) => line.trim());
1365
+ return (
1366
+ // <div key={index} className="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-r-lg">
1367
+ <div key={index} className=" p-4 rounded-r-lg">
1368
+ <ul className="space-y-2">
1369
+ {items.map((item, itemIndex) => (
1370
+ <li key={itemIndex} className="flex items-start space-x-3 items-center">
1371
+ <svg
1372
+ className="w-5 h-5 text-black mt-0.5 flex-shrink-0"
1373
+ fill="currentColor"
1374
+ viewBox="0 0 20 20"
1375
+ >
1376
+ <path
1377
+ fillRule="evenodd"
1378
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
1379
+ clipRule="evenodd"
1380
+ />
1381
+ </svg>
1382
+ <span className="text-gray-700 leading-relaxed">
1383
+ {item
1384
+ .replace(/^[-•]\s*/, '')
1385
+ .split(/(\*\*.*?\*\*)/)
1386
+ .map((part, partIndex) => {
1387
+ if (part.match(/\*\*.*?\*\*/)) {
1388
+ // Extract text between ** markers and make it bold
1389
+ const boldText = part.replace(/\*\*/g, '');
1390
+ return (
1391
+ <strong
1392
+ key={partIndex}
1393
+ className="font-semibold text-gray-900"
1394
+ >
1395
+ {boldText}
1396
+ </strong>
1397
+ );
1398
+ }
1399
+ return part;
1400
+ })}
1401
+ </span>
1402
+ </li>
1403
+ ))}
1404
+ </ul>
1405
+ </div>
1406
+ );
1407
+ } else if (trimmedBlock.startsWith('```')) {
1408
+ // Code block
1409
+ const codeMatch = trimmedBlock.match(/^```([\w-]*)\n?([\s\S]*?)```$/);
1410
+ if (codeMatch) {
1411
+ const [, language, codeContent] = codeMatch;
1412
+ const lang = (language || '').trim() || 'code';
1413
+ // Clean up the code content properly
1414
+ const cleanCode = codeContent
1415
+ .replace(/^\s+/, '') // Remove leading whitespace
1416
+ .replace(/\s+$/, '') // Remove trailing whitespace
1417
+ .split('\n')
1418
+ .map((line) => line.trim())
1419
+ .filter((line) => line.length > 0)
1420
+ .join('\n');
1421
+
1422
+ return (
1423
+ <div
1424
+ key={index}
1425
+ className="rounded-xl border border-gray-200 bg-gray-50 overflow-hidden shadow-sm"
1426
+ >
1427
+ <div className="px-4 py-2 text-sm font-bold text-gray-900 bg-gray-100 border-b border-gray-200">
1428
+ {lang}
1429
+ </div>
1430
+ <div className="px-4 py-4 overflow-x-auto bg-white">
1431
+ <pre className="text-sm text-gray-900 font-mono leading-relaxed whitespace-pre-wrap not-prose">
1432
+ <code>{cleanCode}</code>
1433
+ </pre>
1434
+ </div>
1435
+ </div>
1436
+ );
1437
+ }
1438
+ // Fallback for malformed code blocks
1439
+ const codeContent = trimmedBlock.replace(/^```[\w]*\n?/, '').replace(/```$/, '');
1440
+ // If the fenced code actually looks like plain bullet text, render as text instead of code
1441
+ const hyphenLines = codeContent.split('\n').filter((l) => /^[-•]/.test(l.trim())).length;
1442
+ const hasCodeSymbols =
1443
+ /[{}<>;=]/.test(codeContent) || /\b(function|class|import|export)\b/.test(codeContent);
1444
+ if (hyphenLines >= 2 && !hasCodeSymbols) {
1445
+ return (
1446
+ <p key={index} className="text-gray-700 leading-relaxed whitespace-pre-wrap">
1447
+ {codeContent}
1448
+ </p>
1449
+ );
1450
+ }
1451
+
1452
+ return (
1453
+ <div
1454
+ key={index}
1455
+ className="rounded-xl border border-gray-200 bg-gray-50 overflow-hidden shadow-sm"
1456
+ >
1457
+ <div className="px-4 py-2 text-sm font-bold text-gray-900 bg-gray-100 border-b border-gray-200">
1458
+ code
1459
+ </div>
1460
+ <div className="px-4 py-4 overflow-x-auto bg-white">
1461
+ <pre className="text-sm text-gray-900 font-mono leading-relaxed whitespace-pre-wrap not-prose">
1462
+ <code>{codeContent}</code>
1463
+ </pre>
1464
+ </div>
1465
+ </div>
1466
+ );
1467
+ } else if (trimmedBlock.startsWith('🔧 ')) {
1468
+ // Tool status message - render as bordered container
1469
+ const toolText = trimmedBlock.substring(2); // Remove the 🔧 emoji
1470
+ return (
1471
+ <div key={index} className="mt-2">
1472
+ <div className="inline-block px-3 py-1 bg-gray-100 border border-gray-300 rounded-lg text-sm text-gray-700">
1473
+ <span className="text-green-500 mr-2">🔧</span>
1474
+ {toolText}
1475
+ </div>
1476
+ </div>
1477
+ );
1478
+ } else if (trimmedBlock.includes('**') || trimmedBlock.includes('`')) {
1479
+ // Inline formatting (bold, inline code, highlights)
1480
+ const formattedContent = trimmedBlock
1481
+ .replace(
1482
+ /\*\*(.*?)\*\*/g,
1483
+ '<span class="font-semibold text-gray-900 bg-yellow-100 px-1 py-0.5 rounded">$1</span>',
1484
+ )
1485
+ .replace(
1486
+ /`([^`]+)`/g,
1487
+ '<code class="bg-gray-100 px-2 py-1 rounded text-sm font-mono text-gray-800">$1</code>',
1488
+ );
1489
+
1490
+ return (
1491
+ <div
1492
+ key={index}
1493
+ className="text-gray-700 leading-relaxed"
1494
+ dangerouslySetInnerHTML={{ __html: sanitizeHtml(formattedContent) }}
1495
+ />
1496
+ );
1497
+ } else if (isCodeBlock(trimmedBlock)) {
1498
+ // Code block detection for content that looks like code
1499
+ // Clean up the code content for better display
1500
+ const cleanCode = trimmedBlock
1501
+ .replace(/^\s+/, '') // Remove leading whitespace
1502
+ .replace(/\s+$/, '') // Remove trailing whitespace
1503
+ .split('\n')
1504
+ .map((line) => line.trim())
1505
+ .filter((line) => line.length > 0)
1506
+ .join('\n');
1507
+
1508
+ // Heuristic: if the "code" contains many leading hyphen bullets and no typical code symbols, treat as text
1509
+ const hyphenLines = cleanCode.split('\n').filter((l) => /^[-•]/.test(l.trim())).length;
1510
+ const hasCodeSymbols =
1511
+ /[{}<>;=]/.test(cleanCode) || /\b(function|class|import|export)\b/.test(cleanCode);
1512
+ if (hyphenLines >= 2 && !hasCodeSymbols) {
1513
+ return (
1514
+ <p key={index} className="text-gray-700 leading-relaxed whitespace-pre-wrap">
1515
+ {trimmedBlock}
1516
+ </p>
1517
+ );
1518
+ }
1519
+
1520
+ return (
1521
+ <div key={index} className="rounded-xl border border-gray-200 bg-gray-50 overflow-hidden">
1522
+ <div className="px-4 py-2 text-sm font-bold text-gray-900">typescript</div>
1523
+ <div className="px-4 pb-4 overflow-x-auto">
1524
+ <pre className="text-sm text-gray-900 font-mono leading-relaxed whitespace-pre-wrap not-prose">
1525
+ <code>{cleanCode}</code>
1526
+ </pre>
1527
+ </div>
1528
+ </div>
1529
+ );
1530
+ } else if (
1531
+ // Fallback: More aggressive code detection for specific patterns
1532
+ trimmedBlock.includes('export default') ||
1533
+ trimmedBlock.includes('function') ||
1534
+ trimmedBlock.includes('return (') ||
1535
+ trimmedBlock.includes('className=') ||
1536
+ trimmedBlock.includes('Route::') ||
1537
+ trimmedBlock.includes('class Post') ||
1538
+ trimmedBlock.includes('extends Model') ||
1539
+ trimmedBlock.includes('extends Controller') ||
1540
+ (trimmedBlock.includes('{') && trimmedBlock.includes('}') && trimmedBlock.includes(';'))
1541
+ ) {
1542
+ console.log('Fallback code detection triggered for block:', trimmedBlock.substring(0, 100) + '...');
1543
+
1544
+ // Clean up the code content for better display
1545
+ const cleanCode = trimmedBlock
1546
+ .replace(/^\s+/, '') // Remove leading whitespace
1547
+ .replace(/\s+$/, '') // Remove trailing whitespace
1548
+ .split('\n')
1549
+ .map((line) => line.trim())
1550
+ .filter((line) => line.length > 0)
1551
+ .join('\n');
1552
+
1553
+ return (
1554
+ <div
1555
+ key={index}
1556
+ className="rounded-xl border border-gray-200 bg-gray-50 overflow-hidden shadow-sm"
1557
+ >
1558
+ <div className="px-4 py-2 text-sm font-bold text-gray-900 bg-gray-100 border-b border-gray-200">
1559
+ code
1560
+ </div>
1561
+ <div className="px-4 py-4 overflow-x-auto bg-white">
1562
+ <pre className="text-sm text-gray-900 font-mono leading-relaxed whitespace-pre-wrap not-prose">
1563
+ <code>{cleanCode}</code>
1564
+ </pre>
1565
+ </div>
1566
+ </div>
1567
+ );
1568
+ } else {
1569
+ // Regular paragraph
1570
+ return (
1571
+ <p key={index} className="text-gray-700 leading-relaxed whitespace-pre-wrap">
1572
+ {trimmedBlock}
1573
+ </p>
1574
+ );
1575
+ }
1576
+ })}
1577
+ </div>
1578
+ );
1579
+ };
1580
+
1581
+ // Detect predominant text direction (RTL/LTR) for international content
1582
+ const detectTextDirection = (text: string): 'rtl' | 'ltr' => {
1583
+ if (!text) return 'ltr';
1584
+ const rtlRanges = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/g; // Hebrew, Arabic, etc.
1585
+ const ltrRanges = /[A-Za-z\u00C0-\u024F\u1E00-\u1EFF]/g;
1586
+ const rtlCount = (text.match(rtlRanges) || []).length;
1587
+ const ltrCount = (text.match(ltrRanges) || []).length;
1588
+ return rtlCount > ltrCount ? 'rtl' : 'ltr';
1589
+ };
1590
+
1591
+ // Lightweight language heuristic for lang attribute
1592
+ const detectLanguageTag = (text: string): string => {
1593
+ if (!text) return 'en';
1594
+ if (/\p{Script=Arabic}/u.test(text)) return 'ar';
1595
+ if (/[\u0590-\u05FF]/.test(text)) return 'he';
1596
+ if (/[\u0600-\u06FF]/.test(text)) return 'fa';
1597
+ if (/[\u0900-\u097F]/.test(text)) return 'hi';
1598
+ if (/[\u4E00-\u9FFF]/.test(text)) return 'zh';
1599
+ if (/[\u3040-\u309F\u30A0-\u30FF]/.test(text)) return 'ja';
1600
+ if (/[\uAC00-\uD7AF]/.test(text)) return 'ko';
1601
+ if (/[\u0400-\u04FF]/.test(text)) return 'ru';
1602
+ if (/[\u0E00-\u0E7F]/.test(text)) return 'th';
1603
+ return 'en';
1604
+ };
1605
+
1606
+ // Attempt to parse Builder.io-like JSON blocks embedded as message string
1607
+ const tryExtractBuilderBlocks = (content?: string): any[] | null => {
1608
+ if (!content || content.length < 2) return null;
1609
+ try {
1610
+ const trimmed = content.trim();
1611
+ const jsonMatch = trimmed.match(/([\[{][\s\S]*[\]}])/);
1612
+ const jsonText = jsonMatch ? jsonMatch[1] : trimmed;
1613
+ const parsed = JSON.parse(jsonText);
1614
+ if (Array.isArray(parsed)) {
1615
+ if (parsed.length && (parsed[0]['@type'] || parsed[0].type)) return parsed;
1616
+ } else if (parsed && typeof parsed === 'object') {
1617
+ if (Array.isArray(parsed.blocks)) return parsed.blocks;
1618
+ if (Array.isArray(parsed?.data?.blocks)) return parsed.data.blocks;
1619
+ }
1620
+ return null;
1621
+ } catch {
1622
+ return null;
1623
+ }
1624
+ };
1625
+
1626
+ const ModernMessageGroup: React.FC<MessageGroupProps> = ({
1627
+ author,
1628
+ messages,
1629
+ currentUser,
1630
+ onOpen,
1631
+ onMessageClick,
1632
+ isDesktopView = false,
1633
+ isSmallScreen = false,
1634
+ }) => {
1635
+ // Inject CSS styles for HTML content
1636
+ useInjectStyles();
1637
+
1638
+ const isOwnMessage = author?.id === currentUser?.id;
1639
+ const authorName =
1640
+ author?.givenName && author?.familyName
1641
+ ? `${author.givenName} ${author.familyName}`
1642
+ : author?.username || 'Unknown User';
1643
+
1644
+ const firstMessage = messages[0];
1645
+ const formatTime = (timestamp: string) => {
1646
+ const date = new Date(timestamp);
1647
+ return format(date, 'h:mm a');
1648
+ };
1649
+
1650
+ // Determine if this is an AI/system message for special styling
1651
+ const isSystemMessage =
1652
+ author?.username?.toLowerCase().includes('ai') ||
1653
+ author?.username?.toLowerCase().includes('assistant') ||
1654
+ author?.username?.toLowerCase().includes('system');
1655
+
1656
+ // For user messages, don't show group header, just individual messages with avatars
1657
+ if (isOwnMessage) {
1658
+ return (
1659
+ <div
1660
+ className={`min-h-fit ${
1661
+ isDesktopView ? 'space-y-1 mb-3' : isSmallScreen ? 'space-y-0.5 mb-2' : 'space-y-0.5 mb-2'
1662
+ }`}
1663
+ >
1664
+ {messages.map((message, index) => (
1665
+ <ModernMessageBubble
1666
+ key={message.id}
1667
+ message={message}
1668
+ isOwnMessage={isOwnMessage}
1669
+ isSystemMessage={isSystemMessage}
1670
+ isFirstInGroup={index === 0}
1671
+ isLastInGroup={index === messages.length - 1}
1672
+ onMessageClick={onMessageClick}
1673
+ formatTime={formatTime}
1674
+ />
1675
+ ))}
1676
+ </div>
1677
+ );
1678
+ }
1679
+
1680
+ // For other messages (non-user), show the traditional group layout
1681
+ return (
1682
+ <div
1683
+ className={`group transition-all duration-300 ease-in-out ${
1684
+ isDesktopView ? 'mb-4 px-4 py-3' : isSmallScreen ? 'mb-2 px-2 py-1' : 'mb-3 px-3 py-1'
1685
+ } ${isSystemMessage ? 'bg-white rounded-lg' : ' rounded-lg'}`}
1686
+ >
1687
+ <div
1688
+ className={`flex items-start ${
1689
+ isDesktopView ? 'space-x-4' : isSmallScreen ? 'space-x-2' : 'space-x-3'
1690
+ }`}
1691
+ >
1692
+ {/* Enhanced Avatar - Only show for non-system messages */}
1693
+ {!isSystemMessage && (
1694
+ <div className="flex-shrink-0 mt-1">
1695
+ <div className="relative rounded-lg overflow-hidden">
1696
+ <img
1697
+ className={`cursor-pointer hover:opacity-80 transition-opacity ${
1698
+ isDesktopView ? 'w-12 h-12' : isSmallScreen ? 'w-8 h-8' : 'w-10 h-10'
1699
+ } rounded-lg object-cover`}
1700
+ src={author?.picture || '/default-avatar.svg'}
1701
+ alt={authorName}
1702
+ onClick={() => onOpen(firstMessage)}
1703
+ onError={(e) => {
1704
+ e.currentTarget.src = '/default-avatar.svg';
1705
+ }}
1706
+ />
1707
+ </div>
1708
+ </div>
1709
+ )}
1710
+
1711
+ <div className="flex-1 min-w-0 overflow-hidden">
1712
+ {/* Enhanced author header - Only show for non-system messages */}
1713
+ {!isSystemMessage && (
1714
+ <div className="flex items-center space-x-3 mb-2">
1715
+ <span className="font-semibold truncate text-gray-900">{authorName}</span>
1716
+ </div>
1717
+ )}
1718
+
1719
+ {/* Enhanced Messages with rich formatting */}
1720
+ <div className="space-y-1">
1721
+ {messages.map((message, index) => (
1722
+ <ModernMessageBubble
1723
+ key={message.id}
1724
+ message={message}
1725
+ isOwnMessage={isOwnMessage}
1726
+ isSystemMessage={isSystemMessage}
1727
+ isFirstInGroup={index === 0}
1728
+ isLastInGroup={index === messages.length - 1}
1729
+ onMessageClick={onMessageClick}
1730
+ formatTime={formatTime}
1731
+ />
1732
+ ))}
1733
+ </div>
1734
+ </div>
1735
+ </div>
1736
+ </div>
1737
+ );
1738
+ };
1739
+
1740
+ interface ModernMessageBubbleProps {
1741
+ message: IPost;
1742
+ isOwnMessage: boolean;
1743
+ isSystemMessage: boolean;
1744
+ isFirstInGroup: boolean;
1745
+ isLastInGroup: boolean;
1746
+ onMessageClick: (msg: IPost) => void;
1747
+ formatTime: (timestamp: string) => string;
1748
+ }
1749
+
1750
+ const ModernMessageBubble: React.FC<ModernMessageBubbleProps> = ({
1751
+ message,
1752
+ isOwnMessage,
1753
+ isSystemMessage,
1754
+ isFirstInGroup,
1755
+ isLastInGroup,
1756
+ onMessageClick,
1757
+ formatTime,
1758
+ }) => {
1759
+ const handleClick = () => {
1760
+ onMessageClick?.(message);
1761
+ };
1762
+
1763
+ // For user messages, create a right-aligned layout with avatar and name
1764
+ if (isOwnMessage) {
1765
+ const authorName =
1766
+ message.author?.givenName && message.author?.familyName
1767
+ ? `${message.author.givenName} ${message.author.familyName}`
1768
+ : message.author?.username || 'You';
1769
+
1770
+ return (
1771
+ <div className="py-1 hover:bg-gray-50 hover:bg-opacity-50 rounded px-1 sm:px-2 -mx-1 sm:-mx-2 group">
1772
+ <div className="flex items-start justify-end gap-2">
1773
+ {/* Message content and timestamp on the left */}
1774
+ <div className="flex flex-col items-end max-w-xs lg:max-w-md">
1775
+ <div className="flex items-end space-x-2 mb-0.5">
1776
+ <span className="text-sm font-semibold text-gray-900">{authorName}</span>
1777
+ </div>
1778
+
1779
+ <div
1780
+ className="text-sm text-gray-900 cursor-pointer hover:bg-gray-100 px-1 sm:px-2 py-1 rounded"
1781
+ onClick={handleClick}
1782
+ dir={detectTextDirection((message as any)?.message || '')}
1783
+ lang={detectLanguageTag((message as any)?.message || '')}
1784
+ >
1785
+ {message.message && (
1786
+ <div className="prose prose-sm max-w-none">
1787
+ {isProbablyHTML(message.message) ? (
1788
+ <div
1789
+ className="text-gray-800 html-content"
1790
+ dangerouslySetInnerHTML={{
1791
+ __html: prettifyHtmlContent(sanitizeHtml(message.message)),
1792
+ }}
1793
+ />
1794
+ ) : (
1795
+ <FormattedMessageContent content={message.message} />
1796
+ )}
1797
+ </div>
1798
+ )}
1799
+
1800
+ {tryExtractBuilderBlocks((message as any)?.message)?.length ? (
1801
+ <div className="mt-2">
1802
+ <BuilderLikeRenderer blocks={tryExtractBuilderBlocks((message as any)?.message)} />
1803
+ </div>
1804
+ ) : null}
1805
+
1806
+ {(message as any)?.props?.blocks?.length ? (
1807
+ <div className="mt-2">
1808
+ <BuilderLikeRenderer blocks={(message as any).props.blocks} />
1809
+ </div>
1810
+ ) : null}
1811
+
1812
+ {message.files?.totalCount > 0 && (
1813
+ <div className="mt-2">
1814
+ <FilesList uploaded files={message.files.data} />
1815
+ </div>
1816
+ )}
1817
+ </div>
1818
+ </div>
1819
+
1820
+ {/* User avatar on the right */}
1821
+ <div className="flex-shrink-0 mt-0.5">
1822
+ <img
1823
+ className="w-8 h-8 sm:w-10 sm:h-10 rounded-lg cursor-pointer hover:opacity-80 transition-opacity object-cover"
1824
+ src={message.author?.picture || '/default-avatar.svg'}
1825
+ alt={authorName}
1826
+ onClick={handleClick}
1827
+ onError={(e) => {
1828
+ e.currentTarget.src = '/default-avatar.svg';
1829
+ }}
1830
+ />
1831
+ </div>
1832
+ </div>
1833
+ </div>
1834
+ );
1835
+ }
1836
+
1837
+ // For other messages (left-aligned with full formatting)
1838
+ return (
1839
+ <div
1840
+ className={`group/message transition-all duration-200 hover:bg-gray-50 rounded-lg px-3 py-1 -mx-3 ${
1841
+ isSystemMessage ? '' : 'cursor-pointer'
1842
+ }`}
1843
+ onClick={isSystemMessage ? undefined : handleClick}
1844
+ >
1845
+ <div className="flex items-start justify-between gap-3">
1846
+ <div className="flex-1 min-w-0">
1847
+ {message.message && (
1848
+ <div
1849
+ className={`prose prose-sm max-w-none ${
1850
+ isSystemMessage ? 'text-gray-800' : 'text-gray-900'
1851
+ }`}
1852
+ dir={detectTextDirection((message as any)?.message || '')}
1853
+ lang={detectLanguageTag((message as any)?.message || '')}
1854
+ >
1855
+ {/* Prefer fenced code rendering over HTML when present */}
1856
+ {message.message.includes('```') ? (
1857
+ <FormattedMessageContent content={message.message} />
1858
+ ) : isProbablyHTML(message.message) ? (
1859
+ <div
1860
+ className="prose prose-sm max-w-none text-gray-800 html-content"
1861
+ dangerouslySetInnerHTML={{
1862
+ __html: prettifyHtmlContent(sanitizeHtml(message.message)),
1863
+ }}
1864
+ />
1865
+ ) : (
1866
+ <FormattedMessageContent content={message.message} />
1867
+ )}
1868
+ </div>
1869
+ )}
1870
+
1871
+ {tryExtractBuilderBlocks((message as any)?.message)?.length ? (
1872
+ <div className="mt-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
1873
+ <BuilderLikeRenderer blocks={tryExtractBuilderBlocks((message as any)?.message)} />
1874
+ </div>
1875
+ ) : null}
1876
+
1877
+ {(message as any)?.props?.blocks?.length ? (
1878
+ <div className="mt-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
1879
+ <BuilderLikeRenderer blocks={(message as any).props.blocks} />
1880
+ </div>
1881
+ ) : null}
1882
+
1883
+ {message.files?.totalCount > 0 && (
1884
+ <div className="mt-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
1885
+ <FilesList uploaded files={message.files.data} />
1886
+ </div>
1887
+ )}
1888
+ </div>
1889
+
1890
+ {/* Action buttons on hover - Only show for non-system messages */}
1891
+ {!isSystemMessage && (
1892
+ <div className="flex-shrink-0 mt-1">
1893
+ <div className="opacity-0 group-hover/message:opacity-100 transition-opacity duration-200 flex space-x-1">
1894
+ <button className="p-1 hover:bg-gray-200 rounded transition-colors">
1895
+ <svg
1896
+ className="w-3 h-3 text-gray-400"
1897
+ fill="none"
1898
+ stroke="currentColor"
1899
+ viewBox="0 0 24 24"
1900
+ >
1901
+ <path
1902
+ strokeLinecap="round"
1903
+ strokeLinejoin="round"
1904
+ strokeWidth={2}
1905
+ 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"
1906
+ />
1907
+ </svg>
1908
+ </button>
1909
+ <button className="p-1 hover:bg-gray-200 rounded transition-colors">
1910
+ <svg
1911
+ className="w-3 h-3 text-gray-400"
1912
+ fill="none"
1913
+ stroke="currentColor"
1914
+ viewBox="0 0 24 24"
1915
+ >
1916
+ <path
1917
+ strokeLinecap="round"
1918
+ strokeLinejoin="round"
1919
+ strokeWidth={2}
1920
+ 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"
1921
+ />
1922
+ </svg>
1923
+ </button>
1924
+ </div>
1925
+ </div>
1926
+ )}
1927
+ </div>
1928
+ </div>
1929
+ );
1930
+ };
1931
+
1932
+ export const ModernMessageGroupComponent: React.FC<ModernMessageGroupProps> = ({
1933
+ messages,
1934
+ currentUser,
1935
+ onOpen,
1936
+ onMessageClick,
1937
+ isDesktopView = false,
1938
+ isSmallScreen = false,
1939
+ }) => {
1940
+ // Inject CSS styles for HTML content
1941
+ useInjectStyles();
1942
+
1943
+ // Filter out non-message items (like date strings)
1944
+ const actualMessages = messages.filter((msg) => typeof msg !== 'string') as IPost[];
1945
+
1946
+ // Group messages by user and time
1947
+ const messageGroups = groupMessagesByUserAndTime(actualMessages);
1948
+
1949
+ return (
1950
+ <div className={`min-h-fit ${isDesktopView ? 'space-y-6' : isSmallScreen ? 'space-y-4' : 'space-y-5'}`}>
1951
+ {messageGroups.map((group, groupIndex) => {
1952
+ const author = group[0]?.author;
1953
+ return (
1954
+ <ModernMessageGroup
1955
+ key={`group-${groupIndex}-${group[0]?.id}`}
1956
+ author={author}
1957
+ messages={group}
1958
+ currentUser={currentUser}
1959
+ onOpen={onOpen}
1960
+ onMessageClick={onMessageClick}
1961
+ isDesktopView={isDesktopView}
1962
+ isSmallScreen={isSmallScreen}
1963
+ />
1964
+ );
1965
+ })}
1966
+ </div>
1967
+ );
1968
+ };