@memori.ai/memori-react 7.33.3 → 7.34.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 (80) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/components/ChatBubble/ChatBubble.js +10 -2
  3. package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
  4. package/dist/components/ChatHistoryDrawer/ChatHistory.js +49 -29
  5. package/dist/components/ChatHistoryDrawer/ChatHistory.js.map +1 -1
  6. package/dist/components/MemoriWidget/MemoriWidget.js +5 -0
  7. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  8. package/dist/components/UploadButton/UploadButton.css +32 -12
  9. package/dist/components/UploadButton/UploadButton.js +96 -20
  10. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  11. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +12 -0
  12. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +80 -57
  13. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  14. package/dist/components/UploadButton/UploadImages/UploadImages.d.ts +5 -0
  15. package/dist/components/UploadButton/UploadImages/UploadImages.js +10 -42
  16. package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
  17. package/dist/helpers/constants.d.ts +3 -0
  18. package/dist/helpers/constants.js +4 -1
  19. package/dist/helpers/constants.js.map +1 -1
  20. package/dist/helpers/sanitizer.d.ts +6 -0
  21. package/dist/helpers/sanitizer.js +41 -0
  22. package/dist/helpers/sanitizer.js.map +1 -0
  23. package/dist/helpers/tts/ttsVoiceUtility.d.ts +158 -0
  24. package/dist/helpers/tts/ttsVoiceUtility.js +192 -0
  25. package/dist/helpers/tts/ttsVoiceUtility.js.map +1 -0
  26. package/dist/helpers/tts/useTTS.d.ts +26 -0
  27. package/dist/helpers/tts/useTTS.js +294 -0
  28. package/dist/helpers/tts/useTTS.js.map +1 -0
  29. package/dist/index.js +8 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/locales/en.json +3 -1
  32. package/dist/locales/it.json +2 -0
  33. package/esm/components/ChatBubble/ChatBubble.js +10 -2
  34. package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
  35. package/esm/components/ChatHistoryDrawer/ChatHistory.js +49 -29
  36. package/esm/components/ChatHistoryDrawer/ChatHistory.js.map +1 -1
  37. package/esm/components/MemoriWidget/MemoriWidget.js +5 -0
  38. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  39. package/esm/components/UploadButton/UploadButton.css +32 -12
  40. package/esm/components/UploadButton/UploadButton.js +96 -20
  41. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  42. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +12 -0
  43. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +81 -58
  44. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  45. package/esm/components/UploadButton/UploadImages/UploadImages.d.ts +5 -0
  46. package/esm/components/UploadButton/UploadImages/UploadImages.js +10 -42
  47. package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
  48. package/esm/helpers/constants.d.ts +3 -0
  49. package/esm/helpers/constants.js +3 -0
  50. package/esm/helpers/constants.js.map +1 -1
  51. package/esm/helpers/sanitizer.d.ts +6 -0
  52. package/esm/helpers/sanitizer.js +32 -0
  53. package/esm/helpers/sanitizer.js.map +1 -0
  54. package/esm/helpers/tts/ttsVoiceUtility.d.ts +158 -0
  55. package/esm/helpers/tts/ttsVoiceUtility.js +182 -0
  56. package/esm/helpers/tts/ttsVoiceUtility.js.map +1 -0
  57. package/esm/helpers/tts/useTTS.d.ts +26 -0
  58. package/esm/helpers/tts/useTTS.js +290 -0
  59. package/esm/helpers/tts/useTTS.js.map +1 -0
  60. package/esm/index.js +8 -0
  61. package/esm/index.js.map +1 -1
  62. package/esm/locales/en.json +3 -1
  63. package/esm/locales/it.json +2 -0
  64. package/package.json +2 -2
  65. package/src/__snapshots__/index.test.tsx.snap +41 -0
  66. package/src/components/ChatBubble/ChatBubble.stories.tsx +35 -0
  67. package/src/components/ChatBubble/ChatBubble.tsx +25 -7
  68. package/src/components/ChatHistoryDrawer/ChatHistory.stories.tsx +40 -1
  69. package/src/components/ChatHistoryDrawer/ChatHistory.tsx +106 -58
  70. package/src/components/MemoriWidget/MemoriWidget.tsx +9 -0
  71. package/src/components/UploadButton/UploadButton.css +32 -12
  72. package/src/components/UploadButton/UploadButton.tsx +145 -21
  73. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +105 -87
  74. package/src/components/UploadButton/UploadImages/UploadImages.tsx +12 -76
  75. package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +0 -6
  76. package/src/helpers/constants.ts +5 -0
  77. package/src/index.test.tsx +65 -0
  78. package/src/index.tsx +16 -0
  79. package/src/locales/en.json +3 -1
  80. package/src/locales/it.json +2 -0
@@ -377,7 +377,7 @@ const ChatHistoryDrawer = ({
377
377
  loginToken,
378
378
  }: Props) => {
379
379
  const { t } = useTranslation();
380
- const { getUserChatLogsByToken } = apiClient.chatLogs;
380
+ const { getUserChatLogsByTokenPaged } = apiClient.chatLogs;
381
381
 
382
382
  const textCurrentChat = `${t(
383
383
  'write_and_speak.conversationStartedLabel'
@@ -392,6 +392,7 @@ const ChatHistoryDrawer = ({
392
392
  const [chatLogs, setChatLogs] = useState<ChatLog[]>([]);
393
393
  const [selectedChatLog, setSelectedChatLog] = useState<ChatLog | null>(null);
394
394
  const [currentPage, setCurrentPage] = useState(1);
395
+ const [indexPage, setIndexPage] = useState(0);
395
396
  const [searchText, setSearchText] = useState('');
396
397
  const [isLoading, setIsLoading] = useState(false);
397
398
  const [error, setError] = useState<string | null>(null);
@@ -399,6 +400,8 @@ const ChatHistoryDrawer = ({
399
400
  const [dateRange, setDateRange] = useState<
400
401
  'today' | 'yesterday' | 'last_7_days' | 'last_30_days' | 'all'
401
402
  >('all');
403
+ const [totalItems, setTotalItems] = useState(0);
404
+ const [totalPages, setTotalPages] = useState(0);
402
405
 
403
406
  const formatDateForAPI = (date: Date) => {
404
407
  const year = date.getFullYear();
@@ -466,75 +469,111 @@ const ChatHistoryDrawer = ({
466
469
  }
467
470
  };
468
471
 
469
- const fetchChatLogs = useCallback(async () => {
470
- if (!loginToken) {
471
- setError(
472
- t('errorFetchingSession') ||
473
- 'Login token required to fetch chat history'
474
- );
475
- return;
476
- }
477
-
478
- setIsLoading(true);
479
- setError(null);
480
- try {
481
- let dateFrom = '';
482
- let dateTo = '';
483
- if (dateRange !== 'all') {
484
- const { dateFrom: dateFromTemp, dateTo: dateToTemp } = formatDateRangeForAPI(dateRange) ?? {};
485
- dateFrom = dateFromTemp ?? '';
486
- dateTo = dateToTemp ?? '';
472
+ const fetchChatLogs = useCallback(
473
+ async (
474
+ dateRangeValue:
475
+ | 'today'
476
+ | 'yesterday'
477
+ | 'last_7_days'
478
+ | 'last_30_days'
479
+ | 'all' = 'all'
480
+ ) => {
481
+ if (!loginToken) {
482
+ setError(
483
+ t('errorFetchingSession') ||
484
+ 'Login token required to fetch chat history'
485
+ );
486
+ return;
487
487
  }
488
488
 
489
- const res = await getUserChatLogsByToken(
490
- loginToken,
491
- memori.engineMemoriID ?? '',
492
- dateFrom ?? '',
493
- dateTo ?? ''
494
- );
495
- setChatLogs(
496
- res.chatLogs.sort((a, b) => {
489
+ // Calculate the index based on current page
490
+ const calculatedIndex = ITEMS_PER_PAGE * (currentPage - 1);
491
+ setIndexPage(calculatedIndex);
492
+
493
+ setIsLoading(true);
494
+ setError(null);
495
+ let response;
496
+ try {
497
+ if (dateRangeValue === 'all') {
498
+ response = await getUserChatLogsByTokenPaged(
499
+ loginToken,
500
+ memori.engineMemoriID ?? '',
501
+ calculatedIndex,
502
+ ITEMS_PER_PAGE,
503
+ undefined,
504
+ undefined,
505
+ false,
506
+ );
507
+ } else {
508
+ const { dateFrom, dateTo } =
509
+ formatDateRangeForAPI(dateRangeValue) ?? {};
510
+ response = await getUserChatLogsByTokenPaged(
511
+ loginToken,
512
+ memori.engineMemoriID ?? '',
513
+ calculatedIndex,
514
+ ITEMS_PER_PAGE,
515
+ dateFrom ?? undefined,
516
+ dateTo ?? undefined,
517
+ false,
518
+ );
519
+ }
520
+
521
+ const res = response;
522
+
523
+ // Sort the chat logs by date
524
+ const sortedChatLogs = res.chatLogs.sort((a: ChatLog, b: ChatLog) => {
497
525
  const dateA = Math.max(
498
- ...a.lines.map(l => new Date(l.timestamp).getTime())
526
+ ...a.lines.map((l: any) => new Date(l.timestamp).getTime())
499
527
  );
500
528
  const dateB = Math.max(
501
- ...b.lines.map(l => new Date(l.timestamp).getTime())
529
+ ...b.lines.map((l: any) => new Date(l.timestamp).getTime())
502
530
  );
503
531
  return sortOrder === 'desc' ? dateB - dateA : dateA - dateB;
504
- })
505
- );
506
- } catch (err) {
507
- setError(t('errorFetchingSession') || 'Error loading chat history');
508
- console.error('Error fetching chat logs:', err);
509
- } finally {
510
- setIsLoading(false);
532
+ });
533
+
534
+ setChatLogs(sortedChatLogs);
535
+ setTotalItems(res.count || sortedChatLogs.length);
536
+ setTotalPages(
537
+ Math.ceil((res.count || sortedChatLogs.length) / ITEMS_PER_PAGE)
538
+ );
539
+ } catch (err) {
540
+ setError(t('errorFetchingSession') || 'Error loading chat history');
541
+ console.error('Error fetching chat logs:', err);
542
+ } finally {
543
+ setIsLoading(false);
544
+ }
545
+ },
546
+ [loginToken, memori.engineMemoriID, currentPage, sortOrder, apiUrl]
547
+ );
548
+
549
+ useEffect(() => {
550
+ if (open) {
551
+ setCurrentPage(1); // Reset to first page when opening
552
+ setIndexPage(0); // Reset index to 0
511
553
  }
512
- }, [loginToken, memori.memoriID, sortOrder, dateRange]);
554
+ }, [open]);
513
555
 
556
+ // Reset to first page when changing sort order or date range
514
557
  useEffect(() => {
515
- if (open) fetchChatLogs();
516
- }, [open, fetchChatLogs]);
558
+ if (open) {
559
+ setCurrentPage(1);
560
+ setIndexPage(0);
561
+ }
562
+ }, [sortOrder, dateRange, open]);
563
+
564
+ // Fetch chat logs when current page or date range changes
565
+ useEffect(() => {
566
+ if (open && currentPage > 0) {
567
+ fetchChatLogs(dateRange);
568
+ }
569
+ }, [currentPage, dateRange, fetchChatLogs, open]);
517
570
 
518
571
  const debouncedSearch = useMemo(
519
572
  () => debounce((value: string) => setSearchText(value), DEBOUNCE_DELAY),
520
573
  []
521
574
  );
522
-
523
- const filteredChatLogs = useMemo(() => {
524
- return chatLogs.filter(
525
- c =>
526
- c.lines.some(l =>
527
- l.text.toLowerCase().includes(searchText.toLowerCase())
528
- ) && c.lines.length > 1
529
- );
530
- }, [chatLogs, searchText]);
531
-
532
- const totalPages = Math.ceil(filteredChatLogs.length / ITEMS_PER_PAGE);
533
-
534
- const paginatedChatLogs = useMemo(() => {
535
- const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
536
- return filteredChatLogs.slice(startIndex, startIndex + ITEMS_PER_PAGE);
537
- }, [filteredChatLogs, currentPage]);
575
+ // Remove client-side pagination since we're now using server-side pagination
576
+ const paginatedChatLogs = chatLogs;
538
577
 
539
578
  const handleResumeChat = async () => {
540
579
  if (selectedChatLog) {
@@ -613,7 +652,7 @@ const ChatHistoryDrawer = ({
613
652
  );
614
653
  }
615
654
 
616
- if (filteredChatLogs.length === 0) {
655
+ if (chatLogs.length === 0) {
617
656
  return (
618
657
  <div className="memori-chat-history-drawer--no-results">
619
658
  <p>
@@ -849,7 +888,10 @@ const ChatHistoryDrawer = ({
849
888
  <div className="memori-chat-history-drawer--pagination">
850
889
  <Button
851
890
  primary
852
- onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
891
+ onClick={() => {
892
+ setCurrentPage(p => Math.max(1, p - 1));
893
+ // fetchChatLogs will be triggered by the useEffect that depends on currentPage
894
+ }}
853
895
  disabled={currentPage === 1}
854
896
  className="memori-chat-history-drawer--pagination--button"
855
897
  >
@@ -863,7 +905,10 @@ const ChatHistoryDrawer = ({
863
905
  </span>
864
906
  <Button
865
907
  primary
866
- onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
908
+ onClick={() => {
909
+ setCurrentPage(p => Math.min(totalPages, p + 1));
910
+ // fetchChatLogs will be triggered by the useEffect that depends on currentPage
911
+ }}
867
912
  disabled={currentPage === totalPages}
868
913
  className="memori-chat-history-drawer--pagination--button"
869
914
  >
@@ -992,7 +1037,10 @@ const ChatHistoryDrawer = ({
992
1037
  | 'yesterday'
993
1038
  | 'last_7_days'
994
1039
  | 'last_30_days'
1040
+ | 'all'
995
1041
  );
1042
+ setCurrentPage(1);
1043
+ // fetchChatLogs will be triggered by the useEffect that depends on dateRange
996
1044
  }}
997
1045
  />
998
1046
  </div>
@@ -834,6 +834,15 @@ const MemoriWidget = ({
834
834
  msg = msg + ' ' + findMediaDocument.content;
835
835
  }
836
836
 
837
+ // Handle multiple documents
838
+ const mediaDocuments = media?.filter(
839
+ m => !m.mediumID && m.properties?.isAttachedFile
840
+ );
841
+ if (mediaDocuments && mediaDocuments.length > 0) {
842
+ const documentContents = mediaDocuments.map(doc => doc.content).join(' ');
843
+ msg = msg + ' ' + documentContents;
844
+ }
845
+
837
846
  // Add chat reference link to the message if it exists
838
847
  // if (chatLogID) {
839
848
  // msg =
@@ -380,10 +380,20 @@
380
380
  /* Positioned containers */
381
381
  .memori--error-message-container,
382
382
  .memori--login-tip {
383
- position: absolute;
383
+ position: fixed;
384
384
  z-index: 1001;
385
- right: 0;
386
- bottom: 50px;
385
+ top: 120px;
386
+ right: 20px;
387
+ }
388
+
389
+ /* Responsive adjustments for mobile */
390
+ @media (max-width: 768px) {
391
+ .memori--error-message-container,
392
+ .memori--login-tip {
393
+ top: 100px;
394
+ right: 10px;
395
+ max-width: calc(100vw - 20px);
396
+ }
387
397
  }
388
398
 
389
399
  /* File Stats */
@@ -425,6 +435,25 @@
425
435
  background-color: #fa5252;
426
436
  }
427
437
 
438
+ /* Document count indicator */
439
+ .memori--document-count {
440
+ position: absolute;
441
+ z-index: 5;
442
+ top: -8px;
443
+ right: -8px;
444
+ padding: 1px 6px;
445
+ border-radius: 10px;
446
+ background-color: #51cf66;
447
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
448
+ color: white;
449
+ font-size: 11px;
450
+ font-weight: bold;
451
+ }
452
+
453
+ .memori--document-count-full {
454
+ background-color: #fa5252;
455
+ }
456
+
428
457
  /* Styled upload slots info */
429
458
  .memori--upload-slots-info {
430
459
  margin-left: 4px;
@@ -465,15 +494,6 @@
465
494
  transform: translateX(4px);
466
495
  }
467
496
 
468
- /* Error container positioning */
469
- .memori--error-message-container {
470
- position: absolute;
471
- z-index: 1001;
472
- right: 0;
473
- bottom: 50px;
474
- min-width: 250px;
475
- }
476
-
477
497
  input.memori--upload-title-input {
478
498
  min-height: 2.5rem;
479
499
  border: 1px solid #e0e0e0;
@@ -9,10 +9,11 @@ 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 { MAX_DOCUMENTS_PER_MESSAGE } from '../../helpers/constants';
12
13
 
13
14
  // Constants
14
15
  const MAX_IMAGES = 5;
15
- const MAX_DOCUMENTS = 1;
16
+ const MAX_DOCUMENTS = MAX_DOCUMENTS_PER_MESSAGE;
16
17
 
17
18
  // Props interface
18
19
  interface UploadManagerProps {
@@ -107,15 +108,12 @@ const UploadButton: React.FC<UploadManagerProps> = ({
107
108
  };
108
109
  }, []);
109
110
 
110
- // Handler for document files - only stores the latest document
111
+ // Handler for document files - now supports multiple documents
111
112
  const handleDocumentFiles = (
112
113
  files: { name: string; id: string; content: string; mimeType: string }[]
113
114
  ) => {
114
115
  if (files.length === 0) return;
115
116
 
116
- // For simplicity, we only take the first file
117
- const file = files[0];
118
-
119
117
  // Funzione helper per fare escape dell'HTML nei valori degli attributi
120
118
  const escapeAttributeValue = (text: string) => {
121
119
  return text
@@ -126,34 +124,134 @@ const UploadButton: React.FC<UploadManagerProps> = ({
126
124
  .replace(/>/g, '&gt;');
127
125
  };
128
126
 
129
- const escapedFileName = escapeAttributeValue(file.name);
130
- const formattedContent = `<document_attachment filename="${escapedFileName}" type="${file.mimeType}">
127
+ // Process each document file
128
+ const processedDocuments = files.map(file => {
129
+ const escapedFileName = escapeAttributeValue(file.name);
130
+ const formattedContent = `<document_attachment filename="${escapedFileName}" type="${file.mimeType}">
131
131
 
132
132
  ${file.content}
133
133
 
134
134
  </document_attachment>`;
135
135
 
136
- //keep just the images in the documentPreviewFiles
137
- const imageFiles = documentPreviewFiles.filter(
138
- (file: any) => file.type === 'image'
139
- );
140
- // Replace existing file with new one
141
- setDocumentPreviewFiles([
142
- {
136
+ return {
143
137
  name: file.name,
144
138
  id: file.id,
145
139
  content: formattedContent,
146
- type: 'file',
140
+ type: 'document',
147
141
  mimeType: file.mimeType,
148
- },
142
+ };
143
+ });
144
+
145
+ // Keep existing images and add new documents
146
+ const imageFiles = documentPreviewFiles.filter(
147
+ (file: any) => file.type === 'image'
148
+ );
149
+
150
+ setDocumentPreviewFiles([
151
+ ...processedDocuments,
149
152
  ...imageFiles,
150
153
  ]);
151
154
 
152
155
  setIsLoading(false);
153
156
  };
154
157
 
158
+ // Document validation and error handling
159
+ const validateDocumentFile = (file: File): boolean => {
160
+ const fileExt = `.${file.name.split('.').pop()?.toLowerCase()}`;
161
+ const ALLOWED_FILE_TYPES = ['.pdf', '.txt', '.json', '.xlsx', '.csv', '.md'];
162
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
163
+
164
+ if (!ALLOWED_FILE_TYPES.includes(fileExt)) {
165
+ addError({
166
+ message: `File type "${fileExt}" is not supported. Please use: ${ALLOWED_FILE_TYPES.join(', ')}`,
167
+ severity: 'error',
168
+ });
169
+ return false;
170
+ }
171
+
172
+ if (file.size > MAX_FILE_SIZE) {
173
+ addError({
174
+ message: `File "${file.name}" exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit`,
175
+ severity: 'error',
176
+ });
177
+ return false;
178
+ }
179
+
180
+ return true;
181
+ };
182
+
183
+ // Validate total payload size
184
+ const validatePayloadSize = (newDocuments: { name: string; id: string; content: string; mimeType: string }[]): boolean => {
185
+ const { MAX_TOTAL_MESSAGE_PAYLOAD } = require('../../helpers/constants');
186
+
187
+ const existingDocuments = documentPreviewFiles.filter(
188
+ (file: any) => file.type === 'document'
189
+ );
190
+
191
+ const allDocuments = [...existingDocuments, ...newDocuments];
192
+ const totalPayloadSize = allDocuments.reduce((total, doc) => total + doc.content.length, 0);
193
+
194
+ if (totalPayloadSize > MAX_TOTAL_MESSAGE_PAYLOAD) {
195
+ addError({
196
+ message: `Total document content exceeds ${MAX_TOTAL_MESSAGE_PAYLOAD} characters limit. Please remove some documents.`,
197
+ severity: 'error',
198
+ });
199
+ return false;
200
+ }
201
+
202
+ return true;
203
+ };
204
+
205
+ // Handle document upload errors
206
+ const handleDocumentError = (error: { message: string; severity: 'error' | 'warning' | 'info' }) => {
207
+ addError(error);
208
+ };
209
+
210
+ // Image validation and error handling
211
+ const validateImageFile = (file: File): boolean => {
212
+ const fileExt = `.${file.name.split('.').pop()?.toLowerCase()}`;
213
+ const ALLOWED_FILE_TYPES = ['.jpg', '.jpeg', '.png'];
214
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
215
+
216
+ if (
217
+ !ALLOWED_FILE_TYPES.includes(fileExt) &&
218
+ !file.type.startsWith('image/')
219
+ ) {
220
+ addError({
221
+ message: `File type "${fileExt}" is not supported. Please use: ${ALLOWED_FILE_TYPES.join(', ')}`,
222
+ severity: 'error',
223
+ });
224
+ return false;
225
+ }
226
+
227
+ if (file.size > MAX_FILE_SIZE) {
228
+ addError({
229
+ message: `File "${file.name}" exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit`,
230
+ severity: 'error',
231
+ });
232
+ return false;
233
+ }
234
+
235
+ return true;
236
+ };
237
+
238
+ // Handle image upload errors
239
+ const handleImageError = (error: { message: string; severity: 'error' | 'warning' | 'info' }) => {
240
+ addError(error);
241
+ };
242
+
155
243
  // When document option is clicked
156
244
  const handleDocumentClick = () => {
245
+ // Check if document limit has been reached
246
+ if (hasReachedDocumentLimit) {
247
+ addError({
248
+ message: `Maximum ${MAX_DOCUMENTS} documents allowed.`,
249
+ severity: 'error',
250
+ });
251
+ closeMenu();
252
+ return;
253
+ }
254
+
157
255
  // Find the actual button in the UploadDocuments component and click it
158
256
  const documentButtonElement = documentRef.current?.querySelector('button');
159
257
  if (documentButtonElement) {
@@ -234,6 +332,17 @@ ${file.content}
234
332
  </div>
235
333
  )}
236
334
 
335
+ {/* Document count indicator */}
336
+ {currentDocumentCount > 0 && (
337
+ <div
338
+ className={cx('memori--document-count', {
339
+ 'memori--document-count-full': hasReachedDocumentLimit,
340
+ })}
341
+ >
342
+ {currentDocumentCount}/{MAX_DOCUMENTS}
343
+ </div>
344
+ )}
345
+
237
346
  {/* Floating menu */}
238
347
  {menuOpen && (
239
348
  <div className="memori--upload-menu" ref={menuRef}>
@@ -242,11 +351,20 @@ ${file.content}
242
351
  'memori--upload-menu-item--disabled': hasReachedDocumentLimit,
243
352
  })}
244
353
  onClick={handleDocumentClick}
354
+ title={
355
+ hasReachedDocumentLimit
356
+ ? t('upload.maxDocumentsReached', { max: MAX_DOCUMENTS }) ??
357
+ `Maximum ${MAX_DOCUMENTS} documents already uploaded`
358
+ : remainingDocumentSlots === 1
359
+ ? t('upload.lastDocumentSlot') ?? 'Upload last document'
360
+ : t('upload.uploadDocument', { remaining: remainingDocumentSlots }) ??
361
+ `Upload document (${remainingDocumentSlots} remaining)`
362
+ }
245
363
  >
246
364
  <DocumentIcon className="memori--upload-menu-icon" />
247
365
  <span>
248
366
  {t('upload.uploadDocument') ?? 'Upload document'}
249
- {currentDocumentCount > 0 && (
367
+ {/* {currentDocumentCount > 0 && (
250
368
  <span className="memori--upload-slots-info">
251
369
  {hasReachedDocumentLimit
252
370
  ? ` (${t('upload.maxReached') ?? 'Max reached'})`
@@ -254,7 +372,7 @@ ${file.content}
254
372
  t('upload.remaining') ?? 'remaining'
255
373
  })`}
256
374
  </span>
257
- )}
375
+ )} */}
258
376
  </span>
259
377
  </div>
260
378
 
@@ -279,7 +397,7 @@ ${file.content}
279
397
  <ImageIcon className="memori--upload-menu-icon-image" />
280
398
  <span>
281
399
  {t('upload.uploadImage') ?? 'Upload image'}
282
- {currentImageCount > 0 && (
400
+ {/* {currentImageCount > 0 && (
283
401
  <span className="memori--upload-slots-info">
284
402
  {hasReachedImageLimit
285
403
  ? ` (${t('upload.maxReached') ?? 'Max reached'})`
@@ -287,7 +405,7 @@ ${file.content}
287
405
  t('upload.remaining') ?? 'remaining'
288
406
  })`}
289
407
  </span>
290
- )}
408
+ )} */}
291
409
  </span>
292
410
  </div>
293
411
  </div>
@@ -299,7 +417,10 @@ ${file.content}
299
417
  setDocumentPreviewFiles={handleDocumentFiles}
300
418
  maxDocuments={MAX_DOCUMENTS}
301
419
  documentPreviewFiles={documentPreviewFiles}
302
- // onLoadingChange={handleLoadingChange}
420
+ onLoadingChange={handleLoadingChange}
421
+ onDocumentError={handleDocumentError}
422
+ onValidateFile={validateDocumentFile}
423
+ onValidatePayloadSize={validatePayloadSize}
303
424
  />
304
425
  </div>
305
426
 
@@ -314,6 +435,8 @@ ${file.content}
314
435
  onLoadingChange={handleLoadingChange}
315
436
  maxImages={MAX_IMAGES}
316
437
  memoriID={memoriID}
438
+ onImageError={handleImageError}
439
+ onValidateImageFile={validateImageFile}
317
440
  />
318
441
  </div>
319
442
 
@@ -321,6 +444,7 @@ ${file.content}
321
444
  <div className="memori--error-message-container">
322
445
  {errors.map((error, index) => (
323
446
  <Alert
447
+ className='memori--error-message-alert'
324
448
  key={`${error.message}-${index}`}
325
449
  open={true}
326
450
  type={error.severity}