@memori.ai/memori-react 8.38.4 → 8.38.6

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 (107) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/components/Chat/Chat.js +11 -23
  3. package/dist/components/Chat/Chat.js.map +1 -1
  4. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.css +12 -3
  5. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.d.ts +0 -1
  6. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.js +18 -8
  7. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.js.map +1 -1
  8. package/dist/components/DrawerFooter/DrawerFooter.js +1 -1
  9. package/dist/components/DrawerFooter/DrawerFooter.js.map +1 -1
  10. package/dist/components/FilePreview/FilePreview.js +20 -2
  11. package/dist/components/FilePreview/FilePreview.js.map +1 -1
  12. package/dist/components/MediaWidget/MediaItemWidget.js +9 -6
  13. package/dist/components/MediaWidget/MediaItemWidget.js.map +1 -1
  14. package/dist/components/MediaWidget/MediaItemWidget.utils.d.ts +1 -0
  15. package/dist/components/MediaWidget/MediaItemWidget.utils.js +27 -7
  16. package/dist/components/MediaWidget/MediaItemWidget.utils.js.map +1 -1
  17. package/dist/components/MobileSessionPanel/MobileSessionPanel.css +48 -4
  18. package/dist/components/MobileSessionPanel/MobileSessionPanel.d.ts +7 -2
  19. package/dist/components/MobileSessionPanel/MobileSessionPanel.js +178 -58
  20. package/dist/components/MobileSessionPanel/MobileSessionPanel.js.map +1 -1
  21. package/dist/components/PositionPopover/PositionPopover.js +2 -1
  22. package/dist/components/PositionPopover/PositionPopover.js.map +1 -1
  23. package/dist/components/UploadButton/UploadButton.js +40 -11
  24. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  25. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +0 -2
  26. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +53 -30
  27. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  28. package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js +6 -2
  29. package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
  30. package/dist/components/layouts/WebsiteAssistant/website-assistant.css +1 -3
  31. package/dist/components/layouts/fullpage.css +79 -28
  32. package/dist/components/ui/Tooltip.js +3 -3
  33. package/dist/components/ui/Tooltip.js.map +1 -1
  34. package/dist/helpers/constants.d.ts +3 -0
  35. package/dist/helpers/constants.js +24 -1
  36. package/dist/helpers/constants.js.map +1 -1
  37. package/dist/helpers/usePressTooltip.d.ts +13 -0
  38. package/dist/helpers/usePressTooltip.js +23 -0
  39. package/dist/helpers/usePressTooltip.js.map +1 -0
  40. package/dist/helpers/utils.d.ts +15 -0
  41. package/dist/helpers/utils.js +45 -1
  42. package/dist/helpers/utils.js.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/esm/components/Chat/Chat.js +12 -24
  46. package/esm/components/Chat/Chat.js.map +1 -1
  47. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.css +12 -3
  48. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.d.ts +0 -1
  49. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.js +18 -8
  50. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.js.map +1 -1
  51. package/esm/components/DrawerFooter/DrawerFooter.js +1 -1
  52. package/esm/components/DrawerFooter/DrawerFooter.js.map +1 -1
  53. package/esm/components/FilePreview/FilePreview.js +22 -4
  54. package/esm/components/FilePreview/FilePreview.js.map +1 -1
  55. package/esm/components/MediaWidget/MediaItemWidget.js +11 -8
  56. package/esm/components/MediaWidget/MediaItemWidget.js.map +1 -1
  57. package/esm/components/MediaWidget/MediaItemWidget.utils.d.ts +1 -0
  58. package/esm/components/MediaWidget/MediaItemWidget.utils.js +25 -6
  59. package/esm/components/MediaWidget/MediaItemWidget.utils.js.map +1 -1
  60. package/esm/components/MobileSessionPanel/MobileSessionPanel.css +48 -4
  61. package/esm/components/MobileSessionPanel/MobileSessionPanel.d.ts +7 -2
  62. package/esm/components/MobileSessionPanel/MobileSessionPanel.js +179 -60
  63. package/esm/components/MobileSessionPanel/MobileSessionPanel.js.map +1 -1
  64. package/esm/components/PositionPopover/PositionPopover.js +2 -1
  65. package/esm/components/PositionPopover/PositionPopover.js.map +1 -1
  66. package/esm/components/UploadButton/UploadButton.js +40 -11
  67. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  68. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +0 -2
  69. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +53 -30
  70. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  71. package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js +6 -2
  72. package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
  73. package/esm/components/layouts/WebsiteAssistant/website-assistant.css +1 -3
  74. package/esm/components/layouts/fullpage.css +79 -28
  75. package/esm/components/ui/Tooltip.js +3 -3
  76. package/esm/components/ui/Tooltip.js.map +1 -1
  77. package/esm/helpers/constants.d.ts +3 -0
  78. package/esm/helpers/constants.js +23 -0
  79. package/esm/helpers/constants.js.map +1 -1
  80. package/esm/helpers/usePressTooltip.d.ts +13 -0
  81. package/esm/helpers/usePressTooltip.js +20 -0
  82. package/esm/helpers/usePressTooltip.js.map +1 -0
  83. package/esm/helpers/utils.d.ts +15 -0
  84. package/esm/helpers/utils.js +39 -0
  85. package/esm/helpers/utils.js.map +1 -1
  86. package/esm/version.d.ts +1 -1
  87. package/esm/version.js +1 -1
  88. package/package.json +1 -1
  89. package/src/components/Chat/Chat.tsx +19 -44
  90. package/src/components/FilePreview/FilePreview.tsx +26 -4
  91. package/src/components/MediaWidget/MediaItemWidget.tsx +19 -7
  92. package/src/components/MediaWidget/MediaItemWidget.utils.test.ts +45 -2
  93. package/src/components/MediaWidget/MediaItemWidget.utils.ts +37 -6
  94. package/src/components/UploadButton/UploadButton.tsx +154 -104
  95. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +54 -42
  96. package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +2 -2
  97. package/src/components/ui/Tooltip.tsx +3 -2
  98. package/src/helpers/constants.ts +29 -1
  99. package/src/helpers/utils.test.ts +101 -0
  100. package/src/helpers/utils.ts +66 -0
  101. package/src/version.ts +1 -1
  102. package/dist/helpers/userMessage.d.ts +0 -2
  103. package/dist/helpers/userMessage.js +0 -23
  104. package/dist/helpers/userMessage.js.map +0 -1
  105. package/esm/helpers/userMessage.d.ts +0 -2
  106. package/esm/helpers/userMessage.js +0 -18
  107. package/esm/helpers/userMessage.js.map +0 -1
@@ -12,6 +12,7 @@ import { getResourceUrl } from '../../helpers/media';
12
12
  import {
13
13
  withLinksOpenInNewTab,
14
14
  stripDocumentAttachmentTags,
15
+ isAssetOnlyDocumentAttachment,
15
16
  } from '../../helpers/utils';
16
17
  import { getTranslation } from '../../helpers/translations';
17
18
  import { prismSyntaxLangs } from '../../helpers/constants';
@@ -36,7 +37,7 @@ import type {
36
37
  import {
37
38
  formatBytes,
38
39
  getFileExtensionFromUrl,
39
- getFileExtensionFromMime,
40
+ getDocumentBadgeLabel,
40
41
  countLines,
41
42
  shouldUseDarkFileCard,
42
43
  fetchLinkPreview,
@@ -272,7 +273,7 @@ export const RenderMediaItem = memo(function RenderMediaItem({
272
273
  ) : (
273
274
  <DocumentCard
274
275
  title={medium.title || 'File'}
275
- badge={getFileExtensionFromMime(medium.mimeType)}
276
+ badge={getDocumentBadgeLabel(medium.mimeType, medium.title)}
276
277
  meta={(() => {
277
278
  const size = getContentSize(medium);
278
279
  return size != null && size > 0 ? formatBytes(size) : null;
@@ -288,7 +289,7 @@ export const RenderMediaItem = memo(function RenderMediaItem({
288
289
  return (
289
290
  <DocumentCard
290
291
  title={medium.title || 'File'}
291
- badge={getFileExtensionFromMime(medium.mimeType)}
292
+ badge={getDocumentBadgeLabel(medium.mimeType, medium.title)}
292
293
  meta={(() => {
293
294
  const size = getContentSize(medium);
294
295
  return size != null && size > 0 ? formatBytes(size) : null;
@@ -302,7 +303,7 @@ export const RenderMediaItem = memo(function RenderMediaItem({
302
303
  return (
303
304
  <DocumentCard
304
305
  title={medium.title || 'File'}
305
- badge={getFileExtensionFromMime(medium.mimeType)}
306
+ badge={getDocumentBadgeLabel(medium.mimeType, medium.title)}
306
307
  meta={(() => {
307
308
  const size = getContentSize(medium);
308
309
  return size != null && size > 0 ? formatBytes(size) : null;
@@ -328,8 +329,11 @@ export const RenderMediaItem = memo(function RenderMediaItem({
328
329
 
329
330
  // Extension and file detection helpers
330
331
  const fileExtensionFromUrl = getFileExtensionFromUrl(normURL || item.url);
331
- const fileExtensionFromMime = getFileExtensionFromMime(item.mimeType);
332
- const fileExtension = fileExtensionFromUrl || fileExtensionFromMime;
332
+ const fileExtension = getDocumentBadgeLabel(
333
+ item.mimeType,
334
+ item.title,
335
+ normURL || item.url
336
+ );
333
337
  const isFile = shouldUseDarkFileCard(
334
338
  item,
335
339
  fileExtensionFromUrl,
@@ -359,8 +363,16 @@ export const RenderMediaItem = memo(function RenderMediaItem({
359
363
  const metaParts = [lineText, sizeText].filter(Boolean);
360
364
  const metaLine = metaParts.length > 0 ? metaParts.join(' · ') : null;
361
365
 
366
+ // Asset-only attachments (e.g. Office native files) open via URL, not modal
367
+ const isAssetOnlyAttachment = isAssetOnlyDocumentAttachment(item);
368
+
362
369
  // Document attachments and attached files should open in modal, not as links
363
- if ((isDocumentAttachment || isAttachedFile) && item.mediumID && _onClick) {
370
+ if (
371
+ (isDocumentAttachment || isAttachedFile) &&
372
+ item.mediumID &&
373
+ _onClick &&
374
+ !isAssetOnlyAttachment
375
+ ) {
364
376
  return (
365
377
  <div
366
378
  onClick={() => _onClick(item)}
@@ -2,6 +2,7 @@ import {
2
2
  formatBytes,
3
3
  getFileExtensionFromUrl,
4
4
  getFileExtensionFromMime,
5
+ getDocumentBadgeLabel,
5
6
  countLines,
6
7
  shouldUseDarkFileCard,
7
8
  fetchLinkPreview,
@@ -57,8 +58,25 @@ describe('MediaItemWidget.utils', () => {
57
58
  expect(getFileExtensionFromMime('text/html')).toBe('HTML');
58
59
  expect(getFileExtensionFromMime('text/plain')).toBe('TXT');
59
60
  expect(getFileExtensionFromMime('application/json')).toBe('JSON');
60
- expect(getFileExtensionFromMime('application/vnd.ms-excel')).toBe('XLS');
61
- expect(getFileExtensionFromMime('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')).toBe('XLSX');
61
+ expect(getFileExtensionFromMime('application/vnd.ms-excel')).toBe('Excel');
62
+ expect(
63
+ getFileExtensionFromMime(
64
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
65
+ )
66
+ ).toBe('Excel');
67
+ });
68
+
69
+ it('maps office mime types with parameters to short labels', () => {
70
+ expect(
71
+ getFileExtensionFromMime(
72
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document; charset=binary'
73
+ )
74
+ ).toBe('Word');
75
+ expect(
76
+ getFileExtensionFromMime(
77
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.template'
78
+ )
79
+ ).toBe('Excel');
62
80
  });
63
81
 
64
82
  it('uses subtype when not in MIME_TO_EXT map', () => {
@@ -71,6 +89,31 @@ describe('MediaItemWidget.utils', () => {
71
89
  });
72
90
  });
73
91
 
92
+ describe('getDocumentBadgeLabel', () => {
93
+ it('prefers URL extension, then filename, then mime type', () => {
94
+ expect(
95
+ getDocumentBadgeLabel(
96
+ 'application/pdf',
97
+ 'report.docx',
98
+ 'https://example.com/file.pdf'
99
+ )
100
+ ).toBe('PDF');
101
+ expect(
102
+ getDocumentBadgeLabel(
103
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
104
+ 'report.docx',
105
+ 'https://example.com/asset/123'
106
+ )
107
+ ).toBe('Word');
108
+ expect(
109
+ getDocumentBadgeLabel(
110
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
111
+ 'budget.xlsx'
112
+ )
113
+ ).toBe('Excel');
114
+ });
115
+ });
116
+
74
117
  describe('countLines', () => {
75
118
  it('returns 0 for undefined or empty', () => {
76
119
  expect(countLines(undefined)).toBe(0);
@@ -1,4 +1,8 @@
1
1
  import type { Medium } from '@memori.ai/memori-api-client/dist/types';
2
+ import {
3
+ officeExtensionShortLabels,
4
+ officeMimeShortLabels,
5
+ } from '../../helpers/constants';
2
6
  import type { LinkPreviewInfo } from './MediaItemWidget.types';
3
7
 
4
8
  export const FILE_EXTENSIONS_DARK_CARD = [
@@ -54,11 +58,6 @@ export const IMAGE_MIME_TYPES = [
54
58
 
55
59
  const MIME_TO_EXT: Record<string, string> = {
56
60
  'application/pdf': 'PDF',
57
- 'application/msword': 'DOC',
58
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
59
- 'DOCX',
60
- 'application/vnd.ms-excel': 'XLS',
61
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'XLSX',
62
61
  'text/html': 'HTML',
63
62
  'text/plain': 'TXT',
64
63
  'text/css': 'CSS',
@@ -87,14 +86,46 @@ export function getFileExtensionFromUrl(
87
86
  return match ? match[1].toUpperCase() : null;
88
87
  }
89
88
 
89
+ function normalizeMimeType(mimeType: string): string {
90
+ return mimeType.split(';')[0].trim();
91
+ }
92
+
90
93
  export function getFileExtensionFromMime(mimeType: string): string {
94
+ const normalized = normalizeMimeType(mimeType);
95
+ const normalizedLower = normalized.toLowerCase();
96
+
97
+ const officeLabel =
98
+ officeMimeShortLabels[normalized] ||
99
+ officeMimeShortLabels[normalizedLower];
100
+ if (officeLabel) return officeLabel;
101
+
91
102
  return (
103
+ MIME_TO_EXT[normalized] ||
104
+ MIME_TO_EXT[normalizedLower] ||
92
105
  MIME_TO_EXT[mimeType] ||
93
- mimeType.split('/')[1]?.toUpperCase() ||
106
+ normalized.split('/')[1]?.toUpperCase() ||
94
107
  'FILE'
95
108
  );
96
109
  }
97
110
 
111
+ export function getDocumentBadgeLabel(
112
+ mimeType: string,
113
+ filename?: string | null,
114
+ url?: string | null
115
+ ): string {
116
+ const fromUrl = getFileExtensionFromUrl(url || undefined);
117
+ if (fromUrl) {
118
+ return officeExtensionShortLabels[fromUrl] || fromUrl;
119
+ }
120
+
121
+ const fromFilename = getFileExtensionFromUrl(filename || undefined);
122
+ if (fromFilename) {
123
+ return officeExtensionShortLabels[fromFilename] || fromFilename;
124
+ }
125
+
126
+ return getFileExtensionFromMime(mimeType);
127
+ }
128
+
98
129
  export function countLines(content: string | undefined): number {
99
130
  if (!content) return 0;
100
131
  return content.split(/\r\n|\r|\n/).length;
@@ -9,6 +9,7 @@ import UploadDocuments from './UploadDocuments/UploadDocuments';
9
9
  import UploadImages from './UploadImages/UploadImages';
10
10
  import { useTranslation } from 'react-i18next';
11
11
  import memoriApiClient from '@memori.ai/memori-api-client';
12
+ import { officeNativeExtensions } from '../../helpers/constants';
12
13
  import Tooltip from '../ui/Tooltip';
13
14
  // Props interface
14
15
  interface UploadManagerProps {
@@ -48,6 +49,8 @@ const UploadButton: React.FC<UploadManagerProps> = ({
48
49
  maxDocumentContentLength = 300000,
49
50
  onUploadLoadingChange,
50
51
  }) => {
52
+ // Per-document character limit for the inlined `<document_attachment>`
53
+ // content. Does NOT affect the full text uploaded as an asset.
51
54
  const effectivePerDocumentLimit =
52
55
  maxTotalMessagePayload ?? maxDocumentContentLength ?? 300000;
53
56
  // State
@@ -98,7 +101,16 @@ const UploadButton: React.FC<UploadManagerProps> = ({
98
101
 
99
102
  // Check if file is a document
100
103
  const isDocumentFile = (file: File): boolean => {
101
- const documentExtensions = ['.pdf', '.txt', '.json', '.xlsx', '.csv', '.md', '.html'];
104
+ const documentExtensions = [
105
+ '.pdf',
106
+ '.txt',
107
+ '.json',
108
+ '.xlsx',
109
+ '.csv',
110
+ '.md',
111
+ '.html',
112
+ ...officeNativeExtensions,
113
+ ];
102
114
  const fileExt = `.${file.name.split('.').pop()?.toLowerCase()}`;
103
115
  return documentExtensions.includes(fileExt);
104
116
  };
@@ -115,115 +127,124 @@ const UploadButton: React.FC<UploadManagerProps> = ({
115
127
  }, [isMediaAccepted, currentMediaCount, addError]);
116
128
 
117
129
  // Handle unified file selection
118
- const handleUnifiedFileSelection = useCallback((files: FileList | File[]) => {
119
- const fileArray = Array.from(files);
120
- if (fileArray.length === 0) return;
121
-
122
- const supportedFiles: File[] = [];
123
- fileArray.forEach(file => {
124
- if (isImageFile(file)) {
125
- supportedFiles.push(file);
126
- } else if (isDocumentFile(file)) {
127
- supportedFiles.push(file);
128
- } else {
130
+ const handleUnifiedFileSelection = useCallback(
131
+ (files: FileList | File[]) => {
132
+ const fileArray = Array.from(files);
133
+ if (fileArray.length === 0) return;
134
+
135
+ const supportedFiles: File[] = [];
136
+ fileArray.forEach(file => {
137
+ if (isImageFile(file)) {
138
+ supportedFiles.push(file);
139
+ } else if (isDocumentFile(file)) {
140
+ supportedFiles.push(file);
141
+ } else {
142
+ addErrorRef.current({
143
+ message: `File "${file.name}" is not a supported image or document type`,
144
+ severity: 'warning',
145
+ });
146
+ }
147
+ });
148
+
149
+ const totalSupported = supportedFiles.length;
150
+ if (totalSupported === 0) return;
151
+
152
+ const remainingSlots =
153
+ maxDocumentsPerMessage - currentMediaCountRef.current;
154
+ if (remainingSlots <= 0) {
129
155
  addErrorRef.current({
130
- message: `File "${file.name}" is not a supported image or document type`,
156
+ message: `Maximum ${maxDocumentsPerMessage} media files allowed.`,
131
157
  severity: 'warning',
132
158
  });
159
+ return;
133
160
  }
134
- });
135
-
136
- const totalSupported = supportedFiles.length;
137
- if (totalSupported === 0) return;
138
161
 
139
- const remainingSlots = maxDocumentsPerMessage - currentMediaCountRef.current;
140
- if (remainingSlots <= 0) {
141
- addErrorRef.current({
142
- message: `Maximum ${maxDocumentsPerMessage} media files allowed.`,
143
- severity: 'warning',
144
- });
145
- return;
146
- }
162
+ const toProcess = supportedFiles.slice(0, remainingSlots);
163
+ const imageFiles = toProcess.filter(f => isImageFile(f));
164
+ const documentFiles = toProcess.filter(f => isDocumentFile(f));
147
165
 
148
- const toProcess = supportedFiles.slice(0, remainingSlots);
149
- const imageFiles = toProcess.filter(f => isImageFile(f));
150
- const documentFiles = toProcess.filter(f => isDocumentFile(f));
151
-
152
- if (totalSupported > remainingSlots) {
153
- const skipped = totalSupported - remainingSlots;
154
- addErrorRef.current({
155
- message:
156
- t('upload.filesNotAddedMaxAllowed', {
157
- count: skipped,
158
- max: maxDocumentsPerMessage,
159
- defaultValue: `${skipped} file(s) not added (maximum ${maxDocumentsPerMessage} files allowed).`,
160
- }) ?? `${skipped} file(s) not added (maximum ${maxDocumentsPerMessage} files allowed).`,
161
- severity: 'warning',
162
- });
163
- }
164
-
165
- // Process images
166
- if (imageFiles.length > 0) {
167
- if (!isMediaAcceptedRef.current) {
166
+ if (totalSupported > remainingSlots) {
167
+ const skipped = totalSupported - remainingSlots;
168
168
  addErrorRef.current({
169
169
  message:
170
- t('upload.mediaNotAccepted') ?? 'Media uploads are not accepted',
170
+ t('upload.filesNotAddedMaxAllowed', {
171
+ count: skipped,
172
+ max: maxDocumentsPerMessage,
173
+ defaultValue: `${skipped} file(s) not added (maximum ${maxDocumentsPerMessage} files allowed).`,
174
+ }) ??
175
+ `${skipped} file(s) not added (maximum ${maxDocumentsPerMessage} files allowed).`,
171
176
  severity: 'warning',
172
177
  });
173
- } else {
174
- // Trigger image upload by creating a synthetic event
175
- const imageInput = imageRef.current?.querySelector('input[type="file"]') as HTMLInputElement;
176
- if (imageInput) {
178
+ }
179
+
180
+ // Process images
181
+ if (imageFiles.length > 0) {
182
+ if (!isMediaAcceptedRef.current) {
183
+ addErrorRef.current({
184
+ message:
185
+ t('upload.mediaNotAccepted') ?? 'Media uploads are not accepted',
186
+ severity: 'warning',
187
+ });
188
+ } else {
189
+ // Trigger image upload by creating a synthetic event
190
+ const imageInput = imageRef.current?.querySelector(
191
+ 'input[type="file"]'
192
+ ) as HTMLInputElement;
193
+ if (imageInput) {
194
+ const dataTransfer = new DataTransfer();
195
+ imageFiles.forEach(file => {
196
+ try {
197
+ dataTransfer.items.add(file);
198
+ } catch (err) {
199
+ console.warn('Failed to add image file to DataTransfer:', err);
200
+ }
201
+ });
202
+
203
+ // Only proceed if we successfully added files
204
+ if (dataTransfer.files.length > 0) {
205
+ try {
206
+ imageInput.files = dataTransfer.files;
207
+ } catch {
208
+ // JSDOM and some environments do not allow assigning to input.files
209
+ }
210
+ const changeEvent = new Event('change', { bubbles: true });
211
+ imageInput.dispatchEvent(changeEvent);
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ // Process documents – set loading early so skeleton shows for all entry points
218
+ if (documentFiles.length > 0) {
219
+ setIsDocumentLoading(true);
220
+ const documentInput = documentRef.current?.querySelector(
221
+ 'input[type="file"]'
222
+ ) as HTMLInputElement;
223
+ if (documentInput) {
177
224
  const dataTransfer = new DataTransfer();
178
- imageFiles.forEach(file => {
225
+ documentFiles.forEach(file => {
179
226
  try {
180
227
  dataTransfer.items.add(file);
181
228
  } catch (err) {
182
- console.warn('Failed to add image file to DataTransfer:', err);
229
+ console.warn('Failed to add document file to DataTransfer:', err);
183
230
  }
184
231
  });
185
-
232
+
186
233
  // Only proceed if we successfully added files
187
234
  if (dataTransfer.files.length > 0) {
188
235
  try {
189
- imageInput.files = dataTransfer.files;
236
+ documentInput.files = dataTransfer.files;
190
237
  } catch {
191
238
  // JSDOM and some environments do not allow assigning to input.files
192
239
  }
193
240
  const changeEvent = new Event('change', { bubbles: true });
194
- imageInput.dispatchEvent(changeEvent);
241
+ documentInput.dispatchEvent(changeEvent);
195
242
  }
196
243
  }
197
244
  }
198
- }
199
-
200
- // Process documents – set loading early so skeleton shows for all entry points
201
- if (documentFiles.length > 0) {
202
- setIsDocumentLoading(true);
203
- const documentInput = documentRef.current?.querySelector('input[type="file"]') as HTMLInputElement;
204
- if (documentInput) {
205
- const dataTransfer = new DataTransfer();
206
- documentFiles.forEach(file => {
207
- try {
208
- dataTransfer.items.add(file);
209
- } catch (err) {
210
- console.warn('Failed to add document file to DataTransfer:', err);
211
- }
212
- });
213
-
214
- // Only proceed if we successfully added files
215
- if (dataTransfer.files.length > 0) {
216
- try {
217
- documentInput.files = dataTransfer.files;
218
- } catch {
219
- // JSDOM and some environments do not allow assigning to input.files
220
- }
221
- const changeEvent = new Event('change', { bubbles: true });
222
- documentInput.dispatchEvent(changeEvent);
223
- }
224
- }
225
- }
226
- }, [t]);
245
+ },
246
+ [t]
247
+ );
227
248
 
228
249
  // Handle button click - open file chooser directly
229
250
  const handleButtonClick = () => {
@@ -253,16 +274,17 @@ const UploadButton: React.FC<UploadManagerProps> = ({
253
274
  }
254
275
 
255
276
  const files: File[] = [];
256
-
277
+
257
278
  // Helper to check if a file is already in the array
258
279
  const isDuplicate = (file: File) => {
259
- return files.some(f =>
260
- f.name === file.name &&
261
- f.size === file.size &&
262
- f.lastModified === file.lastModified
280
+ return files.some(
281
+ f =>
282
+ f.name === file.name &&
283
+ f.size === file.size &&
284
+ f.lastModified === file.lastModified
263
285
  );
264
286
  };
265
-
287
+
266
288
  // Prefer clipboardData.files if available (most reliable and prevents duplicates)
267
289
  // Only fall back to items if files is empty (some browsers only populate items)
268
290
  if (clipboardData.files && clipboardData.files.length > 0) {
@@ -362,7 +384,6 @@ const UploadButton: React.FC<UploadManagerProps> = ({
362
384
  id: string;
363
385
  content: string;
364
386
  mimeType: string;
365
- sourceUrl?: string;
366
387
  textAssetUrl?: string;
367
388
  }[]
368
389
  ) => {
@@ -381,19 +402,39 @@ const UploadButton: React.FC<UploadManagerProps> = ({
381
402
  // Process each document file
382
403
  const processedDocuments = files.map(file => {
383
404
  const escapedFileName = escapeAttributeValue(file.name);
384
- const formattedContent = `<document_attachment filename="${escapedFileName}" type="${file.mimeType}">
385
405
 
386
- ${file.content}
406
+ let formattedContent: string;
387
407
 
408
+ if (!file.content) {
409
+ // Office native format: metadata-only attachment tag + asset link (no text body)
410
+ formattedContent = `<document_attachment filename="${escapedFileName}" type="${file.mimeType}">
388
411
  </document_attachment>
389
412
 
390
- <attachment_source>
391
- ${file.sourceUrl || ''}
392
- </attachment_source>
413
+ <attachment_link>
414
+ ${file.textAssetUrl || ''}
415
+ </attachment_link>`;
416
+ } else {
417
+ // Truncate only the content that gets inlined inside the message
418
+ // <document_attachment> tag. The full text is still available via
419
+ // textAssetUrl as an uploaded asset.
420
+ const inlinedContent =
421
+ file.content.length > effectivePerDocumentLimit
422
+ ? file.content.substring(0, effectivePerDocumentLimit) +
423
+ '\n\n[Content truncated due to size limits]'
424
+ : file.content;
425
+
426
+ formattedContent = `<document_attachment filename="${escapedFileName}" type="${
427
+ file.mimeType
428
+ }">
429
+
430
+ ${inlinedContent}
431
+
432
+ </document_attachment>
393
433
 
394
434
  <attachment_link>
395
435
  ${file.textAssetUrl || ''}
396
436
  </attachment_link>`;
437
+ }
397
438
 
398
439
  return {
399
440
  name: file.name,
@@ -419,6 +460,7 @@ ${file.textAssetUrl || ''}
419
460
  '.csv',
420
461
  '.md',
421
462
  '.html',
463
+ ...officeNativeExtensions,
422
464
  ];
423
465
  const MAX_FILE_SIZE = 15 * 1024 * 1024; // 15MB
424
466
 
@@ -505,18 +547,17 @@ ${file.textAssetUrl || ''}
505
547
  addError(error);
506
548
  };
507
549
 
508
-
509
550
  const handleDocumentLoadingChange = useCallback(
510
551
  (loading: boolean, fileCount?: number) => {
511
552
  setIsDocumentLoading(loading);
512
- setDocUploadingCount(loading ? (fileCount ?? 1) : 0);
553
+ setDocUploadingCount(loading ? fileCount ?? 1 : 0);
513
554
  },
514
555
  []
515
556
  );
516
557
  const handleImageLoadingChange = useCallback(
517
558
  (loading: boolean, fileCount?: number) => {
518
559
  setIsImageLoading(loading);
519
- setImgUploadingCount(loading ? (fileCount ?? 1) : 0);
560
+ setImgUploadingCount(loading ? fileCount ?? 1 : 0);
520
561
  },
521
562
  []
522
563
  );
@@ -526,7 +567,7 @@ ${file.textAssetUrl || ''}
526
567
  }, [isLoading, uploadingFileCount, onUploadLoadingChange]);
527
568
 
528
569
  return (
529
- <div
570
+ <div
530
571
  className={cx('memori--unified-upload-wrapper', {
531
572
  'memori--dragging': isDragging,
532
573
  })}
@@ -536,7 +577,7 @@ ${file.textAssetUrl || ''}
536
577
  <input
537
578
  ref={unifiedInputRef}
538
579
  type="file"
539
- accept=".jpg,.jpeg,.png,.pdf,.txt,.json,.xlsx,.csv,.md,.html"
580
+ accept={`.jpg,.jpeg,.png,.pdf,.txt,.json,.xlsx,.csv,.md,.html,${officeNativeExtensions.join(',')}`}
540
581
  multiple
541
582
  className="memori--upload-file-input"
542
583
  onChange={handleFileInputChange}
@@ -557,7 +598,15 @@ ${file.textAssetUrl || ''}
557
598
  )}
558
599
  onClick={handleButtonClick}
559
600
  disabled={isLoading || hasReachedMediaLimit}
560
- title={t('upload.uploadFiles', { shortcut: /Mac|iPhone|iPod|iPad/i.test(navigator.platform) || navigator.userAgent.includes('Mac') ? 'Cmd' : 'Ctrl' }) ?? 'Upload files (drag & drop)'}
601
+ title={
602
+ t('upload.uploadFiles', {
603
+ shortcut:
604
+ /Mac|iPhone|iPod|iPad/i.test(navigator.platform) ||
605
+ navigator.userAgent.includes('Mac')
606
+ ? 'Cmd'
607
+ : 'Ctrl',
608
+ }) ?? 'Upload files (drag & drop)'
609
+ }
561
610
  >
562
611
  {isLoading ? (
563
612
  <Spin spinning className="memori--upload-icon" />
@@ -591,7 +640,6 @@ ${file.textAssetUrl || ''}
591
640
  onDocumentError={handleDocumentError}
592
641
  onValidateFile={validateDocumentFile}
593
642
  onValidatePayloadSize={validatePayloadSize}
594
- maxDocumentContentLength={effectivePerDocumentLimit}
595
643
  />
596
644
  </div>
597
645
 
@@ -619,7 +667,9 @@ ${file.textAssetUrl || ''}
619
667
  key={`${error.message}-${index}`}
620
668
  open={true}
621
669
  type={error.severity}
622
- title={t('upload.uploadNotification', { defaultValue: 'Upload notification' })}
670
+ title={t('upload.uploadNotification', {
671
+ defaultValue: 'Upload notification',
672
+ })}
623
673
  description={error.message}
624
674
  onClose={() => removeError(error.message)}
625
675
  width="350px"