@memori.ai/memori-react 7.25.0 → 7.26.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 (132) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/components/Chat/Chat.d.ts +5 -10
  3. package/dist/components/Chat/Chat.js +3 -3
  4. package/dist/components/Chat/Chat.js.map +1 -1
  5. package/dist/components/ChatBubble/ChatBubble.css +10 -0
  6. package/dist/components/ChatBubble/ChatBubble.d.ts +1 -0
  7. package/dist/components/ChatBubble/ChatBubble.js +16 -20
  8. package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
  9. package/dist/components/ChatInputs/ChatInputs.d.ts +6 -10
  10. package/dist/components/ChatInputs/ChatInputs.js +37 -30
  11. package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
  12. package/dist/components/FilePreview/FilePreview.css +169 -140
  13. package/dist/components/FilePreview/FilePreview.d.ts +2 -6
  14. package/dist/components/FilePreview/FilePreview.js +58 -5
  15. package/dist/components/FilePreview/FilePreview.js.map +1 -1
  16. package/dist/components/MemoriWidget/MemoriWidget.d.ts +2 -1
  17. package/dist/components/MemoriWidget/MemoriWidget.js +15 -21
  18. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  19. package/dist/components/UploadButton/UploadButton.css +506 -27
  20. package/dist/components/UploadButton/UploadButton.d.ts +14 -11
  21. package/dist/components/UploadButton/UploadButton.js +110 -288
  22. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  23. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +19 -0
  24. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +211 -0
  25. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -0
  26. package/dist/components/UploadButton/UploadImages/UploadImages.d.ts +13 -0
  27. package/dist/components/UploadButton/UploadImages/UploadImages.js +207 -0
  28. package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -0
  29. package/dist/components/icons/Bug.d.ts +5 -0
  30. package/dist/components/icons/Bug.js +6 -0
  31. package/dist/components/icons/Bug.js.map +1 -0
  32. package/dist/components/icons/Document.d.ts +5 -0
  33. package/dist/components/icons/Document.js +10 -0
  34. package/dist/components/icons/Document.js.map +1 -0
  35. package/dist/components/icons/Image.d.ts +4 -0
  36. package/dist/components/icons/Image.js +9 -0
  37. package/dist/components/icons/Image.js.map +1 -0
  38. package/dist/components/icons/Preview.d.ts +4 -5
  39. package/dist/components/icons/Preview.js +5 -2
  40. package/dist/components/icons/Preview.js.map +1 -1
  41. package/dist/components/icons/Upload.d.ts +4 -5
  42. package/dist/components/icons/Upload.js +5 -2
  43. package/dist/components/icons/Upload.js.map +1 -1
  44. package/dist/components/layouts/HiddenChat.js +100 -10
  45. package/dist/components/layouts/HiddenChat.js.map +1 -1
  46. package/dist/components/layouts/hidden-chat.css +189 -119
  47. package/dist/locales/de.json +16 -0
  48. package/dist/locales/en.json +24 -0
  49. package/dist/locales/es.json +16 -0
  50. package/dist/locales/fr.json +16 -0
  51. package/dist/locales/it.json +22 -0
  52. package/esm/components/Chat/Chat.d.ts +5 -10
  53. package/esm/components/Chat/Chat.js +3 -3
  54. package/esm/components/Chat/Chat.js.map +1 -1
  55. package/esm/components/ChatBubble/ChatBubble.css +10 -0
  56. package/esm/components/ChatBubble/ChatBubble.d.ts +1 -0
  57. package/esm/components/ChatBubble/ChatBubble.js +16 -20
  58. package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
  59. package/esm/components/ChatInputs/ChatInputs.d.ts +6 -10
  60. package/esm/components/ChatInputs/ChatInputs.js +37 -30
  61. package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
  62. package/esm/components/FilePreview/FilePreview.css +169 -140
  63. package/esm/components/FilePreview/FilePreview.d.ts +2 -6
  64. package/esm/components/FilePreview/FilePreview.js +58 -5
  65. package/esm/components/FilePreview/FilePreview.js.map +1 -1
  66. package/esm/components/MemoriWidget/MemoriWidget.d.ts +2 -1
  67. package/esm/components/MemoriWidget/MemoriWidget.js +15 -21
  68. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  69. package/esm/components/UploadButton/UploadButton.css +506 -27
  70. package/esm/components/UploadButton/UploadButton.d.ts +14 -11
  71. package/esm/components/UploadButton/UploadButton.js +111 -289
  72. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  73. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +19 -0
  74. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +208 -0
  75. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -0
  76. package/esm/components/UploadButton/UploadImages/UploadImages.d.ts +13 -0
  77. package/esm/components/UploadButton/UploadImages/UploadImages.js +204 -0
  78. package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -0
  79. package/esm/components/icons/Bug.d.ts +5 -0
  80. package/esm/components/icons/Bug.js +4 -0
  81. package/esm/components/icons/Bug.js.map +1 -0
  82. package/esm/components/icons/Document.d.ts +5 -0
  83. package/esm/components/icons/Document.js +6 -0
  84. package/esm/components/icons/Document.js.map +1 -0
  85. package/esm/components/icons/Image.d.ts +4 -0
  86. package/esm/components/icons/Image.js +5 -0
  87. package/esm/components/icons/Image.js.map +1 -0
  88. package/esm/components/icons/Preview.d.ts +4 -5
  89. package/esm/components/icons/Preview.js +4 -3
  90. package/esm/components/icons/Preview.js.map +1 -1
  91. package/esm/components/icons/Upload.d.ts +4 -5
  92. package/esm/components/icons/Upload.js +4 -3
  93. package/esm/components/icons/Upload.js.map +1 -1
  94. package/esm/components/layouts/HiddenChat.js +101 -11
  95. package/esm/components/layouts/HiddenChat.js.map +1 -1
  96. package/esm/components/layouts/hidden-chat.css +189 -119
  97. package/esm/locales/de.json +16 -0
  98. package/esm/locales/en.json +24 -0
  99. package/esm/locales/es.json +16 -0
  100. package/esm/locales/fr.json +16 -0
  101. package/esm/locales/it.json +22 -0
  102. package/package.json +1 -1
  103. package/src/components/Chat/Chat.tsx +8 -8
  104. package/src/components/ChatBubble/ChatBubble.css +10 -0
  105. package/src/components/ChatBubble/ChatBubble.stories.tsx +25 -0
  106. package/src/components/ChatBubble/ChatBubble.tsx +41 -17
  107. package/src/components/ChatInputs/ChatInputs.tsx +92 -43
  108. package/src/components/FilePreview/FilePreview.css +169 -140
  109. package/src/components/FilePreview/FilePreview.tsx +106 -14
  110. package/src/components/FilePreview/__snapshots__/FilePreview.test.tsx.snap +146 -29
  111. package/src/components/MemoriWidget/MemoriWidget.tsx +14 -22
  112. package/src/components/UploadButton/UploadButton.css +506 -27
  113. package/src/components/UploadButton/UploadButton.stories.tsx +122 -20
  114. package/src/components/UploadButton/UploadButton.test.tsx +1 -1
  115. package/src/components/UploadButton/UploadButton.tsx +264 -454
  116. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +352 -0
  117. package/src/components/UploadButton/UploadImages/UploadImages.tsx +434 -0
  118. package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +140 -13
  119. package/src/components/icons/Bug.tsx +81 -0
  120. package/src/components/icons/Document.tsx +50 -0
  121. package/src/components/icons/Image.tsx +37 -0
  122. package/src/components/icons/Preview.tsx +28 -22
  123. package/src/components/icons/Upload.tsx +33 -22
  124. package/src/components/layouts/HiddenChat.tsx +143 -7
  125. package/src/components/layouts/__snapshots__/HiddenChat.test.tsx.snap +1 -1
  126. package/src/components/layouts/hidden-chat.css +189 -119
  127. package/src/index.stories.tsx +19 -19
  128. package/src/locales/de.json +16 -0
  129. package/src/locales/en.json +24 -0
  130. package/src/locales/es.json +16 -0
  131. package/src/locales/fr.json +16 -0
  132. package/src/locales/it.json +22 -0
@@ -1,506 +1,211 @@
1
- import React, { useState, useRef } from 'react';
2
- import cx from 'classnames';
3
- import UploadIcon from '../icons/Upload';
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { DocumentIcon } from '../icons/Document';
3
+ import { ImageIcon } from '../icons/Image';
4
+ import { UploadIcon } from '../icons/Upload';
4
5
  import Spin from '../ui/Spin';
5
6
  import Alert from '../ui/Alert';
6
-
7
- // Define error types for better type safety
8
- type UploadError = {
9
- message: string;
10
- severity: 'error' | 'warning';
11
- fileId?: string;
12
- };
13
-
14
- /**
15
- * FileUploadButton component allows users to upload and convert files to text
16
- * Supports PDF, TXT, CSV and XLSX files up to 10MB
17
- * Extracts text from PDFs using PDF.js
18
- * Extracts text from XLSX using xlsx library
19
- */
20
-
21
- const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
22
- const MAX_TEXT_LENGTH = 100000; // 100,000 characters
23
- const PDF_JS_VERSION = '3.11.174'; // Last stable version with .min.js files
24
- const WORKER_URL = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDF_JS_VERSION}/pdf.worker.min.js`;
25
- const PDF_JS_URL = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDF_JS_VERSION}/pdf.min.js`;
26
- const XLSX_URL =
27
- 'https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js';
28
-
29
- // Add type definitions for external libraries
30
- declare global {
31
- interface Window {
32
- pdfjsLib: any;
33
- XLSX: any;
34
- }
7
+ import cx from 'classnames';
8
+ import UploadDocuments from './UploadDocuments/UploadDocuments';
9
+ import UploadImages from './UploadImages/UploadImages';
10
+ import { useTranslation } from 'react-i18next';
11
+
12
+ // Constants
13
+ const MAX_IMAGES = 5;
14
+ const MAX_DOCUMENTS = 1;
15
+
16
+ // Props interface
17
+ interface UploadManagerProps {
18
+ authToken?: string;
19
+ apiUrl?: string;
20
+ sessionID?: string;
21
+ isMediaAccepted?: boolean;
22
+ setDocumentPreviewFiles: any;
23
+ documentPreviewFiles: {
24
+ name: string;
25
+ id: string;
26
+ content: string;
27
+ mediumID?: string;
28
+ type?: string;
29
+ }[];
35
30
  }
36
31
 
37
- const FileUploadButton = ({
38
- setPreviewFiles,
39
- }: {
40
- setPreviewFiles: (
41
- previewFiles: { name: string; id: string; content: string }[]
42
- ) => void;
32
+ const UploadButton: React.FC<UploadManagerProps> = ({
33
+ authToken = '',
34
+ apiUrl = '',
35
+ sessionID = '',
36
+ isMediaAccepted = false,
37
+ setDocumentPreviewFiles,
38
+ documentPreviewFiles,
43
39
  }) => {
44
- // State for loading indicator
40
+ // State
45
41
  const [isLoading, setIsLoading] = useState(false);
46
- // State for tracking upload errors
47
- const [errors, setErrors] = useState<UploadError[]>([]);
48
- // Reference to hidden file input
49
- const fileInputRef = useRef<HTMLInputElement>(null);
50
-
51
- // Clear all errors
42
+ const [menuOpen, setMenuOpen] = useState(false);
43
+ const [errors, setErrors] = useState<
44
+ { message: string; severity: 'error' | 'warning' | 'info' }[]
45
+ >([]);
46
+ const { t, i18n } = useTranslation();
47
+
48
+ // Refs
49
+ const menuRef = useRef<HTMLDivElement>(null);
50
+ const buttonRef = useRef<HTMLButtonElement>(null);
51
+ const documentRef = useRef<HTMLDivElement>(null);
52
+ const imageRef = useRef<HTMLDivElement>(null);
53
+
54
+ // Calculate image count and remaining slots
55
+ const currentImageCount = documentPreviewFiles.filter(file => file.type === 'image').length;
56
+ const remainingSlots = MAX_IMAGES - currentImageCount;
57
+ const currentDocumentCount = documentPreviewFiles.filter(file => file.type === 'document').length;
58
+ const remainingDocumentSlots = MAX_DOCUMENTS - currentDocumentCount;
59
+ const hasReachedImageLimit = remainingSlots <= 0;
60
+ const hasReachedDocumentLimit = remainingDocumentSlots <= 0;
61
+
62
+ // Error handling
52
63
  const clearErrors = () => setErrors([]);
53
64
 
54
- // Remove a specific error by message
55
65
  const removeError = (errorMessage: string) => {
56
66
  setErrors(prev => prev.filter(e => e.message !== errorMessage));
57
67
  };
58
68
 
59
- // Add a new error and auto-remove after 5 seconds
60
- const addError = (error: UploadError) => {
69
+ const addError = (error: {
70
+ message: string;
71
+ severity: 'error' | 'warning' | 'info';
72
+ }) => {
61
73
  setErrors(prev => [...prev, error]);
62
- // Auto-clear errors after 5 seconds
63
- setTimeout(() => {
64
- removeError(error.message);
65
- }, 5000);
74
+ setTimeout(() => removeError(error.message), 5000);
66
75
  };
67
76
 
68
- /**
69
- * Extracts text from PDF using PDF.js
70
- * @param file PDF file to process
71
- * @returns Promise resolving to extracted text
72
- */
73
- const extractTextFromPDF = async (file: File): Promise<string> => {
74
- try {
75
- // Load PDF.js if not already loaded
76
- if (!window.pdfjsLib) {
77
- await new Promise((resolve, reject) => {
78
- const script = document.createElement('script');
79
- script.src = PDF_JS_URL;
80
- script.onload = () => {
81
- // Set up worker
82
- window.pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_URL;
83
- resolve(true);
84
- };
85
- script.onerror = reject;
86
- document.head.appendChild(script);
87
- });
88
- }
89
-
90
- // Extract text from PDF
91
- const arrayBuffer = await file.arrayBuffer();
92
- // Get PDF document
93
- const pdf = await window.pdfjsLib.getDocument({ data: arrayBuffer })
94
- .promise;
95
- let text = '';
96
-
97
- // Iterate through each page and extract text
98
- for (let i = 1; i <= pdf.numPages; i++) {
99
- const page = await pdf.getPage(i);
100
- const content = await page.getTextContent();
101
- // Filter out non-string items and join text
102
- const pageText = content.items
103
- .filter((item: any) => item.str && typeof item.str === 'string')
104
- .map((item: any) => item.str)
105
- .join(' ');
106
- text += pageText + '\n';
107
- }
108
-
109
- // Return extracted text
110
- return text;
111
- } catch (error) {
112
- setErrors(prev => [
113
- ...prev,
114
- {
115
- message: `PDF extraction failed: ${
116
- error instanceof Error ? error.message : 'Unknown error'
117
- }`,
118
- severity: 'error',
119
- fileId: file.name,
120
- },
121
- ]);
122
- throw new Error(
123
- `PDF extraction failed: ${
124
- error instanceof Error ? error.message : 'Unknown error'
125
- }`
126
- );
127
- }
77
+ // Menu handling
78
+ const toggleMenu = () => {
79
+ setMenuOpen(prev => !prev);
128
80
  };
129
81
 
130
- /**
131
- * Extracts text from XLSX using xlsx library with enhanced error handling
132
- * @param file XLSX file to process
133
- * @returns Promise resolving to extracted text
134
- */
135
- const extractTextFromXLSX = async (file: File): Promise<string> => {
136
- try {
137
- // First, check if the XLSX library is loaded in the window object
138
- // If not, dynamically load it by creating and appending a script tag
139
- if (!window.XLSX) {
140
- await new Promise((resolve, reject) => {
141
- const script = document.createElement('script');
142
- script.src = XLSX_URL;
143
- script.onload = resolve;
144
- script.onerror = reject;
145
- document.head.appendChild(script);
146
- });
147
- }
148
-
149
- // Convert the File object to ArrayBuffer for XLSX parsing
150
- const arrayBuffer = await file.arrayBuffer();
151
-
152
- // Check for minimum valid Excel file size
153
- if (arrayBuffer.byteLength < 4) {
154
- throw new Error('File appears to be corrupted or empty');
155
- }
156
-
157
- let workbook;
158
- try {
159
- // Try parsing with full options first
160
- workbook = window.XLSX.read(arrayBuffer, {
161
- type: 'array',
162
- cellFormula: true,
163
- cellNF: true,
164
- cellHTML: true,
165
- cellText: true,
166
- cellDates: true,
167
- error: (e: any) => {
168
- console.warn('Non-fatal XLSX error:', e);
169
- },
170
- cellStyles: true,
171
- });
172
- } catch (initialError) {
173
- console.warn(
174
- 'Initial XLSX parsing failed, attempting recovery mode:',
175
- initialError
176
- );
177
-
178
- // Fallback to a more permissive parsing method
179
- try {
180
- workbook = window.XLSX.read(arrayBuffer, {
181
- type: 'array',
182
- sheetRows: 1000,
183
- cellFormula: true,
184
- cellStyles: true,
185
- bookDeps: true,
186
- bookFiles: true,
187
- bookProps: true,
188
- bookSheets: true,
189
- bookVBA: true,
190
- WTF: true,
191
- });
192
- } catch (recoveryError) {
193
- setErrors(prev => [
194
- ...prev,
195
- {
196
- message: `File appears to be corrupted. Recovery attempt failed: ${
197
- recoveryError instanceof Error
198
- ? recoveryError.message
199
- : 'Unknown error'
200
- }`,
201
- severity: 'error',
202
- fileId: file.name,
203
- },
204
- ]);
205
- throw new Error(
206
- `File appears to be corrupted. Recovery attempt failed: ${
207
- recoveryError instanceof Error
208
- ? recoveryError.message
209
- : 'Unknown error'
210
- }`
211
- );
212
- }
213
- }
82
+ const closeMenu = () => {
83
+ setMenuOpen(false);
84
+ };
214
85
 
215
- // Verify that workbook contains at least one sheet
86
+ // Click outside handler
87
+ useEffect(() => {
88
+ const handleClickOutside = (event: MouseEvent) => {
216
89
  if (
217
- !workbook ||
218
- !workbook.SheetNames ||
219
- workbook.SheetNames.length === 0
90
+ menuRef.current &&
91
+ buttonRef.current &&
92
+ !menuRef.current.contains(event.target as Node) &&
93
+ !buttonRef.current.contains(event.target as Node)
220
94
  ) {
221
- throw new Error('Excel file contains no valid worksheets');
222
- }
223
-
224
- let text = '';
225
- let successfulSheets = 0;
226
- const totalSheets = workbook.SheetNames.length;
227
-
228
- // Loop through each sheet in the workbook
229
- for (const sheetName of workbook.SheetNames) {
230
- try {
231
- const worksheet = workbook.Sheets[sheetName];
232
- if (!worksheet) {
233
- throw new Error(`Sheet ${sheetName} is empty or corrupted`);
234
- }
235
-
236
- // Safely get the dimensions of the sheet
237
- const range = window.XLSX.utils.decode_range(
238
- worksheet['!ref'] || 'A1:A1'
239
- );
240
-
241
- // Check if sheet seems abnormally large (possible corruption)
242
- const rowCount = range.e.r - range.s.r + 1;
243
- const colCount = range.e.c - range.s.c + 1;
244
- if (rowCount > 10000 || colCount > 1000) {
245
- throw new Error(
246
- `Sheet ${sheetName} has suspicious dimensions (${rowCount}x${colCount}) and may be corrupted`
247
- );
248
- }
249
-
250
- // Try to convert sheet to formatted text with columns
251
- let formattedText;
252
- try {
253
- // Get array of arrays representation
254
- const data = window.XLSX.utils.sheet_to_json(worksheet, {
255
- header: 1,
256
- raw: false,
257
- });
258
-
259
- // Find the maximum width for each column
260
- const colWidths = data.reduce((widths: number[], row: any[]) => {
261
- row.forEach((cell, i) => {
262
- const cellWidth = (cell || '').toString().length;
263
- widths[i] = Math.max(widths[i] || 0, cellWidth);
264
- });
265
- return widths;
266
- }, []);
267
-
268
- // Format each row with proper column spacing
269
- formattedText = data.map((row: any[]) => {
270
- return row
271
- .map((cell, i) => {
272
- const cellStr = (cell || '').toString();
273
- return cellStr.padEnd(colWidths[i] + 2); // Add 2 spaces padding
274
- })
275
- .join('|')
276
- .trim();
277
- });
278
-
279
- // Add separator line after header
280
- if (formattedText.length > 0) {
281
- const separator = colWidths
282
- .map((w: number) => '-'.repeat(w + 2))
283
- .join('+');
284
- formattedText.splice(1, 0, separator);
285
- }
286
-
287
- formattedText = formattedText.join('\n');
288
- } catch (formatError) {
289
- // Fallback to basic formatting if advanced fails
290
- formattedText = '';
291
- for (let r = range.s.r; r <= Math.min(range.e.r, 1000); ++r) {
292
- let row = '';
293
- for (let c = range.s.c; c <= Math.min(range.e.c, 100); ++c) {
294
- const cell =
295
- worksheet[window.XLSX.utils.encode_cell({ r: r, c: c })];
296
- row +=
297
- (cell ? String(cell.v || '').padEnd(15) : ' '.repeat(15)) +
298
- '|';
299
- }
300
- formattedText += row + '\n';
301
- }
302
- formattedText += '...(truncated due to potential corruption)';
303
- }
304
-
305
- // Add sheet name and formatted content to final text
306
- text += `Sheet: ${sheetName}\n${formattedText}\n\n`;
307
- successfulSheets++;
308
- } catch (sheetError) {
309
- // Log sheet-specific error but continue with other sheets
310
- text += `Sheet: ${sheetName}\nError extracting content: ${
311
- sheetError instanceof Error ? sheetError.message : 'Unknown error'
312
- }\n\n`;
313
- }
95
+ closeMenu();
314
96
  }
97
+ };
98
+
99
+ document.addEventListener('mousedown', handleClickOutside);
100
+ return () => {
101
+ document.removeEventListener('mousedown', handleClickOutside);
102
+ };
103
+ }, []);
104
+
105
+ // Handler for document files - only stores the latest document
106
+ const handleDocumentFiles = (
107
+ files: { name: string; id: string; content: string; mimeType: string }[]
108
+ ) => {
109
+ if (files.length === 0) return;
315
110
 
316
- // If we couldn't extract any sheets successfully
317
- if (successfulSheets === 0) {
318
- throw new Error(
319
- 'Could not extract any valid content from the Excel file'
320
- );
321
- }
111
+ // For simplicity, we only take the first file
112
+ const file = files[0];
113
+
114
+ // Format content with XML tags to improve readability for LLM
115
+ const formattedContent = `<Documento allegato al messaggio: ${file.name}>
116
+ ${file.content}
117
+ </Documento allegato al messaggio: ${file.name}>`;
118
+
119
+ //keep just the images in the documentPreviewFiles
120
+ const imageFiles = documentPreviewFiles.filter(
121
+ (file: any) => file.type === 'image'
122
+ );
123
+
124
+ // Replace existing file with new one
125
+ setDocumentPreviewFiles([
126
+ {
127
+ name: file.name,
128
+ id: file.id,
129
+ content: formattedContent,
130
+ type: 'file',
131
+ mimeType: file.mimeType,
132
+ },
133
+ ...imageFiles,
134
+ ]);
322
135
 
323
- // If some sheets failed but others succeeded
324
- if (successfulSheets < totalSheets) {
325
- text =
326
- `Warning: Only extracted ${successfulSheets} of ${totalSheets} sheets due to possible corruption.\n\n` +
327
- text;
328
- }
136
+ setIsLoading(false);
137
+ };
329
138
 
330
- return text;
331
- } catch (error) {
332
- setErrors(prev => [
333
- ...prev,
334
- {
335
- message: `XLSX extraction failed: ${
336
- error instanceof Error ? error.message : 'Unknown error'
337
- }`,
338
- severity: 'error',
339
- fileId: file.name,
340
- },
341
- ]);
342
- throw new Error(
343
- `XLSX extraction failed: ${
344
- error instanceof Error ? error.message : 'Unknown error'
345
- }`
346
- );
139
+ // When document option is clicked
140
+ const handleDocumentClick = () => {
141
+ // Find the actual button in the UploadDocuments component and click it
142
+ const documentButtonElement = documentRef.current?.querySelector('button');
143
+ if (documentButtonElement) {
144
+ documentButtonElement.click();
347
145
  }
146
+ closeMenu();
348
147
  };
349
148
 
350
- /**
351
- * Validates uploaded file
352
- * Checks file type and size restrictions
353
- * @param file File to validate
354
- * @returns boolean indicating if file is valid
355
- */
356
- const validateFile = (file: File): boolean => {
357
- const fileExt = `.${file.name.split('.').pop()?.toLowerCase()}`;
358
- const ALLOWED_FILE_TYPES = ['.pdf', '.txt', '.json', '.xlsx', '.csv'];
359
-
360
- if (!ALLOWED_FILE_TYPES.includes(fileExt)) {
149
+ // When image option is clicked
150
+ const handleImageClick = () => {
151
+ if (!authToken) {
361
152
  addError({
362
- message: `File type "${fileExt}" is not supported. Please use: ${ALLOWED_FILE_TYPES.join(
363
- ', '
364
- )}`,
365
- severity: 'error',
366
- fileId: file.name,
153
+ message: t('upload.loginRequired') ?? 'Login required to upload images',
154
+ severity: 'info',
367
155
  });
368
- return false;
156
+ closeMenu();
157
+ return;
369
158
  }
370
-
371
- if (file.size > MAX_FILE_SIZE) {
159
+
160
+ if (!isMediaAccepted) {
372
161
  addError({
373
- message: `File "${file.name}" exceeds ${
374
- MAX_FILE_SIZE / 1024 / 1024
375
- }MB limit`,
376
- severity: 'error',
377
- fileId: file.name,
162
+ message: t('upload.mediaNotAccepted') ?? 'Media uploads are not accepted',
163
+ severity: 'info',
378
164
  });
379
- return false;
165
+ closeMenu();
166
+ return;
380
167
  }
381
-
382
- return true;
383
- };
384
-
385
- /**
386
- * Processes file to extract text content
387
- * @param file File to process
388
- * @returns Promise resolving to extracted text or null if processing fails
389
- */
390
- const processFile = async (file: File): Promise<string | null> => {
391
- const fileExt = file.name.split('.').pop()?.toLowerCase() || '';
392
-
393
- try {
394
- let text: string | null = null;
395
-
396
- if (fileExt === 'pdf') {
397
- text = await extractTextFromPDF(file);
398
- } else if (
399
- fileExt === 'txt' ||
400
- fileExt === 'md' ||
401
- fileExt === 'json' ||
402
- fileExt === 'csv'
403
- ) {
404
- text = await file.text();
405
- } else if (fileExt === 'xlsx') {
406
- text = await extractTextFromXLSX(file);
407
- }
408
-
409
- // Check text length limit
410
- if (text && text.length > MAX_TEXT_LENGTH) {
411
- addError({
412
- message: `File "${file.name}" content exceeds ${MAX_TEXT_LENGTH} characters`,
413
- severity: 'error',
414
- fileId: file.name,
415
- });
416
- return null;
417
- }
418
-
419
- return text;
420
- } catch (error) {
168
+
169
+ if (hasReachedImageLimit) {
421
170
  addError({
422
- message: `Failed to process "${file.name}": ${
423
- error instanceof Error ? error.message : 'Unknown error'
424
- }`,
425
- severity: 'error',
426
- fileId: file.name,
171
+ message: t('upload.maxImagesReached', { max: MAX_IMAGES }) ??
172
+ `Maximum ${MAX_IMAGES} images already uploaded`,
173
+ severity: 'warning',
427
174
  });
428
- return null;
429
- }
430
- };
431
-
432
- /**
433
- * Handles file selection event
434
- * Validates files and processes them to extract text
435
- * Updates preview files state with processed content
436
- */
437
- const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
438
- const files = Array.from(e.target.files || []);
439
- if (files.length === 0) return;
440
-
441
- setIsLoading(true);
442
- clearErrors();
443
-
444
- const newPreviewFiles: { name: string; id: string; content: string }[] = [];
445
-
446
- // Process each selected file
447
- for (const file of files) {
448
- if (!validateFile(file)) continue;
449
-
450
- const fileId = Math.random().toString(36).substr(2, 9);
451
- const text = await processFile(file);
452
-
453
- if (text) {
454
- newPreviewFiles.push({
455
- name: file.name,
456
- id: fileId,
457
- content: text,
458
- });
459
- }
175
+ closeMenu();
176
+ return;
460
177
  }
461
-
462
- // Update preview files if any processing succeeded
463
- if (newPreviewFiles.length > 0) {
464
- setPreviewFiles(newPreviewFiles);
465
- if (newPreviewFiles.length < files.length) {
466
- addError({
467
- message: 'Some files were not processed successfully',
468
- severity: 'warning',
469
- });
470
- }
178
+
179
+ // If all checks pass, click the button in UploadImages component
180
+ const imageButtonElement = imageRef.current?.querySelector('button');
181
+ if (imageButtonElement) {
182
+ imageButtonElement.click();
471
183
  }
184
+ closeMenu();
185
+ };
472
186
 
473
- setIsLoading(false);
474
- if (fileInputRef.current) {
475
- fileInputRef.current.value = '';
476
- }
187
+ // Set loading state for child components
188
+ const handleLoadingChange = (loading: boolean) => {
189
+ setIsLoading(loading);
477
190
  };
478
191
 
479
192
  return (
480
- <div className="relative file-upload-wrapper">
481
- {/* Hidden file input triggered by button click */}
482
- <input
483
- ref={fileInputRef}
484
- type="file"
485
- accept=".pdf,.txt,.md,.json,.xlsx,.csv"
486
- className="memori--upload-file-input"
487
- onChange={handleFileSelect}
488
- multiple
489
- />
490
-
491
- {/* Upload button with loading state */}
193
+ <div className="memori--unified-upload-wrapper">
194
+ {/* Main upload button */}
492
195
  <button
196
+ ref={buttonRef}
493
197
  className={cx(
494
198
  'memori-button',
495
199
  'memori-button--circle',
496
200
  'memori-button--icon-only',
497
201
  'memori-share-button--button',
498
202
  'memori--conversation-button',
203
+ 'memori--unified-upload-button',
499
204
  { 'memori--error': errors.length > 0 }
500
205
  )}
501
- onClick={() => fileInputRef.current?.click()}
206
+ onClick={toggleMenu}
502
207
  disabled={isLoading}
503
- title="Upload file"
208
+ title={t('upload.uploadFiles') ?? 'Upload files'}
504
209
  >
505
210
  {isLoading ? (
506
211
  <Spin spinning className="memori--upload-icon" />
@@ -508,6 +213,98 @@ const FileUploadButton = ({
508
213
  <UploadIcon className="memori--upload-icon" />
509
214
  )}
510
215
  </button>
216
+
217
+ {/* Image count indicator - moved here from UploadImages */}
218
+ {currentImageCount > 0 && (
219
+ <div className={cx(
220
+ 'memori--image-count',
221
+ { 'memori--image-count-full': hasReachedImageLimit }
222
+ )}>
223
+ {currentImageCount}/{MAX_IMAGES}
224
+ </div>
225
+ )}
226
+
227
+ {/* Floating menu */}
228
+ {menuOpen && (
229
+ <div className="memori--upload-menu" ref={menuRef}>
230
+ <div
231
+ className={cx('memori--upload-menu-item', {
232
+ 'memori--upload-menu-item--disabled':
233
+ !authToken || hasReachedDocumentLimit,
234
+ })}
235
+ onClick={handleDocumentClick}
236
+ >
237
+ <DocumentIcon className="memori--upload-menu-icon" />
238
+ <span>
239
+ {t('upload.uploadDocument') ?? 'Upload document'}
240
+ {currentDocumentCount > 0 && (
241
+ <span className="memori--upload-slots-info">
242
+ {hasReachedDocumentLimit
243
+ ? ` (${t('upload.maxReached') ?? 'Max reached'})`
244
+ : ` (${remainingDocumentSlots} ${t('upload.remaining') ?? 'remaining'})`
245
+ }
246
+ </span>
247
+ )}
248
+ </span>
249
+ </div>
250
+
251
+ <div
252
+ className={cx('memori--upload-menu-item', {
253
+ 'memori--upload-menu-item--disabled':
254
+ !isMediaAccepted || !authToken || hasReachedImageLimit,
255
+ })}
256
+ onClick={handleImageClick}
257
+ title={
258
+ !authToken
259
+ ? t('upload.loginRequired') ?? 'Login Required'
260
+ : !isMediaAccepted
261
+ ? t('upload.mediaNotAccepted') ?? 'Media uploads not accepted'
262
+ : hasReachedImageLimit
263
+ ? t('upload.maxImagesReached', { max: MAX_IMAGES }) ?? `Maximum ${MAX_IMAGES} images already uploaded`
264
+ : remainingSlots === 1
265
+ ? t('upload.lastImageSlot') ?? 'Upload last image'
266
+ : t('upload.uploadImage', { remaining: remainingSlots }) ?? `Upload image (${remainingSlots} remaining)`
267
+ }
268
+ >
269
+ <ImageIcon className="memori--upload-menu-icon-image" />
270
+ <span>
271
+ {t('upload.uploadImage') ?? 'Upload image'}
272
+ {currentImageCount > 0 && (
273
+ <span className="memori--upload-slots-info">
274
+ {hasReachedImageLimit
275
+ ? ` (${t('upload.maxReached') ?? 'Max reached'})`
276
+ : ` (${remainingSlots} ${t('upload.remaining') ?? 'remaining'})`
277
+ }
278
+ </span>
279
+ )}
280
+ </span>
281
+ </div>
282
+ </div>
283
+ )}
284
+
285
+ {/* Hidden components */}
286
+ <div className="memori--hidden-uploader" ref={documentRef}>
287
+ <UploadDocuments
288
+ setDocumentPreviewFiles={handleDocumentFiles}
289
+ maxDocuments={MAX_DOCUMENTS}
290
+ documentPreviewFiles={documentPreviewFiles}
291
+ // onLoadingChange={handleLoadingChange}
292
+ />
293
+ </div>
294
+
295
+ <div className="memori--hidden-uploader" ref={imageRef}>
296
+ <UploadImages
297
+ authToken={authToken}
298
+ apiUrl={apiUrl}
299
+ setDocumentPreviewFiles={setDocumentPreviewFiles}
300
+ sessionID={sessionID}
301
+ documentPreviewFiles={documentPreviewFiles}
302
+ isMediaAccepted={isMediaAccepted}
303
+ onLoadingChange={handleLoadingChange}
304
+ // Pass the constants to UploadImages
305
+ maxImages={MAX_IMAGES}
306
+ />
307
+ </div>
511
308
 
512
309
  {/* Error messages container */}
513
310
  <div className="memori--error-message-container">
@@ -516,15 +313,28 @@ const FileUploadButton = ({
516
313
  key={`${error.message}-${index}`}
517
314
  open={true}
518
315
  type={error.severity}
519
- title={'File upload failed'}
316
+ title={'Upload notification'}
520
317
  description={error.message}
521
318
  onClose={() => removeError(error.message)}
522
319
  width="350px"
523
320
  />
524
321
  ))}
525
322
  </div>
323
+
324
+ {/* Login tip */}
325
+ {!authToken && menuOpen && (
326
+ <div className="memori--login-tip">
327
+ <Alert
328
+ type="info"
329
+ title={t('upload.loginRequired') ?? 'Login Required'}
330
+ description={t('upload.loginRequiredDescription') ?? 'Please login to upload images'}
331
+ width="350px"
332
+ onClose={closeMenu}
333
+ />
334
+ </div>
335
+ )}
526
336
  </div>
527
337
  );
528
338
  };
529
339
 
530
- export default FileUploadButton;
340
+ export default UploadButton;