@tuturuuu/ui 0.25.2 → 0.25.3

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 (36) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/biome.json +1 -1
  3. package/package.json +13 -12
  4. package/src/components/ui/chat/chat-sidebar-items.tsx +25 -19
  5. package/src/components/ui/chat/chat-utils.test.ts +88 -2
  6. package/src/components/ui/chat/chat-workspace.tsx +1 -0
  7. package/src/components/ui/chat/composer-attachment-chip.tsx +128 -0
  8. package/src/components/ui/chat/external-message-content.test.ts +41 -0
  9. package/src/components/ui/chat/external-message-content.ts +28 -0
  10. package/src/components/ui/chat/message-bubble.tsx +4 -2
  11. package/src/components/ui/chat/message-composer.tsx +121 -33
  12. package/src/components/ui/chat/message-links.test.tsx +14 -0
  13. package/src/components/ui/chat/utils.ts +39 -3
  14. package/src/components/ui/custom/education/courses/course-row-actions.tsx +1 -1
  15. package/src/components/ui/custom/education/modules/course-module-row-actions.tsx +1 -1
  16. package/src/components/ui/custom/structure.test.tsx +55 -1
  17. package/src/components/ui/custom/structure.tsx +10 -1
  18. package/src/components/ui/custom/tables/custom-data-table.tsx +3 -2
  19. package/src/components/ui/custom/tables/data-table-column-header.tsx +4 -3
  20. package/src/components/ui/custom/tables/data-table-faceted-filter.tsx +4 -3
  21. package/src/components/ui/custom/tables/data-table-pagination.tsx +6 -6
  22. package/src/components/ui/custom/tables/data-table-toolbar.tsx +5 -5
  23. package/src/components/ui/custom/tables/data-table-view-options.tsx +4 -3
  24. package/src/components/ui/custom/tables/data-table.tsx +69 -30
  25. package/src/components/ui/finance/invoices/columns.test.tsx +3 -3
  26. package/src/components/ui/finance/invoices/columns.tsx +4 -2
  27. package/src/components/ui/finance/invoices/pending-columns.tsx +1 -1
  28. package/src/components/ui/finance/invoices/row-actions.tsx +1 -1
  29. package/src/components/ui/finance/transactions/categories/columns.test.tsx +5 -5
  30. package/src/components/ui/finance/transactions/categories/columns.tsx +4 -2
  31. package/src/components/ui/finance/transactions/categories/row-actions.tsx +1 -1
  32. package/src/components/ui/finance/transactions/columns.test.tsx +5 -5
  33. package/src/components/ui/finance/transactions/columns.tsx +4 -2
  34. package/src/components/ui/finance/transactions/row-actions.tsx +1 -1
  35. package/src/components/ui/finance/wallets/columns.tsx +4 -2
  36. package/src/components/ui/finance/wallets/row-actions.tsx +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.25.3](https://github.com/tutur3u/platform/compare/ui-v0.25.2...ui-v0.25.3) (2026-08-09)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **chat:** decode external visitor labels ([1ca3e75](https://github.com/tutur3u/platform/commit/1ca3e75a939c06222c842da9fe5892a1bf9ce729))
9
+ * **chat:** decode external visitor labels ([#5112](https://github.com/tutur3u/platform/issues/5112)) ([cb7af54](https://github.com/tutur3u/platform/commit/cb7af54264344961234da681c877c5677bf950fa))
10
+ * **platform:** merge notification and group visibility fixes ([4fe9e97](https://github.com/tutur3u/platform/commit/4fe9e970bf61bffaee4353b1ebb83ce2f880a4c8))
11
+ * **platform:** restore notifications and group visibility ([eb570a4](https://github.com/tutur3u/platform/commit/eb570a47e7a3d38fc855fbf3e887ecbde853ece0))
12
+
13
+ ## [0.25.2](https://github.com/tutur3u/platform/compare/ui-v0.25.1...ui-v0.25.2) (2026-08-07)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **chat:** restore external inbox display parity ([#5108](https://github.com/tutur3u/platform/issues/5108)) ([b34ea6d](https://github.com/tutur3u/platform/commit/b34ea6d7d8ffda344053ed1f580128446d0d0a81))
19
+ * **tasks:** repair board share access and harden AI media attachments ([65b8092](https://github.com/tutur3u/platform/commit/65b809245cc01fb7a5f034f703083f329c20f1c1))
20
+
21
+ ## [0.25.1](https://github.com/tutur3u/platform/compare/ui-v0.25.0...ui-v0.25.1) (2026-08-06)
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * **workspaces:** harden invitation interactions ([b7f8f6c](https://github.com/tutur3u/platform/commit/b7f8f6cf52ceec1b67d36b11513eae7806284f5d))
27
+ * **workspaces:** restore invitation access across apps ([#5099](https://github.com/tutur3u/platform/issues/5099)) ([c7032c3](https://github.com/tutur3u/platform/commit/c7032c310639c2783b60ac560e83a84d65a5c7f5))
28
+
3
29
  ## [0.25.0](https://github.com/tutur3u/platform/compare/ui-v0.24.0...ui-v0.25.0) (2026-08-06)
4
30
 
5
31
 
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "root": false,
3
- "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
3
+ "$schema": "https://biomejs.dev/schemas/2.5.7/schema.json",
4
4
  "extends": "//"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuturuuu/ui",
3
- "version": "0.25.2",
3
+ "version": "0.25.3",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -58,7 +58,7 @@
58
58
  "@tanstack/react-hotkeys": "^0.10.0",
59
59
  "@tanstack/react-pacer": "^0.22.1",
60
60
  "@tanstack/react-query": "^5.101.2",
61
- "@tanstack/react-table": "^8.21.3",
61
+ "@tanstack/react-table": "^9.0.0",
62
62
  "@tanstack/react-virtual": "^3.14.9",
63
63
  "@tiptap/core": "3.29.2",
64
64
  "@tiptap/extension-collaboration": "3.29.2",
@@ -86,13 +86,13 @@
86
86
  "@tiptap/pm": "3.29.2",
87
87
  "@tiptap/react": "3.29.2",
88
88
  "@tiptap/starter-kit": "3.29.2",
89
- "@tuturuuu/ai": "0.8.0",
89
+ "@tuturuuu/ai": "0.8.1",
90
90
  "@tuturuuu/apis": "0.11.0",
91
91
  "@tuturuuu/hooks": "0.1.0",
92
92
  "@tuturuuu/icons": "0.1.0",
93
- "@tuturuuu/internal-api": "0.28.0",
93
+ "@tuturuuu/internal-api": "0.29.0",
94
94
  "@tuturuuu/supabase": "0.5.0",
95
- "@tuturuuu/utils": "0.22.0",
95
+ "@tuturuuu/utils": "0.23.1",
96
96
  "@types/debug": "^4.1.13",
97
97
  "browser-image-compression": "^2.0.2",
98
98
  "class-variance-authority": "^0.7.1",
@@ -104,18 +104,19 @@
104
104
  "debug": "^4.4.3",
105
105
  "embla-carousel-react": "^8.6.0",
106
106
  "eventemitter3": "^5.0.4",
107
- "framer-motion": "^12.43.0",
107
+ "framer-motion": "^13.0.0",
108
108
  "gsap": "^3.15.0",
109
+ "html-entities": "^2.6.0",
109
110
  "html2canvas-pro": "^2.3.3",
110
111
  "input-otp": "^1.4.2",
111
112
  "katex": "^0.18.1",
112
113
  "lodash": "4.18.1",
113
114
  "moment": "^2.30.1",
114
- "motion": "^12.43.0",
115
+ "motion": "^13.0.0",
115
116
  "next": "^16.3.0",
116
- "next-intl": "^4.13.4",
117
+ "next-intl": "^4.13.5",
117
118
  "next-themes": "^0.4.6",
118
- "nuqs": "^2.9.3",
119
+ "nuqs": "^2.9.5",
119
120
  "prosemirror-state": "^1.4.4",
120
121
  "prosemirror-view": "1.42.2",
121
122
  "qrcode.react": "^4.2.0",
@@ -142,14 +143,14 @@
142
143
  "use-debounce": "^10.1.1",
143
144
  "vaul": "^1.1.2",
144
145
  "y-protocols": "^1.0.7",
145
- "yjs": "^13.6.31",
146
+ "yjs": "^13.6.32",
146
147
  "zod": "^4.4.3"
147
148
  },
148
149
  "devDependencies": {
149
150
  "@tailwindcss/postcss": "^4.3.3",
150
151
  "@tailwindcss/typography": "^0.5.20",
151
152
  "@tanstack/react-query": "^5.101.4",
152
- "@tanstack/react-table": "^8.21.3",
153
+ "@tanstack/react-table": "^9.0.0",
153
154
  "@testing-library/jest-dom": "^7.0.0",
154
155
  "@testing-library/react": "^16.3.2",
155
156
  "@tuturuuu/types": "0.27.0",
@@ -174,7 +175,7 @@
174
175
  },
175
176
  "peerDependencies": {
176
177
  "@tanstack/react-query": "^5.101.2",
177
- "@tanstack/react-table": "^8.21.3"
178
+ "@tanstack/react-table": "^9.0.0"
178
179
  },
179
180
  "exports": {
180
181
  "./globals.css": "./src/globals.css",
@@ -6,6 +6,7 @@ import { cn } from '@tuturuuu/utils/format';
6
6
  import { useLocale, useTranslations } from 'next-intl';
7
7
  import { Button } from '../button';
8
8
  import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
9
+ import { getChatMessageDisplayContent } from './external-message-content';
9
10
  import {
10
11
  getChatConversationQueueDetails,
11
12
  getChatMessageSenderLabel,
@@ -37,6 +38,7 @@ export function ConversationRow({
37
38
  channel: t('untitled_channel'),
38
39
  chat: t('untitled_chat'),
39
40
  direct: t('direct_message'),
41
+ external: t('external_sender'),
40
42
  group: t('group_chat'),
41
43
  });
42
44
  const pinned = isChatConversationPinned(conversation, currentUserId);
@@ -160,25 +162,29 @@ export function SearchResultList({
160
162
 
161
163
  return (
162
164
  <div className="h-full space-y-1 overflow-y-auto p-2">
163
- {messages.map((message) => (
164
- <button
165
- className="w-full rounded-md px-2 py-2 text-left transition-colors hover:bg-accent"
166
- key={message.id}
167
- onClick={() => onSelectConversation(message.conversationId)}
168
- type="button"
169
- >
170
- <span className="block truncate font-medium text-sm">
171
- {getChatMessageSenderLabel(message, {
172
- assistant: t('assistant_name'),
173
- external: t('external_sender'),
174
- unknown: t('unknown_sender'),
175
- })}
176
- </span>
177
- <span className="mt-1 line-clamp-2 text-muted-foreground text-xs">
178
- {message.content}
179
- </span>
180
- </button>
181
- ))}
165
+ {messages.map((message) => {
166
+ const displayContent = getChatMessageDisplayContent(message);
167
+
168
+ return (
169
+ <button
170
+ className="w-full rounded-md px-2 py-2 text-left transition-colors hover:bg-accent"
171
+ key={message.id}
172
+ onClick={() => onSelectConversation(message.conversationId)}
173
+ type="button"
174
+ >
175
+ <span className="block truncate font-medium text-sm">
176
+ {getChatMessageSenderLabel(message, {
177
+ assistant: t('assistant_name'),
178
+ external: t('external_sender'),
179
+ unknown: t('unknown_sender'),
180
+ })}
181
+ </span>
182
+ <span className="mt-1 line-clamp-2 text-muted-foreground text-xs">
183
+ {displayContent}
184
+ </span>
185
+ </button>
186
+ );
187
+ })}
182
188
  </div>
183
189
  );
184
190
  }
@@ -134,6 +134,62 @@ describe('chat utils', () => {
134
134
  expect(title).toBe('Ada Lovelace');
135
135
  });
136
136
 
137
+ it('gives unnamed external conversations a stable pseudonymous title', () => {
138
+ const title = getConversationTitle(
139
+ conversation({
140
+ id: 'a7d12840-f72d-48f2-8057-a1ea15aecd29',
141
+ metadata: { externalChat: true },
142
+ title: 'External visitor',
143
+ type: 'channel',
144
+ }),
145
+ 'user-1',
146
+ { external: 'Website visitor' }
147
+ );
148
+
149
+ expect(title).toBe('Website visitor #AECD29');
150
+ });
151
+
152
+ it('prefers an external profile name over a persisted generic title', () => {
153
+ const title = getConversationTitle(
154
+ conversation({
155
+ metadata: { displayName: 'Visitor 42', externalChat: true },
156
+ title: 'External visitor',
157
+ type: 'channel',
158
+ }),
159
+ 'user-1',
160
+ { external: 'Website visitor' }
161
+ );
162
+
163
+ expect(title).toBe('Visitor 42');
164
+ });
165
+
166
+ it('decodes external profile and persisted conversation titles', () => {
167
+ expect(
168
+ getConversationTitle(
169
+ conversation({
170
+ metadata: {
171
+ displayName: 'Nguyễn Thị Vi&amp;ecirc;n',
172
+ externalChat: true,
173
+ },
174
+ title: 'External visitor',
175
+ type: 'channel',
176
+ }),
177
+ 'user-1'
178
+ )
179
+ ).toBe('Nguyễn Thị Viên');
180
+
181
+ expect(
182
+ getConversationTitle(
183
+ conversation({
184
+ metadata: { externalChat: true },
185
+ title: 'Nguyễn Thị Vi&ecirc;n',
186
+ type: 'channel',
187
+ }),
188
+ 'user-1'
189
+ )
190
+ ).toBe('Nguyễn Thị Viên');
191
+ });
192
+
137
193
  it('builds useful message previews for files and deleted messages', () => {
138
194
  expect(
139
195
  getLastMessagePreview(
@@ -171,6 +227,21 @@ describe('chat utils', () => {
171
227
  ).toBe('message_deleted');
172
228
  });
173
229
 
230
+ it('decodes external message previews without changing native previews', () => {
231
+ const encoded = 'T&ocirc;i &#273;&atilde; g&#7917;i tin';
232
+
233
+ expect(
234
+ getLastMessagePreview({
235
+ ...baseMessage,
236
+ content: encoded,
237
+ metadata: { externalChat: true },
238
+ })
239
+ ).toBe('Tôi đã gửi tin');
240
+ expect(getLastMessagePreview({ ...baseMessage, content: encoded })).toBe(
241
+ encoded
242
+ );
243
+ });
244
+
174
245
  it('formats file sizes for attachment rows', () => {
175
246
  expect(formatFileSize(0)).toBe('0 B');
176
247
  expect(formatFileSize(2048)).toBe('2.0 KB');
@@ -344,12 +415,12 @@ describe('chat utils', () => {
344
415
  ...baseMessage,
345
416
  metadata: {
346
417
  externalChat: true,
347
- externalSender: { displayName: 'Visitor 42' },
418
+ externalSender: { displayName: 'Visitor &amp;amp; 42' },
348
419
  },
349
420
  },
350
421
  fallback
351
422
  )
352
- ).toBe('Visitor 42');
423
+ ).toBe('Visitor & 42');
353
424
  expect(
354
425
  getChatMessageSenderLabel(
355
426
  { ...baseMessage, metadata: { externalChat: true } },
@@ -382,6 +453,21 @@ describe('chat utils', () => {
382
453
  });
383
454
  });
384
455
 
456
+ it('decodes connected-site queue previews using conversation scope', () => {
457
+ const details = getChatConversationQueueDetails(
458
+ conversation({
459
+ latestMessage: {
460
+ ...baseMessage,
461
+ content: 'T&ocirc;i &#273;&atilde; g&#7917;i tin',
462
+ metadata: { status: 'seen' },
463
+ },
464
+ metadata: { externalChat: true },
465
+ })
466
+ );
467
+
468
+ expect(details.preview).toBe('Tôi đã gửi tin');
469
+ });
470
+
385
471
  it('normalizes connected-site delivery and deletion states', () => {
386
472
  expect(
387
473
  getChatConversationDeliveryState({
@@ -325,6 +325,7 @@ export function ChatWorkspace({
325
325
  channel: t('untitled_channel'),
326
326
  chat: t('untitled_chat'),
327
327
  direct: t('direct_message'),
328
+ external: t('external_sender'),
328
329
  group: t('group_chat'),
329
330
  })
330
331
  : t('title');
@@ -0,0 +1,128 @@
1
+ 'use client';
2
+
3
+ import { FileText, Film, Music, X } from '@tuturuuu/icons';
4
+ import type { ChatAttachmentDraft } from '@tuturuuu/internal-api';
5
+ import { cn } from '@tuturuuu/utils/format';
6
+ import { Button } from '../button';
7
+ import { formatFileSize } from './utils';
8
+
9
+ export type ComposerAttachmentKind = 'audio' | 'file' | 'image' | 'video';
10
+
11
+ export function getComposerAttachmentKind(
12
+ filename: string,
13
+ contentType?: string | null
14
+ ): ComposerAttachmentKind {
15
+ const type = contentType?.toLowerCase() ?? '';
16
+ const extension = filename.split('.').pop()?.toLowerCase() ?? '';
17
+
18
+ if (
19
+ type.startsWith('image/') ||
20
+ ['avif', 'gif', 'heic', 'jpeg', 'jpg', 'png', 'webp'].includes(extension)
21
+ ) {
22
+ return 'image';
23
+ }
24
+ if (
25
+ type.startsWith('video/') ||
26
+ ['m4v', 'mov', 'mp4', 'webm'].includes(extension)
27
+ ) {
28
+ return 'video';
29
+ }
30
+ if (
31
+ type.startsWith('audio/') ||
32
+ ['aac', 'flac', 'm4a', 'mp3', 'ogg', 'opus', 'wav'].includes(extension)
33
+ ) {
34
+ return 'audio';
35
+ }
36
+ return 'file';
37
+ }
38
+
39
+ const kindIcon = {
40
+ audio: Music,
41
+ file: FileText,
42
+ image: FileText,
43
+ video: Film,
44
+ } as const;
45
+
46
+ /**
47
+ * A queued attachment, shown before the message is sent. Media gets a real
48
+ * thumbnail from the local file rather than a filename chip — a picture is the
49
+ * only reliable way to confirm you attached the right screenshot.
50
+ */
51
+ export function ComposerAttachmentChip({
52
+ attachment,
53
+ onRemove,
54
+ previewUrl,
55
+ removeLabel,
56
+ }: {
57
+ attachment: ChatAttachmentDraft;
58
+ onRemove: () => void;
59
+ previewUrl?: string;
60
+ removeLabel: string;
61
+ }) {
62
+ const kind = getComposerAttachmentKind(
63
+ attachment.filename,
64
+ attachment.contentType
65
+ );
66
+ const showsThumbnail = Boolean(previewUrl) && kind !== 'file';
67
+ const Icon = kindIcon[kind];
68
+
69
+ return (
70
+ <div
71
+ className={cn(
72
+ 'group relative flex min-w-0 items-center gap-2 overflow-hidden rounded-md border bg-muted/40 text-sm',
73
+ showsThumbnail ? 'p-1 pr-1' : 'px-2 py-1'
74
+ )}
75
+ >
76
+ {showsThumbnail ? (
77
+ <span className="size-10 shrink-0 overflow-hidden rounded bg-background">
78
+ {kind === 'video' ? (
79
+ <video
80
+ className="size-full object-cover"
81
+ muted
82
+ preload="metadata"
83
+ src={previewUrl}
84
+ />
85
+ ) : kind === 'audio' ? (
86
+ <span className="flex size-full items-center justify-center">
87
+ <Music className="size-4 text-muted-foreground" />
88
+ </span>
89
+ ) : (
90
+ /* biome-ignore lint/performance/noImgElement: local blob: URL for a not-yet-sent file; next/image cannot optimize object URLs */
91
+ <img
92
+ alt={attachment.filename}
93
+ className="size-full object-cover"
94
+ src={previewUrl}
95
+ />
96
+ )}
97
+ </span>
98
+ ) : (
99
+ <Icon className="size-4 shrink-0 text-muted-foreground" />
100
+ )}
101
+
102
+ <span className="min-w-0 flex-1">
103
+ <span
104
+ className="block max-w-40 truncate font-medium"
105
+ title={attachment.filename}
106
+ >
107
+ {attachment.filename}
108
+ </span>
109
+ {attachment.sizeBytes ? (
110
+ <span className="block text-muted-foreground text-xs">
111
+ {formatFileSize(attachment.sizeBytes)}
112
+ </span>
113
+ ) : null}
114
+ </span>
115
+
116
+ <Button
117
+ aria-label={removeLabel}
118
+ className="size-6 shrink-0"
119
+ onClick={onRemove}
120
+ size="icon"
121
+ type="button"
122
+ variant="ghost"
123
+ >
124
+ <X className="size-3.5" />
125
+ </Button>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getChatMessageDisplayContent } from './external-message-content';
3
+
4
+ describe('getChatMessageDisplayContent', () => {
5
+ it('decodes named and numeric entities for external chat messages', () => {
6
+ expect(
7
+ getChatMessageDisplayContent({
8
+ content:
9
+ 'T&ocirc;i &#273;&atilde; g&#7917;i tin &amp; ch&#7901; ph&#7843;n h&#7891;i.',
10
+ metadata: { externalChat: true },
11
+ })
12
+ ).toBe('Tôi đã gửi tin & chờ phản hồi.');
13
+ });
14
+
15
+ it('decodes bounded nested entities from double-encoded legacy rows', () => {
16
+ expect(
17
+ getChatMessageDisplayContent({
18
+ content: 'Kh&aacute;m trực tiếp &amp;amp; mang theo kết quả',
19
+ metadata: { externalChat: true },
20
+ })
21
+ ).toBe('Khám trực tiếp & mang theo kết quả');
22
+ });
23
+
24
+ it('leaves native chat content unchanged', () => {
25
+ expect(
26
+ getChatMessageDisplayContent({
27
+ content: 'Keep &amp; exactly as authored',
28
+ metadata: {},
29
+ })
30
+ ).toBe('Keep &amp; exactly as authored');
31
+ });
32
+
33
+ it('returns decoded markup as text content rather than an HTML contract', () => {
34
+ expect(
35
+ getChatMessageDisplayContent({
36
+ content: '&lt;img src=x onerror=alert(1)&gt;',
37
+ metadata: { externalChat: true },
38
+ })
39
+ ).toBe('<img src=x onerror=alert(1)>');
40
+ });
41
+ });
@@ -0,0 +1,28 @@
1
+ import type { ChatMessage } from '@tuturuuu/internal-api';
2
+ import { decode } from 'html-entities';
3
+
4
+ const MAX_ENTITY_DECODE_PASSES = 3;
5
+
6
+ export function decodeExternalChatContent(
7
+ content: string,
8
+ externalChat: boolean
9
+ ) {
10
+ if (!externalChat) return content;
11
+
12
+ let displayContent = content;
13
+ for (let pass = 0; pass < MAX_ENTITY_DECODE_PASSES; pass += 1) {
14
+ const decoded = decode(displayContent, { level: 'html5', scope: 'body' });
15
+ if (decoded === displayContent) break;
16
+ displayContent = decoded;
17
+ }
18
+ return displayContent;
19
+ }
20
+
21
+ export function getChatMessageDisplayContent(
22
+ message: Pick<ChatMessage, 'content' | 'metadata'>
23
+ ) {
24
+ return decodeExternalChatContent(
25
+ message.content,
26
+ message.metadata.externalChat === true
27
+ );
28
+ }
@@ -26,6 +26,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '../popover';
26
26
  import { toast } from '../sonner';
27
27
  import { AiMessageParts } from './ai-message-parts';
28
28
  import { getAiMessagePartsFromMetadata } from './ai-message-render-utils';
29
+ import { getChatMessageDisplayContent } from './external-message-content';
29
30
  import { MessageAttachmentButton } from './message-attachment-button';
30
31
  import { MessageLinkPreviews, MessageText } from './message-links';
31
32
  import {
@@ -259,6 +260,7 @@ function MessageContent({
259
260
  const t = useTranslations('chat');
260
261
  const aiParts = getAiMessagePartsFromMetadata(message.metadata);
261
262
  const externalAttachment = readExternalAttachment(message.metadata);
263
+ const displayContent = getChatMessageDisplayContent(message);
262
264
 
263
265
  return (
264
266
  <div
@@ -284,10 +286,10 @@ function MessageContent({
284
286
  textFallback={message.content}
285
287
  />
286
288
  ) : (
287
- <MessageText content={message.content} />
289
+ <MessageText content={displayContent} />
288
290
  )}
289
291
  <MessageLinkPreviews
290
- content={message.content}
292
+ content={displayContent}
291
293
  conversationId={message.conversationId}
292
294
  isOwnMessage={isOwnMessage}
293
295
  wsId={wsId}