@memori.ai/memori-react 8.12.0 → 8.13.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 +24 -0
- package/dist/components/Chat/Chat.css +37 -3
- package/dist/components/Chat/Chat.js +60 -22
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/ChatBubble/ChatBubble.css +9 -5
- package/dist/components/ChatBubble/ChatBubble.js +54 -11
- package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
- package/dist/components/ChatInputs/ChatInputs.css +293 -17
- package/dist/components/ChatInputs/ChatInputs.js +41 -25
- package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
- package/dist/components/ChatTextArea/ChatTextArea.css +75 -31
- package/dist/components/ChatTextArea/ChatTextArea.js +47 -18
- package/dist/components/ChatTextArea/ChatTextArea.js.map +1 -1
- package/dist/components/FilePreview/FilePreview.css +225 -146
- package/dist/components/FilePreview/FilePreview.d.ts +1 -2
- package/dist/components/FilePreview/FilePreview.js +20 -6
- package/dist/components/FilePreview/FilePreview.js.map +1 -1
- package/dist/components/Header/Header.css +2 -2
- package/dist/components/MediaWidget/MediaItemWidget.js +2 -1
- package/dist/components/MediaWidget/MediaItemWidget.js.map +1 -1
- package/dist/components/MediaWidget/MediaWidget.css +0 -4
- package/dist/components/MemoriWidget/MemoriWidget.css +11 -2
- package/dist/components/MemoriWidget/MemoriWidget.js +41 -5
- package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/dist/components/MicrophoneButton/MicrophoneButton.css +2 -2
- package/dist/components/StartPanel/StartPanel.css +8 -0
- package/dist/components/UploadButton/UploadButton.css +20 -17
- package/dist/components/UploadButton/UploadButton.js +218 -89
- package/dist/components/UploadButton/UploadButton.js.map +1 -1
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +14 -4
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
- package/dist/components/UploadButton/UploadImages/UploadImages.js +143 -16
- package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
- package/dist/components/layouts/chat.css +1 -1
- package/dist/helpers/constants.d.ts +1 -0
- package/dist/helpers/constants.js +2 -1
- package/dist/helpers/constants.js.map +1 -1
- package/dist/helpers/imageCompression.d.ts +7 -0
- package/dist/helpers/imageCompression.js +123 -0
- package/dist/helpers/imageCompression.js.map +1 -0
- package/dist/locales/de.json +7 -4
- package/dist/locales/en.json +8 -5
- package/dist/locales/es.json +7 -4
- package/dist/locales/fr.json +7 -4
- package/dist/locales/it.json +8 -5
- package/dist/styles.css +1 -2
- package/esm/components/Chat/Chat.css +37 -3
- package/esm/components/Chat/Chat.js +60 -22
- package/esm/components/Chat/Chat.js.map +1 -1
- package/esm/components/ChatBubble/ChatBubble.css +9 -5
- package/esm/components/ChatBubble/ChatBubble.js +54 -11
- package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
- package/esm/components/ChatInputs/ChatInputs.css +293 -17
- package/esm/components/ChatInputs/ChatInputs.js +42 -26
- package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
- package/esm/components/ChatTextArea/ChatTextArea.css +75 -31
- package/esm/components/ChatTextArea/ChatTextArea.js +49 -20
- package/esm/components/ChatTextArea/ChatTextArea.js.map +1 -1
- package/esm/components/FilePreview/FilePreview.css +225 -146
- package/esm/components/FilePreview/FilePreview.d.ts +1 -2
- package/esm/components/FilePreview/FilePreview.js +21 -7
- package/esm/components/FilePreview/FilePreview.js.map +1 -1
- package/esm/components/Header/Header.css +2 -2
- package/esm/components/MediaWidget/MediaItemWidget.js +2 -1
- package/esm/components/MediaWidget/MediaItemWidget.js.map +1 -1
- package/esm/components/MediaWidget/MediaWidget.css +0 -4
- package/esm/components/MemoriWidget/MemoriWidget.css +11 -2
- package/esm/components/MemoriWidget/MemoriWidget.js +41 -5
- package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/esm/components/MicrophoneButton/MicrophoneButton.css +2 -2
- package/esm/components/StartPanel/StartPanel.css +8 -0
- package/esm/components/UploadButton/UploadButton.css +20 -17
- package/esm/components/UploadButton/UploadButton.js +219 -90
- package/esm/components/UploadButton/UploadButton.js.map +1 -1
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +14 -4
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
- package/esm/components/UploadButton/UploadImages/UploadImages.js +143 -16
- package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
- package/esm/components/layouts/chat.css +1 -1
- package/esm/helpers/constants.d.ts +1 -0
- package/esm/helpers/constants.js +1 -0
- package/esm/helpers/constants.js.map +1 -1
- package/esm/helpers/imageCompression.d.ts +7 -0
- package/esm/helpers/imageCompression.js +119 -0
- package/esm/helpers/imageCompression.js.map +1 -0
- package/esm/locales/de.json +7 -4
- package/esm/locales/en.json +8 -5
- package/esm/locales/es.json +7 -4
- package/esm/locales/fr.json +7 -4
- package/esm/locales/it.json +8 -5
- package/esm/styles.css +1 -2
- package/package.json +1 -1
- package/src/components/Chat/Chat.css +37 -3
- package/src/components/Chat/Chat.tsx +89 -21
- package/src/components/Chat/__snapshots__/Chat.test.tsx.snap +672 -732
- package/src/components/ChatBubble/ChatBubble.css +9 -5
- package/src/components/ChatBubble/ChatBubble.tsx +111 -20
- package/src/components/ChatBubble/__snapshots__/ChatBubble.test.tsx.snap +4 -4
- package/src/components/ChatInputs/ChatInputs.css +293 -17
- package/src/components/ChatInputs/ChatInputs.tsx +144 -87
- package/src/components/ChatInputs/__snapshots__/ChatInputs.test.tsx.snap +430 -424
- package/src/components/ChatTextArea/ChatTextArea.css +75 -31
- package/src/components/ChatTextArea/ChatTextArea.test.tsx +1 -16
- package/src/components/ChatTextArea/ChatTextArea.tsx +51 -22
- package/src/components/ChatTextArea/__snapshots__/ChatTextArea.test.tsx.snap +9 -72
- package/src/components/FilePreview/FilePreview.css +225 -146
- package/src/components/FilePreview/FilePreview.tsx +49 -36
- package/src/components/FilePreview/__snapshots__/FilePreview.test.tsx.snap +2 -2
- package/src/components/Header/Header.css +2 -2
- package/src/components/MediaWidget/MediaItemWidget.tsx +2 -1
- package/src/components/MediaWidget/MediaWidget.css +0 -4
- package/src/components/MemoriWidget/MemoriWidget.css +11 -2
- package/src/components/MemoriWidget/MemoriWidget.tsx +61 -12
- package/src/components/MicrophoneButton/MicrophoneButton.css +2 -2
- package/src/components/StartPanel/StartPanel.css +8 -0
- package/src/components/UploadButton/UploadButton.css +20 -17
- package/src/components/UploadButton/UploadButton.stories.tsx +247 -35
- package/src/components/UploadButton/UploadButton.tsx +280 -175
- package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +19 -4
- package/src/components/UploadButton/UploadImages/UploadImages.tsx +195 -35
- package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +10 -1
- package/src/components/layouts/chat.css +1 -1
- package/src/helpers/constants.ts +1 -1
- package/src/helpers/imageCompression.ts +230 -0
- package/src/locales/de.json +7 -4
- package/src/locales/en.json +8 -5
- package/src/locales/es.json +7 -4
- package/src/locales/fr.json +7 -4
- package/src/locales/it.json +8 -5
- package/src/styles.css +1 -2
- package/src/components/UploadMenu/UploadMenu.css +0 -47
- package/src/components/UploadMenu/UploadMenu.stories.tsx +0 -66
- package/src/components/UploadMenu/UploadMenu.test.tsx +0 -34
- package/src/components/UploadMenu/UploadMenu.tsx +0 -68
- package/src/components/UploadMenu/__snapshots__/UploadMenu.test.tsx.snap +0 -137
|
@@ -8,6 +8,7 @@ import memoriApiClient from '@memori.ai/memori-api-client';
|
|
|
8
8
|
import { Asset, Medium } from '@memori.ai/memori-api-client/dist/types';
|
|
9
9
|
import { useTranslation } from 'react-i18next';
|
|
10
10
|
import Button from '../../ui/Button';
|
|
11
|
+
import { compressImage } from '../../../helpers/imageCompression';
|
|
11
12
|
|
|
12
13
|
// Types
|
|
13
14
|
type PreviewFile = {
|
|
@@ -74,10 +75,8 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
74
75
|
}
|
|
75
76
|
}, [isLoading, onLoadingChange]);
|
|
76
77
|
|
|
77
|
-
// Check current
|
|
78
|
-
const
|
|
79
|
-
(file: any) => file.type === 'image'
|
|
80
|
-
).length;
|
|
78
|
+
// Check current total media count (images + documents)
|
|
79
|
+
const currentMediaCount = documentPreviewFiles.length;
|
|
81
80
|
|
|
82
81
|
// Image upload
|
|
83
82
|
const validateImageFile = (file: File): boolean => {
|
|
@@ -91,42 +90,192 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
91
90
|
const files = Array.from(e.target.files || []);
|
|
92
91
|
if (files.length === 0) return;
|
|
93
92
|
|
|
94
|
-
// Check if adding
|
|
95
|
-
if (
|
|
93
|
+
// Check if adding these files would exceed the total media limit
|
|
94
|
+
if (currentMediaCount + files.length > maxImages) {
|
|
96
95
|
onImageError?.({
|
|
97
96
|
message:
|
|
98
|
-
|
|
99
|
-
`Maximum ${maxImages} images allowed.`,
|
|
97
|
+
`Maximum ${maxImages} media files allowed. You can upload ${Math.max(0, maxImages - currentMediaCount)} more file${maxImages - currentMediaCount !== 1 ? 's' : ''}.`,
|
|
100
98
|
severity: 'error',
|
|
101
99
|
});
|
|
102
100
|
return;
|
|
103
101
|
}
|
|
104
102
|
|
|
105
|
-
|
|
103
|
+
// Validate all files first
|
|
104
|
+
const validFiles = files.filter(file => {
|
|
105
|
+
if (!validateImageFile(file)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
});
|
|
106
110
|
|
|
107
|
-
if (
|
|
111
|
+
if (validFiles.length === 0) {
|
|
108
112
|
if (imageInputRef.current) {
|
|
109
113
|
imageInputRef.current.value = '';
|
|
110
114
|
}
|
|
111
115
|
return;
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
// If only one file, show modal for title input
|
|
119
|
+
if (validFiles.length === 1) {
|
|
120
|
+
const file = validFiles[0];
|
|
121
|
+
setSelectedFile(file);
|
|
122
|
+
setFilePreview(URL.createObjectURL(file));
|
|
123
|
+
|
|
124
|
+
// Set initial title as filename without extension
|
|
125
|
+
const fileName = file.name.split('.').slice(0, -1).join('.');
|
|
126
|
+
setImageTitle(fileName);
|
|
127
|
+
|
|
128
|
+
// Show upload modal with preview
|
|
129
|
+
setShowUploadModal(true);
|
|
130
|
+
} else {
|
|
131
|
+
// For multiple files, upload them directly with default titles
|
|
132
|
+
await uploadMultipleImages(validFiles);
|
|
133
|
+
}
|
|
124
134
|
|
|
125
135
|
if (imageInputRef.current) {
|
|
126
136
|
imageInputRef.current.value = '';
|
|
127
137
|
}
|
|
128
138
|
};
|
|
129
139
|
|
|
140
|
+
const uploadMultipleImages = async (files: File[]) => {
|
|
141
|
+
setIsLoading(true);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const uploadPromises = files.map(async (file) => {
|
|
145
|
+
// Compress image before upload
|
|
146
|
+
let fileToUpload = file;
|
|
147
|
+
try {
|
|
148
|
+
fileToUpload = await compressImage(file);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// If compression fails, use original file
|
|
151
|
+
console.warn('Image compression failed, using original file', error);
|
|
152
|
+
fileToUpload = file;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return new Promise<{
|
|
156
|
+
name: string;
|
|
157
|
+
id: string;
|
|
158
|
+
content: string;
|
|
159
|
+
type: string;
|
|
160
|
+
mediumID: string | undefined;
|
|
161
|
+
url: string;
|
|
162
|
+
mimeType: string;
|
|
163
|
+
} | null>((resolve) => {
|
|
164
|
+
const reader = new FileReader();
|
|
165
|
+
|
|
166
|
+
reader.onload = async (e) => {
|
|
167
|
+
const fileDataUrl = e.target?.result as string;
|
|
168
|
+
const fileId = Math.random().toString(36).substr(2, 9);
|
|
169
|
+
const fileName = fileToUpload.name.split('.').slice(0, -1).join('.');
|
|
170
|
+
|
|
171
|
+
if (client) {
|
|
172
|
+
try {
|
|
173
|
+
let asset: Asset;
|
|
174
|
+
let response;
|
|
175
|
+
|
|
176
|
+
if (authToken && backend?.uploadAsset) {
|
|
177
|
+
response = await backend.uploadAsset(
|
|
178
|
+
fileToUpload.name,
|
|
179
|
+
fileDataUrl,
|
|
180
|
+
authToken
|
|
181
|
+
);
|
|
182
|
+
} else if (memoriID && sessionID && backend?.uploadAssetUnlogged) {
|
|
183
|
+
response = await backend.uploadAssetUnlogged(
|
|
184
|
+
fileToUpload.name,
|
|
185
|
+
fileDataUrl,
|
|
186
|
+
memoriID,
|
|
187
|
+
sessionID
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (!response) {
|
|
191
|
+
throw new Error('Upload failed');
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
throw new Error('Missing required parameters for upload');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
asset = response.asset;
|
|
198
|
+
if (response.resultCode !== 0) {
|
|
199
|
+
throw new Error(response.resultMessage || 'Upload failed');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let medium: any = null;
|
|
203
|
+
if (dialog?.postMediumSelectedEvent && sessionID) {
|
|
204
|
+
medium = await dialog.postMediumSelectedEvent(sessionID, {
|
|
205
|
+
url: asset.assetURL,
|
|
206
|
+
mimeType: asset.mimeType,
|
|
207
|
+
} as Medium);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let finalMediumID: string | undefined = undefined;
|
|
211
|
+
if (medium?.currentState?.currentMedia) {
|
|
212
|
+
const existingMediumIDs = new Set(
|
|
213
|
+
documentPreviewFiles.map((file: any) => file.mediumID)
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
finalMediumID = medium.currentState.currentMedia.find(
|
|
217
|
+
(media: any) => !existingMediumIDs.has(media.mediumID)
|
|
218
|
+
)?.mediumID;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
resolve({
|
|
222
|
+
name: fileName,
|
|
223
|
+
id: fileId,
|
|
224
|
+
url: asset.assetURL,
|
|
225
|
+
content: asset.assetURL,
|
|
226
|
+
type: 'image',
|
|
227
|
+
mediumID: finalMediumID,
|
|
228
|
+
mimeType: asset.mimeType,
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
onImageError?.({
|
|
232
|
+
message: t('upload.uploadFailed') ?? 'Upload failed',
|
|
233
|
+
severity: 'error',
|
|
234
|
+
});
|
|
235
|
+
resolve(null);
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
onImageError?.({
|
|
239
|
+
message:
|
|
240
|
+
t('upload.apiClientNotConfigured') ??
|
|
241
|
+
'API client not configured properly for media upload',
|
|
242
|
+
severity: 'warning',
|
|
243
|
+
});
|
|
244
|
+
resolve(null);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
reader.onerror = () => {
|
|
249
|
+
onImageError?.({
|
|
250
|
+
message: t('upload.fileReadingFailed') ?? 'File reading failed',
|
|
251
|
+
severity: 'error',
|
|
252
|
+
});
|
|
253
|
+
resolve(null);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
reader.readAsDataURL(fileToUpload);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const results = await Promise.all(uploadPromises);
|
|
261
|
+
const successfulUploads = results.filter(result => result !== null);
|
|
262
|
+
|
|
263
|
+
if (successfulUploads.length > 0) {
|
|
264
|
+
setDocumentPreviewFiles((prevFiles: any[]) => [
|
|
265
|
+
...prevFiles,
|
|
266
|
+
...successfulUploads,
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
onImageError?.({
|
|
271
|
+
message: t('upload.uploadFailed') ?? 'Upload failed',
|
|
272
|
+
severity: 'error',
|
|
273
|
+
});
|
|
274
|
+
} finally {
|
|
275
|
+
setIsLoading(false);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
130
279
|
const handleTitleSubmit = async () => {
|
|
131
280
|
if (!selectedFile || !imageTitle.trim()) return;
|
|
132
281
|
|
|
@@ -134,6 +283,16 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
134
283
|
setShowUploadModal(false);
|
|
135
284
|
|
|
136
285
|
try {
|
|
286
|
+
// Compress image before upload
|
|
287
|
+
let fileToUpload = selectedFile;
|
|
288
|
+
try {
|
|
289
|
+
fileToUpload = await compressImage(selectedFile);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// If compression fails, use original file
|
|
292
|
+
console.warn('Image compression failed, using original file', error);
|
|
293
|
+
fileToUpload = selectedFile;
|
|
294
|
+
}
|
|
295
|
+
|
|
137
296
|
const reader = new FileReader();
|
|
138
297
|
|
|
139
298
|
reader.onload = async e => {
|
|
@@ -147,14 +306,14 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
147
306
|
|
|
148
307
|
if (authToken && backend?.uploadAsset) {
|
|
149
308
|
response = await backend.uploadAsset(
|
|
150
|
-
|
|
309
|
+
fileToUpload.name,
|
|
151
310
|
fileDataUrl,
|
|
152
311
|
authToken,
|
|
153
|
-
memoriID
|
|
312
|
+
// memoriID
|
|
154
313
|
);
|
|
155
314
|
} else if (memoriID && sessionID && backend?.uploadAssetUnlogged) {
|
|
156
315
|
response = await backend.uploadAssetUnlogged(
|
|
157
|
-
|
|
316
|
+
fileToUpload.name,
|
|
158
317
|
fileDataUrl,
|
|
159
318
|
memoriID,
|
|
160
319
|
sessionID
|
|
@@ -241,7 +400,7 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
241
400
|
setIsLoading(false);
|
|
242
401
|
};
|
|
243
402
|
|
|
244
|
-
reader.readAsDataURL(
|
|
403
|
+
reader.readAsDataURL(fileToUpload);
|
|
245
404
|
} catch (error) {
|
|
246
405
|
onImageError?.({
|
|
247
406
|
message: t('upload.uploadFailed') ?? 'Upload failed',
|
|
@@ -265,10 +424,11 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
265
424
|
ref={imageInputRef}
|
|
266
425
|
type="file"
|
|
267
426
|
accept=".jpg,.jpeg,.png"
|
|
427
|
+
multiple
|
|
268
428
|
className="memori--upload-file-input"
|
|
269
429
|
onChange={handleImageUpload}
|
|
270
430
|
disabled={
|
|
271
|
-
isLoading || !isMediaAccepted ||
|
|
431
|
+
isLoading || !isMediaAccepted || currentMediaCount >= maxImages
|
|
272
432
|
}
|
|
273
433
|
/>
|
|
274
434
|
|
|
@@ -284,7 +444,7 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
284
444
|
)}
|
|
285
445
|
onClick={() => imageInputRef.current?.click()}
|
|
286
446
|
disabled={
|
|
287
|
-
isLoading || !isMediaAccepted ||
|
|
447
|
+
isLoading || !isMediaAccepted || currentMediaCount >= maxImages
|
|
288
448
|
}
|
|
289
449
|
>
|
|
290
450
|
{isLoading ? (
|
|
@@ -302,7 +462,7 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
302
462
|
className="memori--modal-preview-file"
|
|
303
463
|
onClose={handleCancelUpload}
|
|
304
464
|
closable
|
|
305
|
-
title={t('upload.titleImage', { title: imageTitle })}
|
|
465
|
+
// title={t('upload.titleImage', { title: imageTitle })}
|
|
306
466
|
// description={t('upload.imageTitleDescription')}
|
|
307
467
|
>
|
|
308
468
|
<div
|
|
@@ -338,20 +498,20 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
338
498
|
style={{ width: '90%', marginBottom: '20px' }}
|
|
339
499
|
className="memori--upload-title-input"
|
|
340
500
|
/>
|
|
341
|
-
<div style={{ display: 'flex', gap: '10px' }}>
|
|
501
|
+
<div style={{ display: 'flex', gap: '10px', justifyContent: 'center', alignItems: 'center' }}>
|
|
502
|
+
<Button
|
|
503
|
+
onClick={handleCancelUpload}
|
|
504
|
+
className="memori-button memori-button--outline memori--upload-image"
|
|
505
|
+
>
|
|
506
|
+
{t('cancel') ?? 'Cancel'}
|
|
507
|
+
</Button>
|
|
342
508
|
<Button
|
|
343
509
|
onClick={handleTitleSubmit}
|
|
344
510
|
disabled={!selectedFile || !imageTitle.trim()}
|
|
345
|
-
className="memori-button memori-button--primary memori-button--image-confirm"
|
|
511
|
+
className="memori-button memori-button--primary memori-button--image-confirm memori--upload-image"
|
|
346
512
|
>
|
|
347
513
|
{t('confirm') ?? 'Confirm'}
|
|
348
514
|
</Button>
|
|
349
|
-
<Button
|
|
350
|
-
onClick={handleCancelUpload}
|
|
351
|
-
className="memori-button memori-button--primary"
|
|
352
|
-
>
|
|
353
|
-
{t('cancel') ?? 'Cancel'}
|
|
354
|
-
</Button>
|
|
355
515
|
</div>
|
|
356
516
|
</div>
|
|
357
517
|
</div>
|
|
@@ -5,6 +5,13 @@ exports[`renders UploadButton unchanged 1`] = `
|
|
|
5
5
|
<div
|
|
6
6
|
class="memori--unified-upload-wrapper"
|
|
7
7
|
>
|
|
8
|
+
<input
|
|
9
|
+
accept=".jpg,.jpeg,.png,.pdf,.txt,.json,.xlsx,.csv,.md,.html"
|
|
10
|
+
class="memori--upload-file-input"
|
|
11
|
+
multiple=""
|
|
12
|
+
style="display: none;"
|
|
13
|
+
type="file"
|
|
14
|
+
/>
|
|
8
15
|
<button
|
|
9
16
|
class="memori-button memori-button--circle memori-button--icon-only memori-share-button--button memori--conversation-button memori--unified-upload-button"
|
|
10
17
|
title="upload.uploadFiles"
|
|
@@ -45,8 +52,9 @@ exports[`renders UploadButton unchanged 1`] = `
|
|
|
45
52
|
class="memori--document-upload-wrapper"
|
|
46
53
|
>
|
|
47
54
|
<input
|
|
48
|
-
accept=".pdf,.txt,.md,.json,.xlsx,.csv"
|
|
55
|
+
accept=".pdf,.txt,.md,.json,.xlsx,.csv,.html"
|
|
49
56
|
class="memori--upload-file-input"
|
|
57
|
+
multiple=""
|
|
50
58
|
type="file"
|
|
51
59
|
/>
|
|
52
60
|
<button
|
|
@@ -108,6 +116,7 @@ exports[`renders UploadButton unchanged 1`] = `
|
|
|
108
116
|
accept=".jpg,.jpeg,.png"
|
|
109
117
|
class="memori--upload-file-input"
|
|
110
118
|
disabled=""
|
|
119
|
+
multiple=""
|
|
111
120
|
type="file"
|
|
112
121
|
/>
|
|
113
122
|
<button
|
package/src/helpers/constants.ts
CHANGED
|
@@ -215,5 +215,5 @@ export const MAX_MSG_WORDS = 300;
|
|
|
215
215
|
|
|
216
216
|
// Document upload limits
|
|
217
217
|
export const MAX_DOCUMENTS_PER_MESSAGE = 5;
|
|
218
|
-
|
|
218
|
+
export const MAX_TOTAL_MESSAGE_PAYLOAD = 200000; // 200KB total payload limit
|
|
219
219
|
export const MAX_DOCUMENT_CONTENT_LENGTH = 200000; // 200KB per document content
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image compression utility using HTML5 Canvas API
|
|
3
|
+
* Compresses images before upload to reduce file size and LLM context usage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface CompressionOptions {
|
|
7
|
+
maxWidth?: number;
|
|
8
|
+
maxHeight?: number;
|
|
9
|
+
quality?: number;
|
|
10
|
+
maxSizeMB?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_OPTIONS: Required<CompressionOptions> = {
|
|
14
|
+
maxWidth: 1920,
|
|
15
|
+
maxHeight: 1920,
|
|
16
|
+
quality: 0.85,
|
|
17
|
+
maxSizeMB: 2,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Apply EXIF orientation to canvas context
|
|
23
|
+
*/
|
|
24
|
+
const applyOrientation = (
|
|
25
|
+
ctx: CanvasRenderingContext2D,
|
|
26
|
+
orientation: number,
|
|
27
|
+
width: number,
|
|
28
|
+
height: number
|
|
29
|
+
) => {
|
|
30
|
+
switch (orientation) {
|
|
31
|
+
case 2:
|
|
32
|
+
// Horizontal flip
|
|
33
|
+
ctx.transform(-1, 0, 0, 1, width, 0);
|
|
34
|
+
break;
|
|
35
|
+
case 3:
|
|
36
|
+
// 180° rotation
|
|
37
|
+
ctx.transform(-1, 0, 0, -1, width, height);
|
|
38
|
+
break;
|
|
39
|
+
case 4:
|
|
40
|
+
// Vertical flip
|
|
41
|
+
ctx.transform(1, 0, 0, -1, 0, height);
|
|
42
|
+
break;
|
|
43
|
+
case 5:
|
|
44
|
+
// Vertical flip + 90° rotation
|
|
45
|
+
ctx.transform(0, 1, 1, 0, 0, 0);
|
|
46
|
+
break;
|
|
47
|
+
case 6:
|
|
48
|
+
// 90° rotation
|
|
49
|
+
ctx.transform(0, 1, -1, 0, height, 0);
|
|
50
|
+
break;
|
|
51
|
+
case 7:
|
|
52
|
+
// Horizontal flip + 90° rotation
|
|
53
|
+
ctx.transform(0, -1, -1, 0, height, width);
|
|
54
|
+
break;
|
|
55
|
+
case 8:
|
|
56
|
+
// 270° rotation
|
|
57
|
+
ctx.transform(0, -1, 1, 0, 0, width);
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
// No transformation needed
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load image from File and return HTMLImageElement
|
|
67
|
+
*/
|
|
68
|
+
const loadImage = (file: File): Promise<HTMLImageElement> => {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const img = new Image();
|
|
71
|
+
const url = URL.createObjectURL(file);
|
|
72
|
+
|
|
73
|
+
img.onload = () => {
|
|
74
|
+
URL.revokeObjectURL(url);
|
|
75
|
+
resolve(img);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
img.onerror = () => {
|
|
79
|
+
URL.revokeObjectURL(url);
|
|
80
|
+
reject(new Error('Failed to load image'));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
img.src = url;
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Calculate new dimensions maintaining aspect ratio
|
|
89
|
+
*/
|
|
90
|
+
const calculateDimensions = (
|
|
91
|
+
width: number,
|
|
92
|
+
height: number,
|
|
93
|
+
maxWidth: number,
|
|
94
|
+
maxHeight: number
|
|
95
|
+
): { width: number; height: number } => {
|
|
96
|
+
// If image is already smaller than max dimensions, return original
|
|
97
|
+
if (width <= maxWidth && height <= maxHeight) {
|
|
98
|
+
return { width, height };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Calculate scale factor
|
|
102
|
+
const scaleWidth = maxWidth / width;
|
|
103
|
+
const scaleHeight = maxHeight / height;
|
|
104
|
+
const scale = Math.min(scaleWidth, scaleHeight, 1);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
width: Math.round(width * scale),
|
|
108
|
+
height: Math.round(height * scale),
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Compress image using Canvas API
|
|
114
|
+
* @param file - Original image file
|
|
115
|
+
* @param options - Compression options
|
|
116
|
+
* @returns Compressed File object
|
|
117
|
+
*/
|
|
118
|
+
export const compressImage = async (
|
|
119
|
+
file: File,
|
|
120
|
+
options: CompressionOptions = {}
|
|
121
|
+
): Promise<File> => {
|
|
122
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
123
|
+
|
|
124
|
+
// Check if compression is needed
|
|
125
|
+
const fileSizeMB = file.size / (1024 * 1024);
|
|
126
|
+
const shouldCompress = fileSizeMB > 1; // Compress if > 1 MB
|
|
127
|
+
|
|
128
|
+
if (!shouldCompress) {
|
|
129
|
+
// Load image to check dimensions
|
|
130
|
+
try {
|
|
131
|
+
const img = await loadImage(file);
|
|
132
|
+
const needsResize =
|
|
133
|
+
img.width > opts.maxWidth || img.height > opts.maxHeight;
|
|
134
|
+
|
|
135
|
+
if (!needsResize) {
|
|
136
|
+
// File is already small enough, return original
|
|
137
|
+
return file;
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// If we can't load the image, return original
|
|
141
|
+
return file;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// Load image
|
|
147
|
+
const img = await loadImage(file);
|
|
148
|
+
|
|
149
|
+
// Get orientation (simplified - full EXIF parsing would require a library)
|
|
150
|
+
const orientation = 1;
|
|
151
|
+
|
|
152
|
+
// Calculate new dimensions
|
|
153
|
+
const { width, height } = calculateDimensions(
|
|
154
|
+
img.width,
|
|
155
|
+
img.height,
|
|
156
|
+
opts.maxWidth,
|
|
157
|
+
opts.maxHeight
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Create canvas
|
|
161
|
+
const canvas = document.createElement('canvas');
|
|
162
|
+
canvas.width = width;
|
|
163
|
+
canvas.height = height;
|
|
164
|
+
|
|
165
|
+
const ctx = canvas.getContext('2d');
|
|
166
|
+
if (!ctx) {
|
|
167
|
+
throw new Error('Failed to get canvas context');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Apply orientation transformation
|
|
171
|
+
applyOrientation(ctx, orientation, width, height);
|
|
172
|
+
|
|
173
|
+
// Draw image to canvas
|
|
174
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
175
|
+
|
|
176
|
+
// Convert canvas to blob
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
canvas.toBlob(
|
|
179
|
+
(blob) => {
|
|
180
|
+
if (!blob) {
|
|
181
|
+
reject(new Error('Failed to compress image'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check if compressed size is acceptable
|
|
186
|
+
const compressedSizeMB = blob.size / (1024 * 1024);
|
|
187
|
+
|
|
188
|
+
if (compressedSizeMB > opts.maxSizeMB) {
|
|
189
|
+
// If still too large, try lower quality
|
|
190
|
+
canvas.toBlob(
|
|
191
|
+
(lowerQualityBlob) => {
|
|
192
|
+
if (!lowerQualityBlob) {
|
|
193
|
+
// Fallback to original blob
|
|
194
|
+
const compressedFile = new File(
|
|
195
|
+
[blob],
|
|
196
|
+
file.name.replace(/\.[^/.]+$/, '.jpg'),
|
|
197
|
+
{ type: 'image/jpeg' }
|
|
198
|
+
);
|
|
199
|
+
resolve(compressedFile);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const compressedFile = new File(
|
|
203
|
+
[lowerQualityBlob],
|
|
204
|
+
file.name.replace(/\.[^/.]+$/, '.jpg'),
|
|
205
|
+
{ type: 'image/jpeg' }
|
|
206
|
+
);
|
|
207
|
+
resolve(compressedFile);
|
|
208
|
+
},
|
|
209
|
+
'image/jpeg',
|
|
210
|
+
Math.max(0.5, opts.quality - 0.2) // Reduce quality by 0.2, min 0.5
|
|
211
|
+
);
|
|
212
|
+
} else {
|
|
213
|
+
// Create new File with JPEG extension
|
|
214
|
+
const compressedFile = new File(
|
|
215
|
+
[blob],
|
|
216
|
+
file.name.replace(/\.[^/.]+$/, '.jpg'),
|
|
217
|
+
{ type: 'image/jpeg' }
|
|
218
|
+
);
|
|
219
|
+
resolve(compressedFile);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
'image/jpeg', // Always convert to JPEG for better compression
|
|
223
|
+
opts.quality
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
} catch (error) {
|
|
227
|
+
// If compression fails, return original file
|
|
228
|
+
return file;
|
|
229
|
+
}
|
|
230
|
+
};
|
package/src/locales/de.json
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"date": "Geburtsdatum",
|
|
28
28
|
"day": "Tag",
|
|
29
29
|
"copied": "Kopiert!",
|
|
30
|
+
"placeholder": "Stellen Sie eine Frage",
|
|
30
31
|
"month": "Monat",
|
|
31
32
|
"year": "Jahr",
|
|
32
33
|
"createdAt": "Erstellungsdatum",
|
|
@@ -237,9 +238,10 @@
|
|
|
237
238
|
"upload": {
|
|
238
239
|
"loginRequired": "Login erforderlich",
|
|
239
240
|
"loginRequiredDescription": "Bitte melden Sie sich an, um Bilder hochzuladen",
|
|
240
|
-
"uploadFiles": "Dateien hochladen",
|
|
241
|
-
"
|
|
242
|
-
"
|
|
241
|
+
"uploadFiles": "Dateien hochladen ({{shortcut}}+O zum Öffnen des Datei-Selektors)",
|
|
242
|
+
"uploadFilesWithShortcut": "{{shortcut}}+O zum Öffnen des Datei-Selektors",
|
|
243
|
+
"uploadImages": "Bilder hochladen",
|
|
244
|
+
"uploadDocuments": "Dokumente hochladen",
|
|
243
245
|
"replace": "Ersetzen",
|
|
244
246
|
"remaining": "verbleibend",
|
|
245
247
|
"maxReached": "Maximal erreicht",
|
|
@@ -248,7 +250,8 @@
|
|
|
248
250
|
"uploadFailed": "Hochladen fehlgeschlagen",
|
|
249
251
|
"uploadSuccess": "Hochladen erfolgreich",
|
|
250
252
|
"uploadSuccessDescription": "Die Datei wurde erfolgreich hochgeladen",
|
|
251
|
-
"maxImagesReached": "Sie können bis zu {{max}} Bilder hochladen"
|
|
253
|
+
"maxImagesReached": "Sie können bis zu {{max}} Bilder hochladen",
|
|
254
|
+
"dragAndDropFiles": "Ziehen Sie Dateien hierher, um sie zur Chat hinzuzufügen"
|
|
252
255
|
},
|
|
253
256
|
"media": {
|
|
254
257
|
"title": "Titel",
|
package/src/locales/en.json
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"all": "All",
|
|
23
23
|
"today": "Today",
|
|
24
24
|
"yesterday": "Yesterday",
|
|
25
|
+
"placeholder": "Ask a question",
|
|
25
26
|
"last_7_days": "Last 7 days",
|
|
26
27
|
"last_30_days": "Last 30 days",
|
|
27
28
|
"month": "Month",
|
|
@@ -250,14 +251,15 @@
|
|
|
250
251
|
"upload": {
|
|
251
252
|
"loginRequired": "Login required",
|
|
252
253
|
"loginRequiredDescription": "Please login to upload images",
|
|
253
|
-
"uploadFiles": "Upload files",
|
|
254
|
-
"
|
|
254
|
+
"uploadFiles": "Upload files ({{shortcut}}+O to open file chooser)",
|
|
255
|
+
"uploadFilesWithShortcut": "{{shortcut}}+O to open file chooser",
|
|
256
|
+
"uploadImages": "Upload images",
|
|
255
257
|
"replace": "Replace",
|
|
256
258
|
"maxImagesReached": "You can upload up to {{max}} images",
|
|
257
259
|
"maxDocumentsReached": "You can upload up to {{max}} documents",
|
|
258
260
|
"remaining": "remaining",
|
|
259
261
|
"lastDocumentSlot": "Upload last document",
|
|
260
|
-
"
|
|
262
|
+
"uploadDocuments": "Upload documents",
|
|
261
263
|
"maxReached": "Max limit reached",
|
|
262
264
|
"imageTitle": "Image title: {{title}}",
|
|
263
265
|
"titleHelp": "Adding a descriptive title helps the AI provide more context and appropriate responses.",
|
|
@@ -266,12 +268,13 @@
|
|
|
266
268
|
"fileReadingFailed": "File reading failed",
|
|
267
269
|
"uploadFailed": "Upload failed",
|
|
268
270
|
"uploadSuccess": "Upload success",
|
|
269
|
-
"titleImage": "Image: {{title}}",
|
|
271
|
+
"titleImage": "Image title: {{title}}",
|
|
270
272
|
"uploadSuccessDescription": "The file has been uploaded successfully",
|
|
271
273
|
"titleImageUpload": "Upload images",
|
|
272
274
|
"partialUpload": "Only {{uploaded}} images on {{total}} will be uploaded. Maximum {{max}} images allowed.",
|
|
273
275
|
"maxImages": "Maximum {{max}} images allowed.",
|
|
274
|
-
"upload": "Upload"
|
|
276
|
+
"upload": "Upload",
|
|
277
|
+
"dragAndDropFiles": "Drag and drop files here to add them to the chat"
|
|
275
278
|
},
|
|
276
279
|
"media": {
|
|
277
280
|
"title": "Title",
|