@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.
- package/CHANGELOG.md +42 -0
- package/dist/components/Chat/Chat.d.ts +5 -10
- package/dist/components/Chat/Chat.js +3 -3
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/ChatBubble/ChatBubble.css +10 -0
- package/dist/components/ChatBubble/ChatBubble.d.ts +1 -0
- package/dist/components/ChatBubble/ChatBubble.js +16 -20
- package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
- package/dist/components/ChatInputs/ChatInputs.d.ts +6 -10
- package/dist/components/ChatInputs/ChatInputs.js +37 -30
- package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
- package/dist/components/FilePreview/FilePreview.css +169 -140
- package/dist/components/FilePreview/FilePreview.d.ts +2 -6
- package/dist/components/FilePreview/FilePreview.js +58 -5
- package/dist/components/FilePreview/FilePreview.js.map +1 -1
- package/dist/components/MemoriWidget/MemoriWidget.d.ts +2 -1
- package/dist/components/MemoriWidget/MemoriWidget.js +15 -21
- package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/dist/components/UploadButton/UploadButton.css +506 -27
- package/dist/components/UploadButton/UploadButton.d.ts +14 -11
- package/dist/components/UploadButton/UploadButton.js +110 -288
- package/dist/components/UploadButton/UploadButton.js.map +1 -1
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +19 -0
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +211 -0
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -0
- package/dist/components/UploadButton/UploadImages/UploadImages.d.ts +13 -0
- package/dist/components/UploadButton/UploadImages/UploadImages.js +207 -0
- package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -0
- package/dist/components/icons/Bug.d.ts +5 -0
- package/dist/components/icons/Bug.js +6 -0
- package/dist/components/icons/Bug.js.map +1 -0
- package/dist/components/icons/Document.d.ts +5 -0
- package/dist/components/icons/Document.js +10 -0
- package/dist/components/icons/Document.js.map +1 -0
- package/dist/components/icons/Image.d.ts +4 -0
- package/dist/components/icons/Image.js +9 -0
- package/dist/components/icons/Image.js.map +1 -0
- package/dist/components/icons/Preview.d.ts +4 -5
- package/dist/components/icons/Preview.js +5 -2
- package/dist/components/icons/Preview.js.map +1 -1
- package/dist/components/icons/Upload.d.ts +4 -5
- package/dist/components/icons/Upload.js +5 -2
- package/dist/components/icons/Upload.js.map +1 -1
- package/dist/components/layouts/HiddenChat.js +100 -10
- package/dist/components/layouts/HiddenChat.js.map +1 -1
- package/dist/components/layouts/hidden-chat.css +189 -119
- package/dist/locales/de.json +16 -0
- package/dist/locales/en.json +24 -0
- package/dist/locales/es.json +16 -0
- package/dist/locales/fr.json +16 -0
- package/dist/locales/it.json +22 -0
- package/esm/components/Chat/Chat.d.ts +5 -10
- package/esm/components/Chat/Chat.js +3 -3
- package/esm/components/Chat/Chat.js.map +1 -1
- package/esm/components/ChatBubble/ChatBubble.css +10 -0
- package/esm/components/ChatBubble/ChatBubble.d.ts +1 -0
- package/esm/components/ChatBubble/ChatBubble.js +16 -20
- package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
- package/esm/components/ChatInputs/ChatInputs.d.ts +6 -10
- package/esm/components/ChatInputs/ChatInputs.js +37 -30
- package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
- package/esm/components/FilePreview/FilePreview.css +169 -140
- package/esm/components/FilePreview/FilePreview.d.ts +2 -6
- package/esm/components/FilePreview/FilePreview.js +58 -5
- package/esm/components/FilePreview/FilePreview.js.map +1 -1
- package/esm/components/MemoriWidget/MemoriWidget.d.ts +2 -1
- package/esm/components/MemoriWidget/MemoriWidget.js +15 -21
- package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/esm/components/UploadButton/UploadButton.css +506 -27
- package/esm/components/UploadButton/UploadButton.d.ts +14 -11
- package/esm/components/UploadButton/UploadButton.js +111 -289
- package/esm/components/UploadButton/UploadButton.js.map +1 -1
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +19 -0
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +208 -0
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -0
- package/esm/components/UploadButton/UploadImages/UploadImages.d.ts +13 -0
- package/esm/components/UploadButton/UploadImages/UploadImages.js +204 -0
- package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -0
- package/esm/components/icons/Bug.d.ts +5 -0
- package/esm/components/icons/Bug.js +4 -0
- package/esm/components/icons/Bug.js.map +1 -0
- package/esm/components/icons/Document.d.ts +5 -0
- package/esm/components/icons/Document.js +6 -0
- package/esm/components/icons/Document.js.map +1 -0
- package/esm/components/icons/Image.d.ts +4 -0
- package/esm/components/icons/Image.js +5 -0
- package/esm/components/icons/Image.js.map +1 -0
- package/esm/components/icons/Preview.d.ts +4 -5
- package/esm/components/icons/Preview.js +4 -3
- package/esm/components/icons/Preview.js.map +1 -1
- package/esm/components/icons/Upload.d.ts +4 -5
- package/esm/components/icons/Upload.js +4 -3
- package/esm/components/icons/Upload.js.map +1 -1
- package/esm/components/layouts/HiddenChat.js +101 -11
- package/esm/components/layouts/HiddenChat.js.map +1 -1
- package/esm/components/layouts/hidden-chat.css +189 -119
- package/esm/locales/de.json +16 -0
- package/esm/locales/en.json +24 -0
- package/esm/locales/es.json +16 -0
- package/esm/locales/fr.json +16 -0
- package/esm/locales/it.json +22 -0
- package/package.json +1 -1
- package/src/components/Chat/Chat.tsx +8 -8
- package/src/components/ChatBubble/ChatBubble.css +10 -0
- package/src/components/ChatBubble/ChatBubble.stories.tsx +25 -0
- package/src/components/ChatBubble/ChatBubble.tsx +41 -17
- package/src/components/ChatInputs/ChatInputs.tsx +92 -43
- package/src/components/FilePreview/FilePreview.css +169 -140
- package/src/components/FilePreview/FilePreview.tsx +106 -14
- package/src/components/FilePreview/__snapshots__/FilePreview.test.tsx.snap +146 -29
- package/src/components/MemoriWidget/MemoriWidget.tsx +14 -22
- package/src/components/UploadButton/UploadButton.css +506 -27
- package/src/components/UploadButton/UploadButton.stories.tsx +122 -20
- package/src/components/UploadButton/UploadButton.test.tsx +1 -1
- package/src/components/UploadButton/UploadButton.tsx +264 -454
- package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +352 -0
- package/src/components/UploadButton/UploadImages/UploadImages.tsx +434 -0
- package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +140 -13
- package/src/components/icons/Bug.tsx +81 -0
- package/src/components/icons/Document.tsx +50 -0
- package/src/components/icons/Image.tsx +37 -0
- package/src/components/icons/Preview.tsx +28 -22
- package/src/components/icons/Upload.tsx +33 -22
- package/src/components/layouts/HiddenChat.tsx +143 -7
- package/src/components/layouts/__snapshots__/HiddenChat.test.tsx.snap +1 -1
- package/src/components/layouts/hidden-chat.css +189 -119
- package/src/index.stories.tsx +19 -19
- package/src/locales/de.json +16 -0
- package/src/locales/en.json +24 -0
- package/src/locales/es.json +16 -0
- package/src/locales/fr.json +16 -0
- package/src/locales/it.json +22 -0
|
@@ -1,506 +1,211 @@
|
|
|
1
|
-
import React, { useState, useRef } from 'react';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
const UploadButton: React.FC<UploadManagerProps> = ({
|
|
33
|
+
authToken = '',
|
|
34
|
+
apiUrl = '',
|
|
35
|
+
sessionID = '',
|
|
36
|
+
isMediaAccepted = false,
|
|
37
|
+
setDocumentPreviewFiles,
|
|
38
|
+
documentPreviewFiles,
|
|
43
39
|
}) => {
|
|
44
|
-
// State
|
|
40
|
+
// State
|
|
45
41
|
const [isLoading, setIsLoading] = useState(false);
|
|
46
|
-
|
|
47
|
-
const [errors, setErrors] = useState<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
60
|
-
|
|
69
|
+
const addError = (error: {
|
|
70
|
+
message: string;
|
|
71
|
+
severity: 'error' | 'warning' | 'info';
|
|
72
|
+
}) => {
|
|
61
73
|
setErrors(prev => [...prev, error]);
|
|
62
|
-
|
|
63
|
-
setTimeout(() => {
|
|
64
|
-
removeError(error.message);
|
|
65
|
-
}, 5000);
|
|
74
|
+
setTimeout(() => removeError(error.message), 5000);
|
|
66
75
|
};
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
86
|
+
// Click outside handler
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
216
89
|
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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:
|
|
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
|
-
|
|
156
|
+
closeMenu();
|
|
157
|
+
return;
|
|
369
158
|
}
|
|
370
|
-
|
|
371
|
-
if (
|
|
159
|
+
|
|
160
|
+
if (!isMediaAccepted) {
|
|
372
161
|
addError({
|
|
373
|
-
message:
|
|
374
|
-
|
|
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
|
-
|
|
165
|
+
closeMenu();
|
|
166
|
+
return;
|
|
380
167
|
}
|
|
381
|
-
|
|
382
|
-
|
|
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:
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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="
|
|
481
|
-
{/*
|
|
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={
|
|
206
|
+
onClick={toggleMenu}
|
|
502
207
|
disabled={isLoading}
|
|
503
|
-
title=
|
|
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={'
|
|
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
|
|
340
|
+
export default UploadButton;
|