@memori.ai/memori-react 8.17.2 → 8.18.0

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 (154) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +1 -0
  3. package/dist/components/Chat/Chat.d.ts +1 -0
  4. package/dist/components/Chat/Chat.js +2 -2
  5. package/dist/components/Chat/Chat.js.map +1 -1
  6. package/dist/components/ChatBubble/ChatBubble.js +2 -2
  7. package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
  8. package/dist/components/ChatInputs/ChatInputs.css +9 -3
  9. package/dist/components/ChatInputs/ChatInputs.d.ts +1 -0
  10. package/dist/components/ChatInputs/ChatInputs.js +69 -3
  11. package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
  12. package/dist/components/ChatTextArea/ChatTextArea.d.ts +1 -0
  13. package/dist/components/ChatTextArea/ChatTextArea.js +3 -3
  14. package/dist/components/ChatTextArea/ChatTextArea.js.map +1 -1
  15. package/dist/components/ContentPreviewModal/ContentPreviewModal.css +15 -0
  16. package/dist/components/ContentPreviewModal/ContentPreviewModal.d.ts +1 -0
  17. package/dist/components/ContentPreviewModal/ContentPreviewModal.js +1 -0
  18. package/dist/components/ContentPreviewModal/ContentPreviewModal.js.map +1 -1
  19. package/dist/components/FilePreview/FilePreview.css +17 -9
  20. package/dist/components/FilePreview/FilePreview.js +1 -2
  21. package/dist/components/FilePreview/FilePreview.js.map +1 -1
  22. package/dist/components/MediaWidget/MediaItemWidget.js +7 -1
  23. package/dist/components/MediaWidget/MediaItemWidget.js.map +1 -1
  24. package/dist/components/MemoriWidget/MemoriWidget.d.ts +2 -1
  25. package/dist/components/MemoriWidget/MemoriWidget.js +3 -2
  26. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  27. package/dist/components/UploadButton/UploadButton.d.ts +1 -0
  28. package/dist/components/UploadButton/UploadButton.js +50 -24
  29. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  30. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +5 -1
  31. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +76 -21
  32. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  33. package/dist/components/UploadButton/UploadImages/UploadImages.js +23 -4
  34. package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
  35. package/dist/components/layouts/fullpage.css +114 -0
  36. package/dist/components/layouts/website-assistant.css +17 -7
  37. package/dist/helpers/constants.d.ts +3 -1
  38. package/dist/helpers/constants.js +4 -2
  39. package/dist/helpers/constants.js.map +1 -1
  40. package/dist/helpers/tts/useTTS.js +3 -1
  41. package/dist/helpers/tts/useTTS.js.map +1 -1
  42. package/dist/helpers/utils.d.ts +1 -0
  43. package/dist/helpers/utils.js +16 -1
  44. package/dist/helpers/utils.js.map +1 -1
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +3 -2
  47. package/dist/index.js.map +1 -1
  48. package/dist/locales/de.json +12 -1
  49. package/dist/locales/en.json +15 -1
  50. package/dist/locales/es.json +12 -1
  51. package/dist/locales/fr.json +12 -1
  52. package/dist/locales/it.json +15 -1
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/esm/components/Chat/Chat.d.ts +1 -0
  56. package/esm/components/Chat/Chat.js +2 -2
  57. package/esm/components/Chat/Chat.js.map +1 -1
  58. package/esm/components/ChatBubble/ChatBubble.js +2 -2
  59. package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
  60. package/esm/components/ChatInputs/ChatInputs.css +9 -3
  61. package/esm/components/ChatInputs/ChatInputs.d.ts +1 -0
  62. package/esm/components/ChatInputs/ChatInputs.js +70 -4
  63. package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
  64. package/esm/components/ChatTextArea/ChatTextArea.d.ts +1 -0
  65. package/esm/components/ChatTextArea/ChatTextArea.js +4 -4
  66. package/esm/components/ChatTextArea/ChatTextArea.js.map +1 -1
  67. package/esm/components/ContentPreviewModal/ContentPreviewModal.css +15 -0
  68. package/esm/components/ContentPreviewModal/ContentPreviewModal.d.ts +1 -0
  69. package/esm/components/ContentPreviewModal/ContentPreviewModal.js +1 -0
  70. package/esm/components/ContentPreviewModal/ContentPreviewModal.js.map +1 -1
  71. package/esm/components/FilePreview/FilePreview.css +17 -9
  72. package/esm/components/FilePreview/FilePreview.js +1 -2
  73. package/esm/components/FilePreview/FilePreview.js.map +1 -1
  74. package/esm/components/MediaWidget/MediaItemWidget.js +7 -1
  75. package/esm/components/MediaWidget/MediaItemWidget.js.map +1 -1
  76. package/esm/components/MemoriWidget/MemoriWidget.d.ts +2 -1
  77. package/esm/components/MemoriWidget/MemoriWidget.js +3 -2
  78. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  79. package/esm/components/UploadButton/UploadButton.d.ts +1 -0
  80. package/esm/components/UploadButton/UploadButton.js +50 -24
  81. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  82. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +5 -1
  83. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +77 -22
  84. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  85. package/esm/components/UploadButton/UploadImages/UploadImages.js +23 -4
  86. package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
  87. package/esm/components/layouts/fullpage.css +114 -0
  88. package/esm/components/layouts/website-assistant.css +17 -7
  89. package/esm/helpers/constants.d.ts +3 -1
  90. package/esm/helpers/constants.js +3 -1
  91. package/esm/helpers/constants.js.map +1 -1
  92. package/esm/helpers/tts/useTTS.js +3 -1
  93. package/esm/helpers/tts/useTTS.js.map +1 -1
  94. package/esm/helpers/utils.d.ts +1 -0
  95. package/esm/helpers/utils.js +14 -0
  96. package/esm/helpers/utils.js.map +1 -1
  97. package/esm/index.d.ts +1 -0
  98. package/esm/index.js +3 -2
  99. package/esm/index.js.map +1 -1
  100. package/esm/locales/de.json +12 -1
  101. package/esm/locales/en.json +15 -1
  102. package/esm/locales/es.json +12 -1
  103. package/esm/locales/fr.json +12 -1
  104. package/esm/locales/it.json +15 -1
  105. package/esm/version.d.ts +1 -1
  106. package/esm/version.js +1 -1
  107. package/package.json +1 -1
  108. package/src/components/Chat/Chat.tsx +4 -0
  109. package/src/components/Chat/__snapshots__/Chat.test.tsx.snap +27 -81
  110. package/src/components/ChatBubble/ChatBubble.tsx +2 -2
  111. package/src/components/ChatBubble/__snapshots__/ChatBubble.test.tsx.snap +7 -21
  112. package/src/components/ChatInputs/ChatInputs.css +9 -3
  113. package/src/components/ChatInputs/ChatInputs.test.tsx +328 -1
  114. package/src/components/ChatInputs/ChatInputs.tsx +130 -8
  115. package/src/components/ChatTextArea/ChatTextArea.tsx +7 -3
  116. package/src/components/ContentPreviewModal/ContentPreviewModal.css +15 -0
  117. package/src/components/ContentPreviewModal/ContentPreviewModal.tsx +2 -0
  118. package/src/components/FilePreview/FilePreview.css +17 -9
  119. package/src/components/FilePreview/FilePreview.tsx +1 -7
  120. package/src/components/FilePreview/__snapshots__/FilePreview.test.tsx.snap +3 -3
  121. package/src/components/MediaWidget/MediaItemWidget.tsx +20 -2
  122. package/src/components/MemoriWidget/MemoriWidget.tsx +10 -2
  123. package/src/components/UploadButton/UploadButton.tsx +58 -31
  124. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +98 -30
  125. package/src/components/UploadButton/UploadImages/UploadImages.tsx +26 -5
  126. package/src/components/layouts/layouts.stories.tsx +1 -31
  127. package/src/components/layouts/website-assistant.css +17 -7
  128. package/src/helpers/constants.ts +16 -4
  129. package/src/helpers/tts/useTTS.ts +8 -2
  130. package/src/helpers/utils.ts +28 -0
  131. package/src/index.stories.tsx +3 -2
  132. package/src/index.tsx +5 -0
  133. package/src/locales/de.json +12 -1
  134. package/src/locales/en.json +15 -1
  135. package/src/locales/es.json +12 -1
  136. package/src/locales/fr.json +12 -1
  137. package/src/locales/it.json +15 -1
  138. package/src/version.ts +1 -1
  139. package/dist/components/AttachmentLinkModal/AttachmentLinkModal.css +0 -68
  140. package/dist/components/AttachmentLinkModal/AttachmentLinkModal.d.ts +0 -12
  141. package/dist/components/AttachmentLinkModal/AttachmentLinkModal.js +0 -59
  142. package/dist/components/AttachmentLinkModal/AttachmentLinkModal.js.map +0 -1
  143. package/dist/components/MediaWidget/LinkItemWidget.css +0 -46
  144. package/dist/components/MediaWidget/LinkItemWidget.d.ts +0 -28
  145. package/dist/components/MediaWidget/LinkItemWidget.js +0 -81
  146. package/dist/components/MediaWidget/LinkItemWidget.js.map +0 -1
  147. package/esm/components/AttachmentLinkModal/AttachmentLinkModal.css +0 -68
  148. package/esm/components/AttachmentLinkModal/AttachmentLinkModal.d.ts +0 -12
  149. package/esm/components/AttachmentLinkModal/AttachmentLinkModal.js +0 -56
  150. package/esm/components/AttachmentLinkModal/AttachmentLinkModal.js.map +0 -1
  151. package/esm/components/MediaWidget/LinkItemWidget.css +0 -46
  152. package/esm/components/MediaWidget/LinkItemWidget.d.ts +0 -28
  153. package/esm/components/MediaWidget/LinkItemWidget.js +0 -76
  154. package/esm/components/MediaWidget/LinkItemWidget.js.map +0 -1
@@ -26,8 +26,6 @@ FilePreviewProps) => {
26
26
  type?: string;
27
27
  } | null>(null);
28
28
 
29
- const [hoveredId, setHoveredId] = useState<string | null>(null);
30
-
31
29
  const getFileType = (filename: string, type?: string) => {
32
30
  // If type is explicitly provided, use it first
33
31
  if (type === 'image') {
@@ -136,8 +134,6 @@ FilePreviewProps) => {
136
134
  ? 'memori--preview-item--image'
137
135
  : 'memori--preview-item--document'
138
136
  }`}
139
- onMouseEnter={() => setHoveredId(file.id)}
140
- onMouseLeave={() => setHoveredId(null)}
141
137
  onClick={() => setSelectedFile(file)}
142
138
  >
143
139
  {isImageContent(file.content, file.type) ? (
@@ -160,9 +156,7 @@ FilePreviewProps) => {
160
156
  shape="rounded"
161
157
  icon={<CloseIcon />}
162
158
  danger
163
- className={`memori--remove-button ${
164
- hoveredId === file.id ? 'visible' : ''
165
- }`}
159
+ className="memori--remove-button"
166
160
  onClick={e => {
167
161
  e.stopPropagation();
168
162
  removeFile(file.id, file?.mediumID);
@@ -40,7 +40,7 @@ exports[`renders FilePreview with one file 1`] = `
40
40
  </span>
41
41
  </div>
42
42
  <button
43
- class="memori-button memori-button--rounded memori-button--padded memori-button--icon-only memori-button--danger memori--remove-button "
43
+ class="memori-button memori-button--rounded memori-button--padded memori-button--icon-only memori-button--danger memori--remove-button"
44
44
  >
45
45
  <span
46
46
  class="memori-button--icon"
@@ -105,7 +105,7 @@ exports[`renders FilePreview with two files 1`] = `
105
105
  </span>
106
106
  </div>
107
107
  <button
108
- class="memori-button memori-button--rounded memori-button--padded memori-button--icon-only memori-button--danger memori--remove-button "
108
+ class="memori-button memori-button--rounded memori-button--padded memori-button--icon-only memori-button--danger memori--remove-button"
109
109
  >
110
110
  <span
111
111
  class="memori-button--icon"
@@ -157,7 +157,7 @@ exports[`renders FilePreview with two files 1`] = `
157
157
  </span>
158
158
  </div>
159
159
  <button
160
- class="memori-button memori-button--rounded memori-button--padded memori-button--icon-only memori-button--danger memori--remove-button "
160
+ class="memori-button memori-button--rounded memori-button--padded memori-button--icon-only memori-button--danger memori--remove-button"
161
161
  >
162
162
  <span
163
163
  class="memori-button--icon"
@@ -265,6 +265,22 @@ export const RenderMediaItem = memo(function RenderMediaItem({
265
265
  </div>
266
266
  );
267
267
 
268
+ // Plain text: snippet preview (same UX as other media / code)
269
+ case 'text/plain':
270
+ return medium.content ? (
271
+ <Snippet preview medium={medium} />
272
+ ) : (
273
+ <DocumentCard
274
+ title={medium.title || 'File'}
275
+ badge={getFileExtensionFromMime(medium.mimeType)}
276
+ meta={(() => {
277
+ const size = getContentSize(medium);
278
+ return size != null && size > 0 ? formatBytes(size) : null;
279
+ })()}
280
+ icon={<File className="memori-media-item--document-icon-svg" />}
281
+ />
282
+ );
283
+
268
284
  // HTML files are now handled as file cards (rendered above in the isFile check)
269
285
  // This case is kept for backwards compatibility but should not be reached
270
286
  case 'text/html':
@@ -449,8 +465,10 @@ export const RenderMediaItem = memo(function RenderMediaItem({
449
465
  );
450
466
  }
451
467
 
452
- // Inline previews for code snippets: open in new tab (blob URL if no resource URL)
453
- if (isCodeSnippet && item.content && item.mediumID && _onClick) {
468
+ // Inline previews for code snippets and plain text: Card with preview, click opens modal (same as other media)
469
+ const isPreviewableText =
470
+ (isCodeSnippet || item.mimeType === 'text/plain') && !!item.content;
471
+ if (isPreviewableText && item.mediumID && _onClick) {
454
472
  return (
455
473
  <div
456
474
  className="memori-media-item--link"
@@ -433,6 +433,8 @@ export interface Props {
433
433
  showFunctionCache?: boolean;
434
434
  authToken?: string;
435
435
  __WEBCOMPONENT__?: boolean;
436
+ /** Override total document payload and per-document content limit (character count). Default from constants. */
437
+ maxTotalMessagePayload?: number;
436
438
  }
437
439
 
438
440
  const MemoriWidget = ({
@@ -488,6 +490,7 @@ const MemoriWidget = ({
488
490
  autoStart = false,
489
491
  applyVarsToRoot = false,
490
492
  showFunctionCache = false,
493
+ maxTotalMessagePayload,
491
494
  }: Props) => {
492
495
  const { t, i18n } = useTranslation();
493
496
 
@@ -1887,9 +1890,13 @@ const MemoriWidget = ({
1887
1890
  defaultSpeakerActive ?? integrationConfig?.defaultSpeakerActive ?? true
1888
1891
  );
1889
1892
 
1890
- // Helper function to check if audio should be played
1893
+ // Helper function to check if audio should be played.
1894
+ // When defaultEnableAudio is false, default to muted so we never play before the sync effect runs (avoids audio on first conversation start when audio is disabled).
1891
1895
  const shouldPlayAudio = (text?: string) => {
1892
- const currentSpeakerMuted = getLocalConfig('muteSpeaker', false);
1896
+ const currentSpeakerMuted = getLocalConfig(
1897
+ 'muteSpeaker',
1898
+ !defaultEnableAudio
1899
+ );
1893
1900
  console.log('[MemoriWidget] shouldPlayAudio', currentSpeakerMuted);
1894
1901
  return (
1895
1902
  text &&
@@ -3017,6 +3024,7 @@ const MemoriWidget = ({
3017
3024
  userAvatar,
3018
3025
  experts,
3019
3026
  useMathFormatting: applyMathFormatting,
3027
+ maxTotalMessagePayload,
3020
3028
  };
3021
3029
 
3022
3030
  const integrationBackground =
@@ -10,9 +10,7 @@ import UploadImages from './UploadImages/UploadImages';
10
10
  import { useTranslation } from 'react-i18next';
11
11
  import memoriApiClient from '@memori.ai/memori-api-client';
12
12
  import Tooltip from '../ui/Tooltip';
13
-
14
- // Constants
15
- const MAX_MEDIA = 10;
13
+ import { MAX_DOCUMENTS_PER_MESSAGE } from '../../helpers/constants';
16
14
 
17
15
  // Props interface
18
16
  interface UploadManagerProps {
@@ -29,6 +27,8 @@ interface UploadManagerProps {
29
27
  type?: string;
30
28
  }[];
31
29
  memoriID?: string;
30
+ /** Override total document payload limit (character count). */
31
+ maxTotalMessagePayload?: number;
32
32
  }
33
33
 
34
34
  const UploadButton: React.FC<UploadManagerProps> = ({
@@ -39,6 +39,7 @@ const UploadButton: React.FC<UploadManagerProps> = ({
39
39
  setDocumentPreviewFiles,
40
40
  documentPreviewFiles,
41
41
  memoriID = '',
42
+ maxTotalMessagePayload,
42
43
  }) => {
43
44
  // State
44
45
  const [isLoading, setIsLoading] = useState(false);
@@ -57,7 +58,7 @@ const UploadButton: React.FC<UploadManagerProps> = ({
57
58
 
58
59
  // Calculate total media count
59
60
  const currentMediaCount = documentPreviewFiles.length;
60
- const remainingSlots = MAX_MEDIA - currentMediaCount;
61
+ const remainingSlots = MAX_DOCUMENTS_PER_MESSAGE - currentMediaCount;
61
62
  const hasReachedMediaLimit = remainingSlots <= 0;
62
63
 
63
64
  // Error handling
@@ -104,15 +105,12 @@ const UploadButton: React.FC<UploadManagerProps> = ({
104
105
  const fileArray = Array.from(files);
105
106
  if (fileArray.length === 0) return;
106
107
 
107
- const imageFiles: File[] = [];
108
- const documentFiles: File[] = [];
109
-
110
- // Separate files by type
108
+ const supportedFiles: File[] = [];
111
109
  fileArray.forEach(file => {
112
110
  if (isImageFile(file)) {
113
- imageFiles.push(file);
111
+ supportedFiles.push(file);
114
112
  } else if (isDocumentFile(file)) {
115
- documentFiles.push(file);
113
+ supportedFiles.push(file);
116
114
  } else {
117
115
  addErrorRef.current({
118
116
  message: `File "${file.name}" is not a supported image or document type`,
@@ -121,25 +119,44 @@ const UploadButton: React.FC<UploadManagerProps> = ({
121
119
  }
122
120
  });
123
121
 
124
- // Calculate total files to be added
125
- const totalFilesToAdd = imageFiles.length + documentFiles.length;
126
- const newTotalCount = currentMediaCountRef.current + totalFilesToAdd;
122
+ const totalSupported = supportedFiles.length;
123
+ if (totalSupported === 0) return;
127
124
 
128
- // Check if adding these files would exceed the limit
129
- if (newTotalCount > MAX_MEDIA) {
125
+ const remainingSlots = MAX_DOCUMENTS_PER_MESSAGE - currentMediaCountRef.current;
126
+ if (remainingSlots <= 0) {
130
127
  addErrorRef.current({
131
- message: `Maximum ${MAX_MEDIA} media files allowed.`,
128
+ message: `Maximum ${MAX_DOCUMENTS_PER_MESSAGE} media files allowed.`,
132
129
  severity: 'error',
133
130
  });
134
131
  return;
135
132
  }
136
133
 
134
+ const toProcess = supportedFiles.slice(0, remainingSlots);
135
+ const imageFiles = toProcess.filter(f => isImageFile(f));
136
+ const documentFiles = toProcess.filter(f => isDocumentFile(f));
137
+
138
+ if (totalSupported > remainingSlots) {
139
+ const skipped = totalSupported - remainingSlots;
140
+ addErrorRef.current({
141
+ message:
142
+ t('upload.filesNotAddedMaxAllowed', {
143
+ count: skipped,
144
+ max: MAX_DOCUMENTS_PER_MESSAGE,
145
+ defaultValue: `${skipped} file(s) not added (maximum ${MAX_DOCUMENTS_PER_MESSAGE} files allowed).`,
146
+ }) ?? `${skipped} file(s) not added (maximum ${MAX_DOCUMENTS_PER_MESSAGE} files allowed).`,
147
+ severity: 'info',
148
+ });
149
+ }
150
+
137
151
  // Process images
138
152
  if (imageFiles.length > 0) {
139
153
  if (!isMediaAcceptedRef.current) {
140
154
  addErrorRef.current({
141
155
  message:
142
- t('upload.mediaNotAccepted') ?? 'Media uploads are not accepted',
156
+ t('upload.imagesNotAddedMediaNotAccepted', {
157
+ count: imageFiles.length,
158
+ defaultValue: `${imageFiles.length} image(s) not added (media uploads not accepted).`,
159
+ }) ?? `${imageFiles.length} image(s) not added (media uploads not accepted).`,
143
160
  severity: 'info',
144
161
  });
145
162
  } else {
@@ -157,7 +174,11 @@ const UploadButton: React.FC<UploadManagerProps> = ({
157
174
 
158
175
  // Only proceed if we successfully added files
159
176
  if (dataTransfer.files.length > 0) {
160
- imageInput.files = dataTransfer.files;
177
+ try {
178
+ imageInput.files = dataTransfer.files;
179
+ } catch {
180
+ // JSDOM and some environments do not allow assigning to input.files
181
+ }
161
182
  const changeEvent = new Event('change', { bubbles: true });
162
183
  imageInput.dispatchEvent(changeEvent);
163
184
  }
@@ -181,7 +202,11 @@ const UploadButton: React.FC<UploadManagerProps> = ({
181
202
 
182
203
  // Only proceed if we successfully added files
183
204
  if (dataTransfer.files.length > 0) {
184
- documentInput.files = dataTransfer.files;
205
+ try {
206
+ documentInput.files = dataTransfer.files;
207
+ } catch {
208
+ // JSDOM and some environments do not allow assigning to input.files
209
+ }
185
210
  const changeEvent = new Event('change', { bubbles: true });
186
211
  documentInput.dispatchEvent(changeEvent);
187
212
  }
@@ -410,7 +435,7 @@ ${file.content}
410
435
  return true;
411
436
  };
412
437
 
413
- // Validate total payload size
438
+ // Validate total payload size. Returns result so caller can avoid showing this error when truncation was already shown.
414
439
  const validatePayloadSize = (
415
440
  newDocuments: {
416
441
  name: string;
@@ -418,8 +443,9 @@ ${file.content}
418
443
  content: string;
419
444
  mimeType: string;
420
445
  }[]
421
- ): boolean => {
446
+ ): { valid: boolean; message?: string } => {
422
447
  const { MAX_TOTAL_MESSAGE_PAYLOAD } = require('../../helpers/constants');
448
+ const limit = maxTotalMessagePayload ?? MAX_TOTAL_MESSAGE_PAYLOAD;
423
449
 
424
450
  const existingDocuments = documentPreviewFiles.filter(
425
451
  (file: any) => file.type === 'document'
@@ -431,15 +457,15 @@ ${file.content}
431
457
  0
432
458
  );
433
459
 
434
- if (totalPayloadSize > MAX_TOTAL_MESSAGE_PAYLOAD) {
435
- addError({
436
- message: `Total document content exceeds ${MAX_TOTAL_MESSAGE_PAYLOAD} characters limit. Please remove some documents.`,
437
- severity: 'error',
460
+ if (totalPayloadSize > limit) {
461
+ const msg = t('upload.contextSizeExceedsLimit', {
462
+ defaultValue:
463
+ 'Context size exceeds the limit. Try reducing the number of files or content in the conversation.',
438
464
  });
439
- return false;
465
+ return { valid: false, message: typeof msg === 'string' ? msg : String(msg) };
440
466
  }
441
467
 
442
- return true;
468
+ return { valid: true };
443
469
  };
444
470
 
445
471
  // Handle document upload errors
@@ -544,7 +570,7 @@ ${file.content}
544
570
  'memori--document-count-full': hasReachedMediaLimit,
545
571
  })}
546
572
  >
547
- {currentMediaCount}/{MAX_MEDIA}
573
+ {currentMediaCount}/{MAX_DOCUMENTS_PER_MESSAGE}
548
574
  </div>
549
575
  )}
550
576
 
@@ -552,12 +578,13 @@ ${file.content}
552
578
  <div className="memori--hidden-uploader" ref={documentRef}>
553
579
  <UploadDocuments
554
580
  setDocumentPreviewFiles={handleDocumentFiles}
555
- maxDocuments={MAX_MEDIA}
581
+ maxDocuments={MAX_DOCUMENTS_PER_MESSAGE}
556
582
  documentPreviewFiles={documentPreviewFiles}
557
583
  onLoadingChange={handleLoadingChange}
558
584
  onDocumentError={handleDocumentError}
559
585
  onValidateFile={validateDocumentFile}
560
586
  onValidatePayloadSize={validatePayloadSize}
587
+ maxTotalMessagePayload={maxTotalMessagePayload}
561
588
  />
562
589
  </div>
563
590
 
@@ -570,7 +597,7 @@ ${file.content}
570
597
  documentPreviewFiles={documentPreviewFiles}
571
598
  isMediaAccepted={isMediaAccepted}
572
599
  onLoadingChange={handleLoadingChange}
573
- maxImages={MAX_MEDIA}
600
+ maxImages={MAX_DOCUMENTS_PER_MESSAGE}
574
601
  memoriID={memoriID}
575
602
  onImageError={handleImageError}
576
603
  onValidateImageFile={validateImageFile}
@@ -585,7 +612,7 @@ ${file.content}
585
612
  key={`${error.message}-${index}`}
586
613
  open={true}
587
614
  type={error.severity}
588
- title={'Upload notification'}
615
+ title={t('upload.uploadNotification', { defaultValue: 'Upload notification' })}
589
616
  description={error.message}
590
617
  onClose={() => removeError(error.message)}
591
618
  width="350px"
@@ -3,7 +3,11 @@ import cx from 'classnames';
3
3
  import Spin from '../../ui/Spin';
4
4
  import { DocumentIcon } from '../../icons/Document';
5
5
  import Modal from '../../ui/Modal';
6
- import { MAX_DOCUMENT_CONTENT_LENGTH } from '../../../helpers/constants';
6
+ import { useTranslation } from 'react-i18next';
7
+ import {
8
+ MAX_DOCUMENT_CONTENT_LENGTH,
9
+ MAX_TOTAL_MESSAGE_PAYLOAD,
10
+ } from '../../../helpers/constants';
7
11
 
8
12
  // Types
9
13
  type PreviewFile = {
@@ -51,7 +55,9 @@ interface UploadDocumentsProps {
51
55
  content: string;
52
56
  mimeType: string;
53
57
  }[]
54
- ) => boolean;
58
+ ) => boolean | { valid: boolean; message?: string };
59
+ /** Same as total payload: overrides per-document content limit (character count). */
60
+ maxTotalMessagePayload?: number;
55
61
  }
56
62
 
57
63
  const UploadDocuments: React.FC<UploadDocumentsProps> = ({
@@ -62,7 +68,10 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
62
68
  onDocumentError,
63
69
  onValidateFile,
64
70
  onValidatePayloadSize,
71
+ maxTotalMessagePayload,
65
72
  }) => {
73
+ const { t } = useTranslation();
74
+
66
75
  // State
67
76
  const [isLoading, setIsLoading] = useState(false);
68
77
  const [selectedFile, setSelectedFile] = useState<PreviewFile | null>(null);
@@ -85,7 +94,7 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
85
94
  return true;
86
95
  };
87
96
 
88
- // Validate total payload size
97
+ // Validate total payload size (returns result object for conditional error display)
89
98
  const validatePayloadSize = (
90
99
  newDocuments: {
91
100
  name: string;
@@ -93,11 +102,15 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
93
102
  content: string;
94
103
  mimeType: string;
95
104
  }[]
96
- ): boolean => {
105
+ ): { valid: boolean; message?: string } => {
97
106
  if (onValidatePayloadSize) {
98
- return onValidatePayloadSize(newDocuments);
107
+ const result = onValidatePayloadSize(newDocuments);
108
+ if (typeof result === 'boolean') {
109
+ return result ? { valid: true } : { valid: false, message: '' };
110
+ }
111
+ return result;
99
112
  }
100
- return true;
113
+ return { valid: true };
101
114
  };
102
115
 
103
116
  const extractTextFromPDF = async (file: File): Promise<string> => {
@@ -212,7 +225,9 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
212
225
  }
213
226
  };
214
227
 
215
- const processDocumentFile = async (file: File): Promise<string | null> => {
228
+ const processDocumentFile = async (
229
+ file: File
230
+ ): Promise<{ text: string | null; wasTruncated: boolean }> => {
216
231
  const fileExt = file.name.split('.').pop()?.toLowerCase() || '';
217
232
 
218
233
  try {
@@ -226,22 +241,28 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
226
241
  text = await extractTextFromXLSX(file);
227
242
  }
228
243
 
229
- if (text && text.length > MAX_DOCUMENT_CONTENT_LENGTH) {
244
+ const perDocumentLimit = maxTotalMessagePayload ?? MAX_DOCUMENT_CONTENT_LENGTH;
245
+ let wasTruncated = false;
246
+ if (text && text.length > perDocumentLimit) {
230
247
  console.warn(
231
248
  'Document content exceeds length limit:',
232
249
  text.length,
233
250
  '>',
234
- MAX_DOCUMENT_CONTENT_LENGTH
251
+ perDocumentLimit
235
252
  );
236
253
  onDocumentError?.({
237
- message: `File "${file.name}" content exceeds ${MAX_DOCUMENT_CONTENT_LENGTH} characters and was truncated`,
238
- severity: 'warning',
254
+ message: t('upload.contextSizeExceedsLimit', {
255
+ defaultValue:
256
+ 'Context size exceeds the limit. Try reducing the number of files or content in the conversation.',
257
+ }),
258
+ severity: 'error',
239
259
  });
260
+ wasTruncated = true;
240
261
  text =
241
- text.substring(0, MAX_DOCUMENT_CONTENT_LENGTH) +
262
+ text.substring(0, perDocumentLimit) +
242
263
  '\n\n[Content truncated due to size limits]';
243
264
  }
244
- return text;
265
+ return { text, wasTruncated };
245
266
  } catch (error) {
246
267
  console.error('Document processing failed:', error);
247
268
  throw new Error(
@@ -260,13 +281,25 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
260
281
 
261
282
  // Check current total media count (images + documents)
262
283
  const currentMediaCount = documentPreviewFiles.length;
284
+ const remainingSlots = maxDocuments
285
+ ? Math.max(0, maxDocuments - currentMediaCount)
286
+ : files.length;
287
+ const filesToProcess = files.slice(0, remainingSlots);
263
288
 
264
- // Check if adding these files would exceed the total media limit
265
- if (maxDocuments && currentMediaCount + files.length > maxDocuments) {
289
+ if (files.length > filesToProcess.length) {
290
+ const skipped = files.length - filesToProcess.length;
266
291
  onDocumentError?.({
267
- message: `Maximum ${maxDocuments} media files allowed. You can upload ${Math.max(0, maxDocuments - currentMediaCount)} more file${maxDocuments - currentMediaCount !== 1 ? 's' : ''}.`,
268
- severity: 'error',
292
+ message:
293
+ t('upload.documentsNotAddedMaxAllowed', {
294
+ count: skipped,
295
+ max: maxDocuments ?? 10,
296
+ defaultValue: `${skipped} document(s) not added (maximum ${maxDocuments ?? 10} files allowed).`,
297
+ }) ?? `${skipped} document(s) not added (maximum ${maxDocuments ?? 10} files allowed).`,
298
+ severity: 'info',
269
299
  });
300
+ }
301
+
302
+ if (filesToProcess.length === 0) {
270
303
  if (documentInputRef.current) {
271
304
  documentInputRef.current.value = '';
272
305
  }
@@ -282,8 +315,15 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
282
315
  content: string;
283
316
  mimeType: string;
284
317
  }[] = [];
285
-
286
- for (const file of files) {
318
+ let hadTruncation = false;
319
+ let skippedDueToPayload = 0;
320
+ const payloadLimit =
321
+ maxTotalMessagePayload ?? MAX_TOTAL_MESSAGE_PAYLOAD;
322
+ const existingTotal = documentPreviewFiles
323
+ .filter((f: any) => f.type === 'document')
324
+ .reduce((sum: number, f: any) => sum + (f.content?.length ?? 0), 0);
325
+
326
+ for (const file of filesToProcess) {
287
327
  if (!validateDocumentFile(file)) {
288
328
  continue;
289
329
  }
@@ -291,15 +331,32 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
291
331
  const fileId = Math.random().toString(36).substr(2, 9);
292
332
 
293
333
  try {
294
- const text = await processDocumentFile(file);
334
+ const { text, wasTruncated } = await processDocumentFile(file);
335
+ if (wasTruncated) hadTruncation = true;
295
336
 
296
337
  if (text) {
338
+ const processedSum = processedFiles.reduce(
339
+ (s, d) => s + d.content.length,
340
+ 0
341
+ );
342
+ if (existingTotal + processedSum + text.length > payloadLimit) {
343
+ skippedDueToPayload += 1;
344
+ continue;
345
+ }
297
346
  processedFiles.push({
298
347
  name: file.name,
299
348
  id: fileId,
300
349
  content: text,
301
350
  mimeType: file.type,
302
351
  });
352
+ } else {
353
+ onDocumentError?.({
354
+ message: t('upload.documentNotReadable', {
355
+ name: file.name,
356
+ defaultValue: `Document "${file.name}" could not be read or was empty.`,
357
+ }),
358
+ severity: 'info',
359
+ });
303
360
  }
304
361
  } catch (error) {
305
362
  console.error('File processing error:', error);
@@ -312,17 +369,28 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
312
369
  }
313
370
  }
314
371
 
315
- // Add new documents to existing ones
316
- if (processedFiles.length > 0) {
317
- // Validate total payload size
318
- if (!validatePayloadSize(processedFiles)) {
319
- setIsLoading(false);
320
- if (documentInputRef.current) {
321
- documentInputRef.current.value = '';
322
- }
323
- return;
324
- }
372
+ if (skippedDueToPayload > 0) {
373
+ onDocumentError?.({
374
+ message: t('upload.documentsNotAddedContextSize', {
375
+ count: skippedDueToPayload,
376
+ defaultValue: `${skippedDueToPayload} document(s) not added (context size limit).`,
377
+ }),
378
+ severity: 'info',
379
+ });
380
+ }
325
381
 
382
+ if (processedFiles.length === 0 && filesToProcess.length > 0) {
383
+ onDocumentError?.({
384
+ message: t('upload.noDocumentsAdded', {
385
+ defaultValue:
386
+ 'No documents could be added. Check file type, size (max 10MB), and that the file is readable.',
387
+ }),
388
+ severity: 'info',
389
+ });
390
+ }
391
+
392
+ // Add new documents to existing ones (only those that fit within payload)
393
+ if (processedFiles.length > 0) {
326
394
  const existingDocuments = documentPreviewFiles.filter(
327
395
  (file: any) => file.type === 'document'
328
396
  );
@@ -90,18 +90,31 @@ const UploadImages: React.FC<UploadImagesProps> = ({
90
90
  const files = Array.from(e.target.files || []);
91
91
  if (files.length === 0) return;
92
92
 
93
- // Check if adding these files would exceed the total media limit
94
- if (currentMediaCount + files.length > maxImages) {
93
+ const remainingSlots = Math.max(0, maxImages - currentMediaCount);
94
+ const filesToProcess = files.slice(0, remainingSlots);
95
+
96
+ if (files.length > filesToProcess.length) {
97
+ const skipped = files.length - filesToProcess.length;
95
98
  onImageError?.({
96
99
  message:
97
- `Maximum ${maxImages} media files allowed. You can upload ${Math.max(0, maxImages - currentMediaCount)} more file${maxImages - currentMediaCount !== 1 ? 's' : ''}.`,
98
- severity: 'error',
100
+ t('upload.imagesNotAddedMaxAllowed', {
101
+ count: skipped,
102
+ max: maxImages,
103
+ defaultValue: `${skipped} image(s) not added (maximum ${maxImages} files allowed).`,
104
+ }) ?? `${skipped} image(s) not added (maximum ${maxImages} files allowed).`,
105
+ severity: 'info',
99
106
  });
107
+ }
108
+
109
+ if (filesToProcess.length === 0) {
110
+ if (imageInputRef.current) {
111
+ imageInputRef.current.value = '';
112
+ }
100
113
  return;
101
114
  }
102
115
 
103
116
  // Validate all files first
104
- const validFiles = files.filter(file => {
117
+ const validFiles = filesToProcess.filter(file => {
105
118
  if (!validateImageFile(file)) {
106
119
  return false;
107
120
  }
@@ -109,6 +122,14 @@ const UploadImages: React.FC<UploadImagesProps> = ({
109
122
  });
110
123
 
111
124
  if (validFiles.length === 0) {
125
+ onImageError?.({
126
+ message:
127
+ t('upload.noImagesAdded', {
128
+ defaultValue:
129
+ 'No images could be added. Check file type (.jpg, .jpeg, .png) and size (max 10MB).',
130
+ }) ?? 'No images could be added. Check file type (.jpg, .jpeg, .png) and size (max 10MB).',
131
+ severity: 'info',
132
+ });
112
133
  if (imageInputRef.current) {
113
134
  imageInputRef.current.value = '';
114
135
  }
@@ -49,40 +49,10 @@ DefaultLayout.args = {
49
49
  sessionID: '' as string | undefined,
50
50
  showUpload: true,
51
51
  showReasoning: false,
52
- showLogin: true
52
+ showLogin: true,
53
53
  };
54
54
 
55
55
 
56
- const EnglishLayout = Template.bind({});
57
- EnglishLayout.args = {
58
- memoriName: 'Prova34',
59
- ownerUserName: 'andrea.patini',
60
- memoriID: 'a37fd990-e159-421e-888d-38a1bb5df294',
61
- tenantID: 'aisuru-staging.aclambda.online',
62
- ownerUserID: '91dbc9ba-b684-4fbe-9828-b5980af6cda9',
63
- engineURL: 'https://engine-staging.memori.ai/memori/v2',
64
- apiURL: 'https://backend-staging.memori.ai/api/v2',
65
- layout: 'FULLPAGE',
66
- uiLang: 'EN',
67
- spokenLang: 'EN',
68
- integrationID: '658fe9ac-136d-401f-bc57-f5af4d8d7012',
69
- };
70
-
71
- export const EnglishLayoutStory = Template.bind({});
72
- EnglishLayoutStory.args = {
73
- memoriName: 'Prova34',
74
- ownerUserName: 'andrea.patini',
75
- memoriID: 'a37fd990-e159-421e-888d-38a1bb5df294',
76
- tenantID: 'aisuru-staging.aclambda.online',
77
- ownerUserID: '91dbc9ba-b684-4fbe-9828-b5980af6cda9',
78
- engineURL: 'https://engine-staging.memori.ai/memori/v2',
79
- apiURL: 'https://backend-staging.memori.ai/api/v2',
80
- layout: 'FULLPAGE',
81
- uiLang: 'EN',
82
- spokenLang: 'EN',
83
- integrationID: '658fe9ac-136d-401f-bc57-f5af4d8d7012',
84
- };
85
-
86
56
 
87
57
  export const Default = Template.bind({});
88
58
  Default.args = {