@memori.ai/memori-react 7.33.4 → 7.34.1
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 +34 -0
- package/dist/components/ChatHistoryDrawer/ChatHistory.js +49 -29
- package/dist/components/ChatHistoryDrawer/ChatHistory.js.map +1 -1
- package/dist/components/MemoriWidget/MemoriWidget.js +6 -1
- package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/dist/components/UploadButton/UploadButton.css +32 -12
- package/dist/components/UploadButton/UploadButton.js +96 -20
- package/dist/components/UploadButton/UploadButton.js.map +1 -1
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +12 -0
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +80 -57
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
- package/dist/components/UploadButton/UploadImages/UploadImages.d.ts +5 -0
- package/dist/components/UploadButton/UploadImages/UploadImages.js +10 -42
- package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
- package/dist/helpers/constants.d.ts +3 -0
- package/dist/helpers/constants.js +4 -1
- package/dist/helpers/constants.js.map +1 -1
- package/dist/helpers/utils.d.ts +1 -0
- package/dist/helpers/utils.js +10 -1
- package/dist/helpers/utils.js.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/locales/en.json +3 -1
- package/dist/locales/it.json +2 -0
- package/esm/components/ChatHistoryDrawer/ChatHistory.js +49 -29
- package/esm/components/ChatHistoryDrawer/ChatHistory.js.map +1 -1
- package/esm/components/MemoriWidget/MemoriWidget.js +7 -2
- package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/esm/components/UploadButton/UploadButton.css +32 -12
- package/esm/components/UploadButton/UploadButton.js +96 -20
- package/esm/components/UploadButton/UploadButton.js.map +1 -1
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +12 -0
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +81 -58
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
- package/esm/components/UploadButton/UploadImages/UploadImages.d.ts +5 -0
- package/esm/components/UploadButton/UploadImages/UploadImages.js +10 -42
- package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
- package/esm/helpers/constants.d.ts +3 -0
- package/esm/helpers/constants.js +3 -0
- package/esm/helpers/constants.js.map +1 -1
- package/esm/helpers/utils.d.ts +1 -0
- package/esm/helpers/utils.js +8 -0
- package/esm/helpers/utils.js.map +1 -1
- package/esm/index.js +8 -0
- package/esm/index.js.map +1 -1
- package/esm/locales/en.json +3 -1
- package/esm/locales/it.json +2 -0
- package/package.json +2 -2
- package/src/__snapshots__/index.test.tsx.snap +41 -0
- package/src/components/ChatHistoryDrawer/ChatHistory.stories.tsx +40 -1
- package/src/components/ChatHistoryDrawer/ChatHistory.tsx +106 -58
- package/src/components/MemoriWidget/MemoriWidget.tsx +13 -1
- package/src/components/UploadButton/UploadButton.css +32 -12
- package/src/components/UploadButton/UploadButton.tsx +145 -21
- package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +105 -87
- package/src/components/UploadButton/UploadImages/UploadImages.tsx +12 -76
- package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +0 -6
- package/src/helpers/constants.ts +5 -0
- package/src/helpers/utils.ts +13 -0
- package/src/index.test.tsx +65 -0
- package/src/index.tsx +16 -0
- package/src/locales/en.json +3 -1
- package/src/locales/it.json +2 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useRef } from 'react';
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
2
|
import cx from 'classnames';
|
|
3
3
|
import { UploadIcon } from '../../icons/Upload';
|
|
4
4
|
import Spin from '../../ui/Spin';
|
|
@@ -7,14 +7,9 @@ import { DocumentIcon } from '../../icons/Document';
|
|
|
7
7
|
import CloseIcon from '../../icons/Close';
|
|
8
8
|
import Button from '../../ui/Button';
|
|
9
9
|
import Modal from '../../ui/Modal';
|
|
10
|
+
import { MAX_DOCUMENT_CONTENT_LENGTH, MAX_TOTAL_MESSAGE_PAYLOAD } from '../../../helpers/constants';
|
|
10
11
|
|
|
11
12
|
// Types
|
|
12
|
-
type UploadError = {
|
|
13
|
-
message: string;
|
|
14
|
-
severity: 'error' | 'warning' | 'info';
|
|
15
|
-
fileId?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
13
|
type PreviewFile = {
|
|
19
14
|
name: string;
|
|
20
15
|
id: string;
|
|
@@ -26,8 +21,6 @@ type PreviewFile = {
|
|
|
26
21
|
};
|
|
27
22
|
|
|
28
23
|
// Constants
|
|
29
|
-
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
30
|
-
const MAX_TEXT_LENGTH = 100000; // 100,000 characters
|
|
31
24
|
const PDF_JS_VERSION = '3.11.174';
|
|
32
25
|
const WORKER_URL = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDF_JS_VERSION}/pdf.worker.min.js`;
|
|
33
26
|
const PDF_JS_URL = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDF_JS_VERSION}/pdf.min.js`;
|
|
@@ -46,68 +39,63 @@ interface UploadDocumentsProps {
|
|
|
46
39
|
setDocumentPreviewFiles: (files: { name: string; id: string; content: string; mimeType: string }[]) => void;
|
|
47
40
|
maxDocuments?: number;
|
|
48
41
|
documentPreviewFiles: any;
|
|
42
|
+
onLoadingChange?: (loading: boolean) => void;
|
|
43
|
+
onDocumentError?: (error: { message: string; severity: 'error' | 'warning' | 'info' }) => void;
|
|
44
|
+
onValidateFile?: (file: File) => boolean;
|
|
45
|
+
onValidatePayloadSize?: (newDocuments: { name: string; id: string; content: string; mimeType: string }[]) => boolean;
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
52
49
|
setDocumentPreviewFiles,
|
|
53
50
|
maxDocuments,
|
|
54
51
|
documentPreviewFiles,
|
|
52
|
+
onLoadingChange,
|
|
53
|
+
onDocumentError,
|
|
54
|
+
onValidateFile,
|
|
55
|
+
onValidatePayloadSize,
|
|
55
56
|
}) => {
|
|
56
57
|
// State
|
|
57
58
|
const [isLoading, setIsLoading] = useState(false);
|
|
58
|
-
const [errors, setErrors] = useState<UploadError[]>([]);
|
|
59
59
|
const [selectedFile, setSelectedFile] = useState<PreviewFile | null>(null);
|
|
60
60
|
|
|
61
61
|
// Refs
|
|
62
62
|
const documentInputRef = useRef<HTMLInputElement>(null);
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const addError = (error: UploadError) => {
|
|
72
|
-
setErrors(prev => [...prev, error]);
|
|
73
|
-
setTimeout(() => removeError(error.message), 5000);
|
|
74
|
-
};
|
|
64
|
+
// Update loading state in parent component
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (onLoadingChange) {
|
|
67
|
+
onLoadingChange(isLoading);
|
|
68
|
+
}
|
|
69
|
+
}, [isLoading, onLoadingChange]);
|
|
75
70
|
|
|
76
71
|
// Document upload
|
|
77
72
|
const validateDocumentFile = (file: File): boolean => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (!ALLOWED_FILE_TYPES.includes(fileExt)) {
|
|
82
|
-
addError({
|
|
83
|
-
message: `File type "${fileExt}" is not supported. Please use: ${ALLOWED_FILE_TYPES.join(', ')}`,
|
|
84
|
-
severity: 'error',
|
|
85
|
-
fileId: file.name,
|
|
86
|
-
});
|
|
87
|
-
return false;
|
|
73
|
+
if (onValidateFile) {
|
|
74
|
+
return onValidateFile(file);
|
|
88
75
|
}
|
|
76
|
+
return true;
|
|
77
|
+
};
|
|
89
78
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
fileId: file.name,
|
|
95
|
-
});
|
|
96
|
-
return false;
|
|
79
|
+
// Validate total payload size
|
|
80
|
+
const validatePayloadSize = (newDocuments: { name: string; id: string; content: string; mimeType: string }[]): boolean => {
|
|
81
|
+
if (onValidatePayloadSize) {
|
|
82
|
+
return onValidatePayloadSize(newDocuments);
|
|
97
83
|
}
|
|
98
|
-
|
|
99
84
|
return true;
|
|
100
85
|
};
|
|
101
86
|
|
|
102
87
|
const extractTextFromPDF = async (file: File): Promise<string> => {
|
|
88
|
+
console.log('Extracting text from PDF:', file.name);
|
|
103
89
|
try {
|
|
104
90
|
// Load PDF.js if not already loaded
|
|
105
91
|
if (!window.pdfjsLib) {
|
|
92
|
+
console.log('Loading PDF.js library...');
|
|
106
93
|
await new Promise((resolve, reject) => {
|
|
107
94
|
const script = document.createElement('script');
|
|
108
95
|
script.src = PDF_JS_URL;
|
|
109
96
|
script.onload = () => {
|
|
110
97
|
window.pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_URL;
|
|
98
|
+
console.log('PDF.js loaded successfully');
|
|
111
99
|
resolve(true);
|
|
112
100
|
};
|
|
113
101
|
script.onerror = reject;
|
|
@@ -118,10 +106,12 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
118
106
|
// Extract text from PDF
|
|
119
107
|
const arrayBuffer = await file.arrayBuffer();
|
|
120
108
|
const pdf = await window.pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
|
109
|
+
console.log('PDF loaded, pages:', pdf.numPages);
|
|
121
110
|
let text = '';
|
|
122
111
|
|
|
123
112
|
// Iterate through each page and extract text
|
|
124
113
|
for (let i = 1; i <= pdf.numPages; i++) {
|
|
114
|
+
console.log('Processing page', i);
|
|
125
115
|
const page = await pdf.getPage(i);
|
|
126
116
|
const content = await page.getTextContent();
|
|
127
117
|
const pageText = content.items
|
|
@@ -131,15 +121,19 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
131
121
|
text += pageText + '\n';
|
|
132
122
|
}
|
|
133
123
|
|
|
124
|
+
console.log('PDF text extraction complete');
|
|
134
125
|
return text;
|
|
135
126
|
} catch (error) {
|
|
127
|
+
console.error('PDF extraction failed:', error);
|
|
136
128
|
throw new Error(`PDF extraction failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
137
129
|
}
|
|
138
130
|
};
|
|
139
131
|
|
|
140
132
|
const extractTextFromXLSX = async (file: File): Promise<string> => {
|
|
133
|
+
console.log('Extracting text from XLSX:', file.name);
|
|
141
134
|
try {
|
|
142
135
|
if (!window.XLSX) {
|
|
136
|
+
console.log('Loading XLSX library...');
|
|
143
137
|
await new Promise((resolve, reject) => {
|
|
144
138
|
const script = document.createElement('script');
|
|
145
139
|
script.src = XLSX_URL;
|
|
@@ -147,6 +141,7 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
147
141
|
script.onerror = reject;
|
|
148
142
|
document.head.appendChild(script);
|
|
149
143
|
});
|
|
144
|
+
console.log('XLSX library loaded successfully');
|
|
150
145
|
}
|
|
151
146
|
|
|
152
147
|
const arrayBuffer = await file.arrayBuffer();
|
|
@@ -157,9 +152,11 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
157
152
|
cellText: true,
|
|
158
153
|
cellDates: true,
|
|
159
154
|
});
|
|
155
|
+
console.log('XLSX workbook loaded, sheets:', workbook.SheetNames);
|
|
160
156
|
|
|
161
157
|
let text = '';
|
|
162
158
|
for (const sheetName of workbook.SheetNames) {
|
|
159
|
+
console.log('Processing sheet:', sheetName);
|
|
163
160
|
const worksheet = workbook.Sheets[sheetName];
|
|
164
161
|
const data = window.XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false });
|
|
165
162
|
|
|
@@ -191,13 +188,16 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
191
188
|
text += `Sheet: ${sheetName}\n${formattedText.join('\n')}\n\n`;
|
|
192
189
|
}
|
|
193
190
|
|
|
191
|
+
console.log('XLSX text extraction complete');
|
|
194
192
|
return text;
|
|
195
193
|
} catch (error) {
|
|
194
|
+
console.error('XLSX extraction failed:', error);
|
|
196
195
|
throw new Error(`XLSX extraction failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
197
196
|
}
|
|
198
197
|
};
|
|
199
198
|
|
|
200
199
|
const processDocumentFile = async (file: File): Promise<string | null> => {
|
|
200
|
+
console.log('Processing document file:', file.name);
|
|
201
201
|
const fileExt = file.name.split('.').pop()?.toLowerCase() || '';
|
|
202
202
|
|
|
203
203
|
try {
|
|
@@ -211,62 +211,93 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
211
211
|
text = await extractTextFromXLSX(file);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
if (text && text.length >
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
if (text && text.length > MAX_DOCUMENT_CONTENT_LENGTH) {
|
|
215
|
+
console.warn('Document content exceeds length limit:', text.length, '>', MAX_DOCUMENT_CONTENT_LENGTH);
|
|
216
|
+
onDocumentError?.({
|
|
217
|
+
message: `File "${file.name}" content exceeds ${MAX_DOCUMENT_CONTENT_LENGTH} characters and was truncated`,
|
|
217
218
|
severity: 'warning',
|
|
218
|
-
fileId: file.name,
|
|
219
219
|
});
|
|
220
|
-
text = text.substring(0,
|
|
220
|
+
text = text.substring(0, MAX_DOCUMENT_CONTENT_LENGTH) + "\n\n[Content truncated due to size limits]";
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
console.log('Document processing complete');
|
|
223
224
|
return text;
|
|
224
225
|
} catch (error) {
|
|
226
|
+
console.error('Document processing failed:', error);
|
|
225
227
|
throw new Error(`Failed to process "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
226
228
|
}
|
|
227
229
|
};
|
|
228
230
|
|
|
229
231
|
const handleDocumentUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
232
|
+
console.log('Document upload started');
|
|
230
233
|
const files = Array.from(e.target.files || []);
|
|
231
234
|
if (files.length === 0) return;
|
|
232
235
|
|
|
233
|
-
// Only process the first file
|
|
234
|
-
const file = files[0];
|
|
235
|
-
|
|
236
236
|
setIsLoading(true);
|
|
237
|
-
clearErrors();
|
|
238
237
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
// Process each file
|
|
239
|
+
const processedFiles: { name: string; id: string; content: string; mimeType: string }[] = [];
|
|
240
|
+
|
|
241
|
+
for (const file of files) {
|
|
242
|
+
console.log('Processing file:', file.name);
|
|
243
|
+
if (!validateDocumentFile(file)) {
|
|
244
|
+
continue;
|
|
243
245
|
}
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
246
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
const text = await processDocumentFile(file);
|
|
247
|
+
const fileId = Math.random().toString(36).substr(2, 9);
|
|
251
248
|
|
|
252
|
-
|
|
249
|
+
try {
|
|
250
|
+
const text = await processDocumentFile(file);
|
|
253
251
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
252
|
+
if (text) {
|
|
253
|
+
processedFiles.push({
|
|
254
|
+
name: file.name,
|
|
255
|
+
id: fileId,
|
|
256
|
+
content: text,
|
|
257
|
+
mimeType: file.type,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error('File processing error:', error);
|
|
262
|
+
onDocumentError?.({
|
|
263
|
+
message: `${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
264
|
+
severity: 'error',
|
|
265
|
+
});
|
|
261
266
|
}
|
|
262
|
-
} catch (error) {
|
|
263
|
-
addError({
|
|
264
|
-
message: `${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
265
|
-
severity: 'error',
|
|
266
|
-
fileId: file.name,
|
|
267
|
-
});
|
|
268
267
|
}
|
|
269
268
|
|
|
269
|
+
// Add new documents to existing ones
|
|
270
|
+
if (processedFiles.length > 0) {
|
|
271
|
+
console.log('Successfully processed files:', processedFiles.length);
|
|
272
|
+
// Validate total payload size
|
|
273
|
+
if (!validatePayloadSize(processedFiles)) {
|
|
274
|
+
setIsLoading(false);
|
|
275
|
+
if (documentInputRef.current) {
|
|
276
|
+
documentInputRef.current.value = '';
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const existingDocuments = documentPreviewFiles.filter(
|
|
282
|
+
(file: any) => file.type === 'document'
|
|
283
|
+
);
|
|
284
|
+
const existingImages = documentPreviewFiles.filter(
|
|
285
|
+
(file: any) => file.type === 'image'
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
console.log('existingDocuments', existingDocuments);
|
|
289
|
+
console.log('processedFiles', processedFiles);
|
|
290
|
+
console.log('existingImages', existingImages);
|
|
291
|
+
setDocumentPreviewFiles([
|
|
292
|
+
...existingDocuments,
|
|
293
|
+
...processedFiles.map(file => ({
|
|
294
|
+
...file,
|
|
295
|
+
type: 'document'
|
|
296
|
+
})),
|
|
297
|
+
]);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.log('Document upload complete');
|
|
270
301
|
setIsLoading(false);
|
|
271
302
|
if (documentInputRef.current) {
|
|
272
303
|
documentInputRef.current.value = '';
|
|
@@ -275,6 +306,7 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
275
306
|
|
|
276
307
|
return (
|
|
277
308
|
<div className="memori--document-upload-wrapper">
|
|
309
|
+
|
|
278
310
|
{/* Hidden file input */}
|
|
279
311
|
<input
|
|
280
312
|
ref={documentInputRef}
|
|
@@ -293,7 +325,7 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
293
325
|
'memori-share-button--button',
|
|
294
326
|
'memori--conversation-button',
|
|
295
327
|
'memori--document-upload-button',
|
|
296
|
-
{ 'memori--error': errors.length > 0
|
|
328
|
+
{ 'memori--error': false } // Removed errors.length > 0
|
|
297
329
|
)}
|
|
298
330
|
onClick={() => documentInputRef.current?.click()}
|
|
299
331
|
disabled={isLoading || (maxDocuments && documentPreviewFiles.filter((file: any) => file.type !== 'image').length >= maxDocuments) || false}
|
|
@@ -331,20 +363,6 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
331
363
|
</div>
|
|
332
364
|
</Modal>
|
|
333
365
|
|
|
334
|
-
{/* Error messages container */}
|
|
335
|
-
<div className="memori--error-message-container">
|
|
336
|
-
{errors.map((error, index) => (
|
|
337
|
-
<Alert
|
|
338
|
-
key={`${error.message}-${index}`}
|
|
339
|
-
open={true}
|
|
340
|
-
type={error.severity}
|
|
341
|
-
title={'Upload notification'}
|
|
342
|
-
description={error.message}
|
|
343
|
-
onClose={() => removeError(error.message)}
|
|
344
|
-
width="350px"
|
|
345
|
-
/>
|
|
346
|
-
))}
|
|
347
|
-
</div>
|
|
348
366
|
</div>
|
|
349
367
|
);
|
|
350
368
|
};
|
|
@@ -10,12 +10,6 @@ import { useTranslation } from 'react-i18next';
|
|
|
10
10
|
import Button from '../../ui/Button';
|
|
11
11
|
|
|
12
12
|
// Types
|
|
13
|
-
type UploadError = {
|
|
14
|
-
message: string;
|
|
15
|
-
severity: 'error' | 'warning' | 'info';
|
|
16
|
-
fileId?: string;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
13
|
type PreviewFile = {
|
|
20
14
|
name: string;
|
|
21
15
|
id: string;
|
|
@@ -28,9 +22,6 @@ type PreviewFile = {
|
|
|
28
22
|
title?: string;
|
|
29
23
|
};
|
|
30
24
|
|
|
31
|
-
// Constants
|
|
32
|
-
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
33
|
-
|
|
34
25
|
// Props interface
|
|
35
26
|
interface UploadImagesProps {
|
|
36
27
|
authToken?: string;
|
|
@@ -42,10 +33,10 @@ interface UploadImagesProps {
|
|
|
42
33
|
onLoadingChange?: (loading: boolean) => void;
|
|
43
34
|
maxImages?: number;
|
|
44
35
|
memoriID?: string;
|
|
36
|
+
onImageError?: (error: { message: string; severity: 'error' | 'warning' | 'info' }) => void;
|
|
37
|
+
onValidateImageFile?: (file: File) => boolean;
|
|
45
38
|
}
|
|
46
39
|
|
|
47
|
-
const ALLOWED_FILE_TYPES = ['.jpg', '.jpeg', '.png'];
|
|
48
|
-
|
|
49
40
|
const UploadImages: React.FC<UploadImagesProps> = ({
|
|
50
41
|
authToken = '',
|
|
51
42
|
sessionID = '',
|
|
@@ -56,6 +47,8 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
56
47
|
onLoadingChange,
|
|
57
48
|
maxImages = 5,
|
|
58
49
|
memoriID = '',
|
|
50
|
+
onImageError,
|
|
51
|
+
onValidateImageFile,
|
|
59
52
|
}) => {
|
|
60
53
|
const { t, i18n } = useTranslation();
|
|
61
54
|
// Client
|
|
@@ -66,7 +59,6 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
66
59
|
|
|
67
60
|
// State
|
|
68
61
|
const [isLoading, setIsLoading] = useState(false);
|
|
69
|
-
const [errors, setErrors] = useState<UploadError[]>([]);
|
|
70
62
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
71
63
|
const [filePreview, setFilePreview] = useState<string | null>(null);
|
|
72
64
|
const [imageTitle, setImageTitle] = useState('');
|
|
@@ -82,18 +74,6 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
82
74
|
}
|
|
83
75
|
}, [isLoading, onLoadingChange]);
|
|
84
76
|
|
|
85
|
-
// Error handling
|
|
86
|
-
const clearErrors = () => setErrors([]);
|
|
87
|
-
|
|
88
|
-
const removeError = (errorMessage: string) => {
|
|
89
|
-
setErrors(prev => prev.filter(e => e.message !== errorMessage));
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const addError = (error: UploadError) => {
|
|
93
|
-
setErrors(prev => [...prev, error]);
|
|
94
|
-
setTimeout(() => removeError(error.message), 5000);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
77
|
// Check current image count
|
|
98
78
|
const currentImageCount = documentPreviewFiles.filter(
|
|
99
79
|
(file: any) => file.type === 'image'
|
|
@@ -101,33 +81,9 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
101
81
|
|
|
102
82
|
// Image upload
|
|
103
83
|
const validateImageFile = (file: File): boolean => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
!ALLOWED_FILE_TYPES.includes(fileExt) &&
|
|
108
|
-
!file.type.startsWith('image/')
|
|
109
|
-
) {
|
|
110
|
-
addError({
|
|
111
|
-
message: `File type "${fileExt}" is not supported. Please use: ${ALLOWED_FILE_TYPES.join(
|
|
112
|
-
', '
|
|
113
|
-
)}`,
|
|
114
|
-
severity: 'error',
|
|
115
|
-
fileId: file.name,
|
|
116
|
-
});
|
|
117
|
-
return false;
|
|
84
|
+
if (onValidateImageFile) {
|
|
85
|
+
return onValidateImageFile(file);
|
|
118
86
|
}
|
|
119
|
-
|
|
120
|
-
if (file.size > DEFAULT_MAX_FILE_SIZE) {
|
|
121
|
-
addError({
|
|
122
|
-
message: `File "${file.name}" exceeds ${
|
|
123
|
-
DEFAULT_MAX_FILE_SIZE / 1024 / 1024
|
|
124
|
-
}MB limit`,
|
|
125
|
-
severity: 'error',
|
|
126
|
-
fileId: file.name,
|
|
127
|
-
});
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
87
|
return true;
|
|
132
88
|
};
|
|
133
89
|
|
|
@@ -137,7 +93,7 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
137
93
|
|
|
138
94
|
// Check if adding this file would exceed the limit
|
|
139
95
|
if (currentImageCount >= maxImages) {
|
|
140
|
-
|
|
96
|
+
onImageError?.({
|
|
141
97
|
message:
|
|
142
98
|
t('upload.maxImagesReached') ??
|
|
143
99
|
`Maximum ${maxImages} images allowed.`,
|
|
@@ -146,8 +102,6 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
146
102
|
return;
|
|
147
103
|
}
|
|
148
104
|
|
|
149
|
-
clearErrors();
|
|
150
|
-
|
|
151
105
|
const file = files[0]; // Only handle the first file
|
|
152
106
|
|
|
153
107
|
if (!validateImageFile(file)) {
|
|
@@ -261,14 +215,13 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
261
215
|
]
|
|
262
216
|
);
|
|
263
217
|
} catch (error) {
|
|
264
|
-
|
|
218
|
+
onImageError?.({
|
|
265
219
|
message: t('upload.uploadFailed') ?? 'Upload failed',
|
|
266
220
|
severity: 'error',
|
|
267
|
-
fileId: selectedFile.name,
|
|
268
221
|
});
|
|
269
222
|
}
|
|
270
223
|
} else {
|
|
271
|
-
|
|
224
|
+
onImageError?.({
|
|
272
225
|
message:
|
|
273
226
|
t('upload.apiClientNotConfigured') ??
|
|
274
227
|
'API client not configured properly for media upload',
|
|
@@ -280,20 +233,18 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
280
233
|
};
|
|
281
234
|
|
|
282
235
|
reader.onerror = () => {
|
|
283
|
-
|
|
236
|
+
onImageError?.({
|
|
284
237
|
message: t('upload.fileReadingFailed') ?? 'File reading failed',
|
|
285
238
|
severity: 'error',
|
|
286
|
-
fileId: selectedFile.name,
|
|
287
239
|
});
|
|
288
240
|
setIsLoading(false);
|
|
289
241
|
};
|
|
290
242
|
|
|
291
243
|
reader.readAsDataURL(selectedFile);
|
|
292
244
|
} catch (error) {
|
|
293
|
-
|
|
245
|
+
onImageError?.({
|
|
294
246
|
message: t('upload.uploadFailed') ?? 'Upload failed',
|
|
295
247
|
severity: 'error',
|
|
296
|
-
fileId: selectedFile.name,
|
|
297
248
|
});
|
|
298
249
|
setIsLoading(false);
|
|
299
250
|
}
|
|
@@ -312,7 +263,7 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
312
263
|
<input
|
|
313
264
|
ref={imageInputRef}
|
|
314
265
|
type="file"
|
|
315
|
-
accept=
|
|
266
|
+
accept=".jpg,.jpeg,.png"
|
|
316
267
|
className="memori--upload-file-input"
|
|
317
268
|
onChange={handleImageUpload}
|
|
318
269
|
disabled={
|
|
@@ -404,21 +355,6 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
404
355
|
</div>
|
|
405
356
|
</div>
|
|
406
357
|
</Modal>
|
|
407
|
-
|
|
408
|
-
{/* Error messages container */}
|
|
409
|
-
<div className="memori--error-message-container">
|
|
410
|
-
{errors.map((error, index) => (
|
|
411
|
-
<Alert
|
|
412
|
-
key={`${error.message}-${index}`}
|
|
413
|
-
open={true}
|
|
414
|
-
type={error.severity}
|
|
415
|
-
title={'Upload notification'}
|
|
416
|
-
description={error.message}
|
|
417
|
-
onClose={() => removeError(error.message)}
|
|
418
|
-
width="350px"
|
|
419
|
-
/>
|
|
420
|
-
))}
|
|
421
|
-
</div>
|
|
422
358
|
</div>
|
|
423
359
|
);
|
|
424
360
|
};
|
|
@@ -96,9 +96,6 @@ exports[`renders UploadButton unchanged 1`] = `
|
|
|
96
96
|
/>
|
|
97
97
|
</svg>
|
|
98
98
|
</button>
|
|
99
|
-
<div
|
|
100
|
-
class="memori--error-message-container"
|
|
101
|
-
/>
|
|
102
99
|
</div>
|
|
103
100
|
</div>
|
|
104
101
|
<div
|
|
@@ -149,9 +146,6 @@ exports[`renders UploadButton unchanged 1`] = `
|
|
|
149
146
|
/>
|
|
150
147
|
</svg>
|
|
151
148
|
</button>
|
|
152
|
-
<div
|
|
153
|
-
class="memori--error-message-container"
|
|
154
|
-
/>
|
|
155
149
|
</div>
|
|
156
150
|
</div>
|
|
157
151
|
<div
|
package/src/helpers/constants.ts
CHANGED
|
@@ -186,3 +186,8 @@ export const boardOfExpertsLoadingSentences: {
|
|
|
186
186
|
|
|
187
187
|
export const MAX_MSG_CHARS = 4000;
|
|
188
188
|
export const MAX_MSG_WORDS = 300;
|
|
189
|
+
|
|
190
|
+
// Document upload limits
|
|
191
|
+
export const MAX_DOCUMENTS_PER_MESSAGE = 5;
|
|
192
|
+
export const MAX_TOTAL_MESSAGE_PAYLOAD = 100000; // 100KB total payload limit
|
|
193
|
+
export const MAX_DOCUMENT_CONTENT_LENGTH = 100000; // 100KB per document content
|
package/src/helpers/utils.ts
CHANGED
|
@@ -201,6 +201,19 @@ export const stripOutputTags = (text: string): string => {
|
|
|
201
201
|
return stripOutputTags(strippedText);
|
|
202
202
|
};
|
|
203
203
|
|
|
204
|
+
export const stripReasoningTags = (text: string) => {
|
|
205
|
+
const reasoningTagRegex = /<think.*?<\/think>/gs;
|
|
206
|
+
|
|
207
|
+
if (!reasoningTagRegex.test(text)) {
|
|
208
|
+
return text;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const strippedText = text.replace(reasoningTagRegex, '');
|
|
212
|
+
|
|
213
|
+
// Recursively strip nested reasoning tags
|
|
214
|
+
return stripOutputTags(strippedText);
|
|
215
|
+
};
|
|
216
|
+
|
|
204
217
|
export const stripHTML = (text: string) => {
|
|
205
218
|
const el = document.createElement('div');
|
|
206
219
|
el.innerHTML = text;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { memori, tenant, integration } from './mocks/data';
|
|
4
|
+
import Memori from './index';
|
|
5
|
+
|
|
6
|
+
// Mock window.location
|
|
7
|
+
const mockLocation = {
|
|
8
|
+
hostname: 'localhost',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
Object.defineProperty(window, 'location', {
|
|
12
|
+
value: mockLocation,
|
|
13
|
+
writable: true,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders client', () => {
|
|
17
|
+
const { container } = render(
|
|
18
|
+
<Memori
|
|
19
|
+
memoriID={memori.memoriID}
|
|
20
|
+
ownerUserID={memori.ownerUserID}
|
|
21
|
+
tenantID={tenant.tenantID}
|
|
22
|
+
integration={{
|
|
23
|
+
...integration,
|
|
24
|
+
customData: JSON.stringify({}),
|
|
25
|
+
}}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
expect(container).toMatchSnapshot();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders client with whiteListedDomains on allowed domains', () => {
|
|
32
|
+
mockLocation.hostname = 'memori.ai';
|
|
33
|
+
const { container } = render(
|
|
34
|
+
<Memori
|
|
35
|
+
memoriID={memori.memoriID}
|
|
36
|
+
ownerUserID={memori.ownerUserID}
|
|
37
|
+
tenantID={tenant.tenantID}
|
|
38
|
+
integration={{
|
|
39
|
+
...integration,
|
|
40
|
+
customData: JSON.stringify({
|
|
41
|
+
whiteListedDomains: ['localhost', 'memori.ai'],
|
|
42
|
+
}),
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
expect(container).toMatchSnapshot();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('renders client with whiteListedDomains on not allowed domains', () => {
|
|
50
|
+
mockLocation.hostname = 'aisuru.com';
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<Memori
|
|
53
|
+
memoriID={memori.memoriID}
|
|
54
|
+
ownerUserID={memori.ownerUserID}
|
|
55
|
+
tenantID={tenant.tenantID}
|
|
56
|
+
integration={{
|
|
57
|
+
...integration,
|
|
58
|
+
customData: JSON.stringify({
|
|
59
|
+
whiteListedDomains: ['localhost', 'memori.ai'],
|
|
60
|
+
}),
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
expect(container).toMatchSnapshot();
|
|
65
|
+
});
|
package/src/index.tsx
CHANGED
|
@@ -259,6 +259,22 @@ const Memori: React.FC<Props> = ({
|
|
|
259
259
|
const layoutIntegrationConfig = safeParseJSON(
|
|
260
260
|
layoutIntegration?.customData ?? '{}'
|
|
261
261
|
);
|
|
262
|
+
|
|
263
|
+
const whiteListedDomains = layoutIntegrationConfig.whiteListedDomains;
|
|
264
|
+
if (whiteListedDomains) {
|
|
265
|
+
// check if we are client side
|
|
266
|
+
if (typeof window !== 'undefined') {
|
|
267
|
+
// check if the current domain is in the whiteListedDomains with Regex
|
|
268
|
+
if (
|
|
269
|
+
!whiteListedDomains.some((domain: string) =>
|
|
270
|
+
new RegExp(domain).test(window.location.hostname)
|
|
271
|
+
)
|
|
272
|
+
) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
262
278
|
const initialContextVars =
|
|
263
279
|
context ?? getParsedContext(layoutIntegrationConfig.contextVars);
|
|
264
280
|
const initialQuestionLayout =
|