@memori.ai/memori-react 8.38.5 → 8.38.7

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 (86) hide show
  1. package/CHANGELOG.md +19 -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 +1 -1
  5. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.d.ts +0 -1
  6. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.js +0 -1
  7. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.js.map +1 -1
  8. package/dist/components/FilePreview/FilePreview.js +20 -2
  9. package/dist/components/FilePreview/FilePreview.js.map +1 -1
  10. package/dist/components/MediaWidget/MediaItemWidget.js +13 -9
  11. package/dist/components/MediaWidget/MediaItemWidget.js.map +1 -1
  12. package/dist/components/MediaWidget/MediaItemWidget.utils.d.ts +1 -0
  13. package/dist/components/MediaWidget/MediaItemWidget.utils.js +27 -7
  14. package/dist/components/MediaWidget/MediaItemWidget.utils.js.map +1 -1
  15. package/dist/components/MobileSessionPanel/MobileSessionPanel.css +30 -4
  16. package/dist/components/MobileSessionPanel/MobileSessionPanel.d.ts +3 -2
  17. package/dist/components/MobileSessionPanel/MobileSessionPanel.js +16 -13
  18. package/dist/components/MobileSessionPanel/MobileSessionPanel.js.map +1 -1
  19. package/dist/components/UploadButton/UploadButton.js +21 -6
  20. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  21. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +45 -8
  22. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  23. package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js +5 -1
  24. package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
  25. package/dist/components/layouts/WebsiteAssistant/website-assistant.css +1 -3
  26. package/dist/components/layouts/fullpage.css +55 -21
  27. package/dist/helpers/constants.d.ts +3 -0
  28. package/dist/helpers/constants.js +24 -1
  29. package/dist/helpers/constants.js.map +1 -1
  30. package/dist/helpers/usePressTooltip.d.ts +13 -0
  31. package/dist/helpers/usePressTooltip.js +23 -0
  32. package/dist/helpers/usePressTooltip.js.map +1 -0
  33. package/dist/helpers/utils.d.ts +21 -0
  34. package/dist/helpers/utils.js +66 -1
  35. package/dist/helpers/utils.js.map +1 -1
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/esm/components/Chat/Chat.js +12 -24
  39. package/esm/components/Chat/Chat.js.map +1 -1
  40. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.css +1 -1
  41. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.d.ts +0 -1
  42. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.js +0 -1
  43. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.js.map +1 -1
  44. package/esm/components/FilePreview/FilePreview.js +22 -4
  45. package/esm/components/FilePreview/FilePreview.js.map +1 -1
  46. package/esm/components/MediaWidget/MediaItemWidget.js +15 -11
  47. package/esm/components/MediaWidget/MediaItemWidget.js.map +1 -1
  48. package/esm/components/MediaWidget/MediaItemWidget.utils.d.ts +1 -0
  49. package/esm/components/MediaWidget/MediaItemWidget.utils.js +25 -6
  50. package/esm/components/MediaWidget/MediaItemWidget.utils.js.map +1 -1
  51. package/esm/components/MobileSessionPanel/MobileSessionPanel.css +30 -4
  52. package/esm/components/MobileSessionPanel/MobileSessionPanel.d.ts +3 -2
  53. package/esm/components/MobileSessionPanel/MobileSessionPanel.js +17 -14
  54. package/esm/components/MobileSessionPanel/MobileSessionPanel.js.map +1 -1
  55. package/esm/components/UploadButton/UploadButton.js +21 -6
  56. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  57. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +45 -8
  58. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  59. package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js +5 -1
  60. package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
  61. package/esm/components/layouts/WebsiteAssistant/website-assistant.css +1 -3
  62. package/esm/components/layouts/fullpage.css +55 -21
  63. package/esm/helpers/constants.d.ts +3 -0
  64. package/esm/helpers/constants.js +23 -0
  65. package/esm/helpers/constants.js.map +1 -1
  66. package/esm/helpers/usePressTooltip.d.ts +13 -0
  67. package/esm/helpers/usePressTooltip.js +20 -0
  68. package/esm/helpers/usePressTooltip.js.map +1 -0
  69. package/esm/helpers/utils.d.ts +21 -0
  70. package/esm/helpers/utils.js +59 -0
  71. package/esm/helpers/utils.js.map +1 -1
  72. package/esm/version.d.ts +1 -1
  73. package/esm/version.js +1 -1
  74. package/package.json +1 -1
  75. package/src/components/Chat/Chat.tsx +19 -44
  76. package/src/components/FilePreview/FilePreview.tsx +26 -4
  77. package/src/components/MediaWidget/MediaItemWidget.tsx +24 -10
  78. package/src/components/MediaWidget/MediaItemWidget.utils.test.ts +45 -2
  79. package/src/components/MediaWidget/MediaItemWidget.utils.ts +37 -6
  80. package/src/components/UploadButton/UploadButton.tsx +28 -12
  81. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +48 -9
  82. package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +2 -2
  83. package/src/helpers/constants.ts +29 -1
  84. package/src/helpers/utils.test.ts +113 -0
  85. package/src/helpers/utils.ts +92 -0
  86. package/src/version.ts +1 -1
@@ -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 {
@@ -108,6 +109,7 @@ const UploadButton: React.FC<UploadManagerProps> = ({
108
109
  '.csv',
109
110
  '.md',
110
111
  '.html',
112
+ ...officeNativeExtensions,
111
113
  ];
112
114
  const fileExt = `.${file.name.split('.').pop()?.toLowerCase()}`;
113
115
  return documentExtensions.includes(fileExt);
@@ -401,18 +403,29 @@ const UploadButton: React.FC<UploadManagerProps> = ({
401
403
  const processedDocuments = files.map(file => {
402
404
  const escapedFileName = escapeAttributeValue(file.name);
403
405
 
404
- // Truncate only the content that gets inlined inside the message
405
- // <document_attachment> tag. The full text is still available via
406
- // textAssetUrl as an uploaded asset.
407
- const inlinedContent =
408
- file.content.length > effectivePerDocumentLimit
409
- ? file.content.substring(0, effectivePerDocumentLimit) +
410
- '\n\n[Content truncated due to size limits]'
411
- : file.content;
406
+ let formattedContent: string;
412
407
 
413
- const formattedContent = `<document_attachment filename="${escapedFileName}" type="${
414
- file.mimeType
415
- }">
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}">
411
+ </document_attachment>
412
+
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
+ }">
416
429
 
417
430
  ${inlinedContent}
418
431
 
@@ -421,6 +434,7 @@ ${inlinedContent}
421
434
  <attachment_link>
422
435
  ${file.textAssetUrl || ''}
423
436
  </attachment_link>`;
437
+ }
424
438
 
425
439
  return {
426
440
  name: file.name,
@@ -428,6 +442,7 @@ ${file.textAssetUrl || ''}
428
442
  content: formattedContent,
429
443
  type: 'document',
430
444
  mimeType: file.mimeType,
445
+ url: file.textAssetUrl,
431
446
  };
432
447
  });
433
448
 
@@ -446,6 +461,7 @@ ${file.textAssetUrl || ''}
446
461
  '.csv',
447
462
  '.md',
448
463
  '.html',
464
+ ...officeNativeExtensions,
449
465
  ];
450
466
  const MAX_FILE_SIZE = 15 * 1024 * 1024; // 15MB
451
467
 
@@ -562,7 +578,7 @@ ${file.textAssetUrl || ''}
562
578
  <input
563
579
  ref={unifiedInputRef}
564
580
  type="file"
565
- accept=".jpg,.jpeg,.png,.pdf,.txt,.json,.xlsx,.csv,.md,.html"
581
+ accept={`.jpg,.jpeg,.png,.pdf,.txt,.json,.xlsx,.csv,.md,.html,${officeNativeExtensions.join(',')}`}
566
582
  multiple
567
583
  className="memori--upload-file-input"
568
584
  onChange={handleFileInputChange}
@@ -5,6 +5,8 @@ import { DocumentIcon } from '../../icons/Document';
5
5
  import Modal from '../../ui/Modal';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import memoriApiClient from '@memori.ai/memori-api-client';
8
+ import { officeNativeExtensions } from '../../../helpers/constants';
9
+ import { isOfficeNativeFilename } from '../../../helpers/utils';
8
10
  // Types
9
11
  type PreviewFile = {
10
12
  name: string;
@@ -235,17 +237,20 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
235
237
 
236
238
  const processDocumentFile = async (
237
239
  file: File
238
- ): Promise<{ text: string | null }> => {
239
- const fileExt = file.name.split('.').pop()?.toLowerCase() || '';
240
+ ): Promise<{ text: string | null; uploadAsOriginal?: boolean }> => {
241
+ if (isOfficeNativeFilename(file.name)) {
242
+ return { text: null, uploadAsOriginal: true };
243
+ }
240
244
 
245
+ const ext = file.name.split('.').pop()?.toLowerCase() || '';
241
246
  try {
242
247
  let text: string | null = null;
243
248
 
244
- if (fileExt === 'pdf') {
249
+ if (ext === 'pdf') {
245
250
  text = await extractTextFromPDF(file);
246
- } else if (['txt', 'md', 'json', 'csv', 'html'].includes(fileExt)) {
251
+ } else if (['txt', 'md', 'json', 'csv', 'html'].includes(ext)) {
247
252
  text = await file.text();
248
- } else if (fileExt === 'xlsx') {
253
+ } else if (ext === 'xlsx') {
249
254
  text = await extractTextFromXLSX(file);
250
255
  }
251
256
 
@@ -295,7 +300,12 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
295
300
  throw new Error(response.resultMessage || 'Upload failed');
296
301
  }
297
302
 
298
- return response.asset?.assetURL;
303
+ const assetURL = response.asset?.assetURL;
304
+ if (!assetURL) {
305
+ throw new Error('Upload failed: missing asset URL');
306
+ }
307
+
308
+ return assetURL;
299
309
  };
300
310
 
301
311
  const handleDocumentUpload = async (
@@ -360,9 +370,38 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
360
370
  const fileId = Math.random().toString(36).substr(2, 9);
361
371
 
362
372
  try {
363
- const { text } = await processDocumentFile(file);
373
+ const { text, uploadAsOriginal } = await processDocumentFile(file);
374
+
375
+ if (uploadAsOriginal) {
376
+ // Office native format: upload the original file as asset, no text extraction
377
+ let assetUrl: string | undefined;
378
+ try {
379
+ assetUrl = await uploadAssetFile(file);
380
+ } catch (uploadError) {
381
+ console.error('Office asset upload failed:', uploadError);
382
+ onDocumentError?.({
383
+ message: t('upload.officeAssetUploadFailed', {
384
+ fileName: file.name,
385
+ defaultValue: `"${file.name}" could not be uploaded and was not added.`,
386
+ }),
387
+ severity: 'error',
388
+ });
389
+ }
364
390
 
365
- if (text) {
391
+ if (!assetUrl) {
392
+ activeCount--;
393
+ onLoadingChange?.(true, activeCount);
394
+ continue;
395
+ }
396
+
397
+ processedFiles.push({
398
+ name: file.name,
399
+ id: fileId,
400
+ content: '',
401
+ mimeType: file.type,
402
+ textAssetUrl: assetUrl,
403
+ });
404
+ } else if (text) {
366
405
  const baseName = file.name.replace(/\.[^/.]+$/, '') || file.name;
367
406
  const textFile = new File([text], `${baseName}.txt`, {
368
407
  type: 'text/plain',
@@ -429,7 +468,7 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
429
468
  <input
430
469
  ref={documentInputRef}
431
470
  type="file"
432
- accept=".pdf,.txt,.md,.json,.xlsx,.csv,.html"
471
+ accept={`.pdf,.txt,.md,.json,.xlsx,.csv,.html,${officeNativeExtensions.join(',')}`}
433
472
  multiple
434
473
  className="memori--upload-file-input"
435
474
  onChange={handleDocumentUpload}
@@ -6,7 +6,7 @@ exports[`renders UploadButton unchanged 1`] = `
6
6
  class="memori--unified-upload-wrapper"
7
7
  >
8
8
  <input
9
- accept=".jpg,.jpeg,.png,.pdf,.txt,.json,.xlsx,.csv,.md,.html"
9
+ accept=".jpg,.jpeg,.png,.pdf,.txt,.json,.xlsx,.csv,.md,.html,.doc,.docx,.xls,.xltx,.potx"
10
10
  class="memori--upload-file-input"
11
11
  multiple=""
12
12
  style="display: none;"
@@ -52,7 +52,7 @@ exports[`renders UploadButton unchanged 1`] = `
52
52
  class="memori--document-upload-wrapper"
53
53
  >
54
54
  <input
55
- accept=".pdf,.txt,.md,.json,.xlsx,.csv,.html"
55
+ accept=".pdf,.txt,.md,.json,.xlsx,.csv,.html,.doc,.docx,.xls,.xltx,.potx"
56
56
  class="memori--upload-file-input"
57
57
  multiple=""
58
58
  type="file"
@@ -35,7 +35,7 @@ export const getGroupedChatLanguages = () => {
35
35
  popularLanguageCodes.includes(lang.value)
36
36
  );
37
37
  const all = chatLanguages.filter(lang => !popularLanguageCodes.includes(lang.value));
38
- return {
38
+ return {
39
39
  popular,
40
40
  all,
41
41
  };
@@ -43,6 +43,15 @@ export const getGroupedChatLanguages = () => {
43
43
 
44
44
  export const uiLanguages = ['en', 'it', 'fr', 'es', 'de'];
45
45
 
46
+ /** Extensions uploaded as original Office binaries (no text extraction) */
47
+ export const officeNativeExtensions = [
48
+ '.doc',
49
+ '.docx',
50
+ '.xls',
51
+ '.xltx',
52
+ '.potx',
53
+ ] as const;
54
+
46
55
  export const allowedMediaTypes = [
47
56
  'image/jpeg',
48
57
  'image/png',
@@ -62,6 +71,25 @@ export const allowedMediaTypes = [
62
71
  'model/gltf-binary',
63
72
  ];
64
73
 
74
+ /** Short badge labels for Office document cards */
75
+ export const officeMimeShortLabels: Record<string, string> = {
76
+ 'application/msword': 'Word',
77
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word',
78
+ 'application/vnd.ms-excel': 'Excel',
79
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel',
80
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.template': 'Excel',
81
+ 'application/vnd.openxmlformats-officedocument.presentationml.template': 'PPT',
82
+ };
83
+
84
+ export const officeExtensionShortLabels: Record<string, string> = {
85
+ DOC: 'Word',
86
+ DOCX: 'Word',
87
+ XLS: 'Excel',
88
+ XLSX: 'Excel',
89
+ XLTX: 'Excel',
90
+ POTX: 'PPT',
91
+ };
92
+
65
93
  export const anonTag = '👤';
66
94
 
67
95
  export const prismSyntaxLangs = [
@@ -4,6 +4,11 @@ import {
4
4
  stripMarkdown,
5
5
  stripOutputTags,
6
6
  escapeHTML,
7
+ extractAttachmentLinks,
8
+ extractAttachmentLink,
9
+ isAssetOnlyDocumentAttachment,
10
+ isOfficeNativeFilename,
11
+ parseDocumentAttachmentsFromMessage,
7
12
  } from './utils';
8
13
 
9
14
  describe('Utils/difference', () => {
@@ -141,6 +146,114 @@ describe('utils/stripOutputTags', () => {
141
146
  });
142
147
  });
143
148
 
149
+ describe('utils/attachment helpers', () => {
150
+ it('extracts multiple attachment links in order', () => {
151
+ const input = [
152
+ '<document_attachment filename="a.docx" type="application/vnd.openxmlformats-officedocument.wordprocessingml.document">',
153
+ '</document_attachment>',
154
+ '<attachment_link>',
155
+ 'https://assets.example.com/a.docx',
156
+ '</attachment_link>',
157
+ '<document_attachment filename="b.pdf" type="application/pdf">',
158
+ 'pdf text',
159
+ '</document_attachment>',
160
+ '<attachment_link>https://assets.example.com/b.txt</attachment_link>',
161
+ ].join('\n');
162
+
163
+ expect(extractAttachmentLinks(input)).toEqual([
164
+ 'https://assets.example.com/a.docx',
165
+ 'https://assets.example.com/b.txt',
166
+ ]);
167
+ });
168
+
169
+ it('extracts a single attachment link', () => {
170
+ const input =
171
+ '<attachment_link>\nhttps://assets.example.com/file.docx\n</attachment_link>';
172
+ expect(extractAttachmentLink(input)).toBe(
173
+ 'https://assets.example.com/file.docx'
174
+ );
175
+ });
176
+
177
+ it('detects office native filenames', () => {
178
+ expect(isOfficeNativeFilename('report.doc')).toBe(true);
179
+ expect(isOfficeNativeFilename('report.docx')).toBe(true);
180
+ expect(isOfficeNativeFilename('budget.xls')).toBe(true);
181
+ expect(isOfficeNativeFilename('template.XLTX')).toBe(true);
182
+ expect(isOfficeNativeFilename('slides.potx')).toBe(true);
183
+ expect(isOfficeNativeFilename('notes.pdf')).toBe(false);
184
+ expect(isOfficeNativeFilename('data.xlsx')).toBe(false);
185
+ });
186
+
187
+ it('detects asset-only document attachments', () => {
188
+ expect(
189
+ isAssetOnlyDocumentAttachment({
190
+ title: 'report.docx',
191
+ content: '',
192
+ url: 'https://assets.example.com/file.docx',
193
+ })
194
+ ).toBe(true);
195
+ expect(
196
+ isAssetOnlyDocumentAttachment({
197
+ title: 'report.docx',
198
+ content: [
199
+ '<document_attachment filename="report.docx" type="application/vnd.openxmlformats-officedocument.wordprocessingml.document">',
200
+ '</document_attachment>',
201
+ '<attachment_link>https://assets.example.com/file.docx</attachment_link>',
202
+ ].join('\n'),
203
+ url: '',
204
+ })
205
+ ).toBe(true);
206
+ expect(
207
+ isAssetOnlyDocumentAttachment({
208
+ content: 'extracted text',
209
+ url: 'https://assets.example.com/file.txt',
210
+ })
211
+ ).toBe(false);
212
+ });
213
+
214
+ it('pairs each document attachment with its adjacent link', () => {
215
+ const input = [
216
+ '<attachment_link>https://assets.example.com/orphan.txt</attachment_link>',
217
+ '<document_attachment filename="a.docx" type="application/vnd.openxmlformats-officedocument.wordprocessingml.document">',
218
+ '</document_attachment>',
219
+ '<attachment_link>https://assets.example.com/a.docx</attachment_link>',
220
+ '<document_attachment filename="b.pdf" type="application/pdf">',
221
+ 'pdf text',
222
+ '</document_attachment>',
223
+ '<attachment_link>https://assets.example.com/b.txt</attachment_link>',
224
+ ].join('\n');
225
+
226
+ expect(parseDocumentAttachmentsFromMessage(input)).toEqual([
227
+ {
228
+ filename: 'a.docx',
229
+ type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
230
+ content: '',
231
+ url: 'https://assets.example.com/a.docx',
232
+ },
233
+ {
234
+ filename: 'b.pdf',
235
+ type: 'application/pdf',
236
+ content: 'pdf text',
237
+ url: 'https://assets.example.com/b.txt',
238
+ },
239
+ ]);
240
+ });
241
+
242
+ it('returns an empty url when no adjacent attachment link exists', () => {
243
+ const input =
244
+ '<document_attachment filename="a.docx" type="application/vnd.openxmlformats-officedocument.wordprocessingml.document"></document_attachment>';
245
+
246
+ expect(parseDocumentAttachmentsFromMessage(input)).toEqual([
247
+ {
248
+ filename: 'a.docx',
249
+ type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
250
+ content: '',
251
+ url: '',
252
+ },
253
+ ]);
254
+ });
255
+ });
256
+
144
257
  describe('utils/parsing combined', () => {
145
258
  it('should remove output tag from real message', () => {
146
259
  const result = escapeHTML(
@@ -1,6 +1,7 @@
1
1
  import { useState, useEffect, useRef, useMemo } from 'react';
2
2
  import { Material, MeshStandardMaterial, SkinnedMesh } from 'three';
3
3
  import * as THREE from 'three';
4
+ import { officeNativeExtensions } from './constants';
4
5
 
5
6
  export const hasTouchscreen = (): boolean => {
6
7
  let hasTouchScreen = false;
@@ -237,6 +238,67 @@ export const stripMarkdown = (text: string) => {
237
238
  return text;
238
239
  };
239
240
 
241
+ export const OFFICE_NATIVE_EXTENSIONS = officeNativeExtensions;
242
+
243
+ export const isOfficeNativeFilename = (filename: string): boolean => {
244
+ const ext = `.${filename.split('.').pop()?.toLowerCase() || ''}`;
245
+ return (officeNativeExtensions as readonly string[]).includes(ext);
246
+ };
247
+
248
+ export type ParsedDocumentAttachment = {
249
+ filename: string;
250
+ type: string;
251
+ content: string;
252
+ url: string;
253
+ };
254
+
255
+ const DOCUMENT_ATTACHMENT_REGEX =
256
+ /<document_attachment filename="([^"]+)" type="([^"]+)">([\s\S]*?)<\/document_attachment>/g;
257
+
258
+ const ATTACHMENT_LINK_AFTER_REGEX =
259
+ /<attachment_link>\s*([\s\S]*?)\s*<\/attachment_link>/;
260
+
261
+ export const parseDocumentAttachmentsFromMessage = (
262
+ text: string
263
+ ): ParsedDocumentAttachment[] => {
264
+ if (!text) return [];
265
+
266
+ const attachments: ParsedDocumentAttachment[] = [];
267
+ const regex = new RegExp(DOCUMENT_ATTACHMENT_REGEX.source, 'g');
268
+ let match;
269
+
270
+ while ((match = regex.exec(text)) !== null) {
271
+ const [, filename, type, content] = match;
272
+ const afterTag = text.slice(match.index + match[0].length);
273
+ const linkMatch = afterTag.match(ATTACHMENT_LINK_AFTER_REGEX);
274
+ const rawUrl = linkMatch?.[1]?.trim() || '';
275
+ const url = /^https?:\/\//.test(rawUrl) ? rawUrl : '';
276
+
277
+ attachments.push({
278
+ filename,
279
+ type,
280
+ content: content.trim(),
281
+ url,
282
+ });
283
+ }
284
+
285
+ return attachments;
286
+ };
287
+
288
+ export const extractAttachmentLinks = (content: string): string[] => {
289
+ return parseDocumentAttachmentsFromMessage(content).map(
290
+ attachment => attachment.url
291
+ );
292
+ };
293
+
294
+ export const extractAttachmentLink = (content: string): string | null => {
295
+ const match = content?.match(
296
+ /<attachment_link>\s*([\s\S]*?)\s*<\/attachment_link>/
297
+ );
298
+ const rawUrl = match?.[1]?.trim() || '';
299
+ return /^https?:\/\//.test(rawUrl) ? rawUrl : null;
300
+ };
301
+
240
302
  export const stripDocumentAttachmentTags = (text: string): string => {
241
303
  const documentAttachmentTagRegex = /<document_attachment filename="([^"]+)" type="([^"]+)">([\s\S]*?)<\/document_attachment>/g;
242
304
  return text
@@ -245,6 +307,36 @@ export const stripDocumentAttachmentTags = (text: string): string => {
245
307
  .replace(/<attachment_link>\s*[\s\S]*?\s*<\/attachment_link>/g, '');
246
308
  };
247
309
 
310
+ export const getDocumentAttachmentAssetUrl = (attachment: {
311
+ content?: string | null;
312
+ url?: string | null;
313
+ }): string =>
314
+ attachment.url?.trim() ||
315
+ extractAttachmentLink(attachment.content || '') ||
316
+ '';
317
+
318
+ export const isAssetOnlyDocumentAttachment = (attachment: {
319
+ title?: string;
320
+ name?: string;
321
+ content?: string | null;
322
+ url?: string | null;
323
+ }): boolean => {
324
+ const filename = attachment.title || attachment.name || '';
325
+ const assetUrl = getDocumentAttachmentAssetUrl(attachment);
326
+
327
+ if (!assetUrl) return false;
328
+
329
+ if (isOfficeNativeFilename(filename)) {
330
+ return true;
331
+ }
332
+
333
+ const strippedContent = attachment.content
334
+ ? stripDocumentAttachmentTags(attachment.content).trim()
335
+ : '';
336
+
337
+ return !strippedContent;
338
+ };
339
+
248
340
  export const stripOutputTags = (text: string): string => {
249
341
  const outputTagRegex = /<output.*?<\/output>/gs;
250
342
 
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
- export const version = '8.38.5';
2
+ export const version = '8.38.7';