@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.
Files changed (135) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/components/Chat/Chat.css +37 -3
  3. package/dist/components/Chat/Chat.js +60 -22
  4. package/dist/components/Chat/Chat.js.map +1 -1
  5. package/dist/components/ChatBubble/ChatBubble.css +9 -5
  6. package/dist/components/ChatBubble/ChatBubble.js +54 -11
  7. package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
  8. package/dist/components/ChatInputs/ChatInputs.css +293 -17
  9. package/dist/components/ChatInputs/ChatInputs.js +41 -25
  10. package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
  11. package/dist/components/ChatTextArea/ChatTextArea.css +75 -31
  12. package/dist/components/ChatTextArea/ChatTextArea.js +47 -18
  13. package/dist/components/ChatTextArea/ChatTextArea.js.map +1 -1
  14. package/dist/components/FilePreview/FilePreview.css +225 -146
  15. package/dist/components/FilePreview/FilePreview.d.ts +1 -2
  16. package/dist/components/FilePreview/FilePreview.js +20 -6
  17. package/dist/components/FilePreview/FilePreview.js.map +1 -1
  18. package/dist/components/Header/Header.css +2 -2
  19. package/dist/components/MediaWidget/MediaItemWidget.js +2 -1
  20. package/dist/components/MediaWidget/MediaItemWidget.js.map +1 -1
  21. package/dist/components/MediaWidget/MediaWidget.css +0 -4
  22. package/dist/components/MemoriWidget/MemoriWidget.css +11 -2
  23. package/dist/components/MemoriWidget/MemoriWidget.js +41 -5
  24. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  25. package/dist/components/MicrophoneButton/MicrophoneButton.css +2 -2
  26. package/dist/components/StartPanel/StartPanel.css +8 -0
  27. package/dist/components/UploadButton/UploadButton.css +20 -17
  28. package/dist/components/UploadButton/UploadButton.js +218 -89
  29. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  30. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +14 -4
  31. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  32. package/dist/components/UploadButton/UploadImages/UploadImages.js +143 -16
  33. package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
  34. package/dist/components/layouts/chat.css +1 -1
  35. package/dist/helpers/constants.d.ts +1 -0
  36. package/dist/helpers/constants.js +2 -1
  37. package/dist/helpers/constants.js.map +1 -1
  38. package/dist/helpers/imageCompression.d.ts +7 -0
  39. package/dist/helpers/imageCompression.js +123 -0
  40. package/dist/helpers/imageCompression.js.map +1 -0
  41. package/dist/locales/de.json +7 -4
  42. package/dist/locales/en.json +8 -5
  43. package/dist/locales/es.json +7 -4
  44. package/dist/locales/fr.json +7 -4
  45. package/dist/locales/it.json +8 -5
  46. package/dist/styles.css +1 -2
  47. package/esm/components/Chat/Chat.css +37 -3
  48. package/esm/components/Chat/Chat.js +60 -22
  49. package/esm/components/Chat/Chat.js.map +1 -1
  50. package/esm/components/ChatBubble/ChatBubble.css +9 -5
  51. package/esm/components/ChatBubble/ChatBubble.js +54 -11
  52. package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
  53. package/esm/components/ChatInputs/ChatInputs.css +293 -17
  54. package/esm/components/ChatInputs/ChatInputs.js +42 -26
  55. package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
  56. package/esm/components/ChatTextArea/ChatTextArea.css +75 -31
  57. package/esm/components/ChatTextArea/ChatTextArea.js +49 -20
  58. package/esm/components/ChatTextArea/ChatTextArea.js.map +1 -1
  59. package/esm/components/FilePreview/FilePreview.css +225 -146
  60. package/esm/components/FilePreview/FilePreview.d.ts +1 -2
  61. package/esm/components/FilePreview/FilePreview.js +21 -7
  62. package/esm/components/FilePreview/FilePreview.js.map +1 -1
  63. package/esm/components/Header/Header.css +2 -2
  64. package/esm/components/MediaWidget/MediaItemWidget.js +2 -1
  65. package/esm/components/MediaWidget/MediaItemWidget.js.map +1 -1
  66. package/esm/components/MediaWidget/MediaWidget.css +0 -4
  67. package/esm/components/MemoriWidget/MemoriWidget.css +11 -2
  68. package/esm/components/MemoriWidget/MemoriWidget.js +41 -5
  69. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  70. package/esm/components/MicrophoneButton/MicrophoneButton.css +2 -2
  71. package/esm/components/StartPanel/StartPanel.css +8 -0
  72. package/esm/components/UploadButton/UploadButton.css +20 -17
  73. package/esm/components/UploadButton/UploadButton.js +219 -90
  74. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  75. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +14 -4
  76. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  77. package/esm/components/UploadButton/UploadImages/UploadImages.js +143 -16
  78. package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
  79. package/esm/components/layouts/chat.css +1 -1
  80. package/esm/helpers/constants.d.ts +1 -0
  81. package/esm/helpers/constants.js +1 -0
  82. package/esm/helpers/constants.js.map +1 -1
  83. package/esm/helpers/imageCompression.d.ts +7 -0
  84. package/esm/helpers/imageCompression.js +119 -0
  85. package/esm/helpers/imageCompression.js.map +1 -0
  86. package/esm/locales/de.json +7 -4
  87. package/esm/locales/en.json +8 -5
  88. package/esm/locales/es.json +7 -4
  89. package/esm/locales/fr.json +7 -4
  90. package/esm/locales/it.json +8 -5
  91. package/esm/styles.css +1 -2
  92. package/package.json +1 -1
  93. package/src/components/Chat/Chat.css +37 -3
  94. package/src/components/Chat/Chat.tsx +89 -21
  95. package/src/components/Chat/__snapshots__/Chat.test.tsx.snap +672 -732
  96. package/src/components/ChatBubble/ChatBubble.css +9 -5
  97. package/src/components/ChatBubble/ChatBubble.tsx +111 -20
  98. package/src/components/ChatBubble/__snapshots__/ChatBubble.test.tsx.snap +4 -4
  99. package/src/components/ChatInputs/ChatInputs.css +293 -17
  100. package/src/components/ChatInputs/ChatInputs.tsx +144 -87
  101. package/src/components/ChatInputs/__snapshots__/ChatInputs.test.tsx.snap +430 -424
  102. package/src/components/ChatTextArea/ChatTextArea.css +75 -31
  103. package/src/components/ChatTextArea/ChatTextArea.test.tsx +1 -16
  104. package/src/components/ChatTextArea/ChatTextArea.tsx +51 -22
  105. package/src/components/ChatTextArea/__snapshots__/ChatTextArea.test.tsx.snap +9 -72
  106. package/src/components/FilePreview/FilePreview.css +225 -146
  107. package/src/components/FilePreview/FilePreview.tsx +49 -36
  108. package/src/components/FilePreview/__snapshots__/FilePreview.test.tsx.snap +2 -2
  109. package/src/components/Header/Header.css +2 -2
  110. package/src/components/MediaWidget/MediaItemWidget.tsx +2 -1
  111. package/src/components/MediaWidget/MediaWidget.css +0 -4
  112. package/src/components/MemoriWidget/MemoriWidget.css +11 -2
  113. package/src/components/MemoriWidget/MemoriWidget.tsx +61 -12
  114. package/src/components/MicrophoneButton/MicrophoneButton.css +2 -2
  115. package/src/components/StartPanel/StartPanel.css +8 -0
  116. package/src/components/UploadButton/UploadButton.css +20 -17
  117. package/src/components/UploadButton/UploadButton.stories.tsx +247 -35
  118. package/src/components/UploadButton/UploadButton.tsx +280 -175
  119. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +19 -4
  120. package/src/components/UploadButton/UploadImages/UploadImages.tsx +195 -35
  121. package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +10 -1
  122. package/src/components/layouts/chat.css +1 -1
  123. package/src/helpers/constants.ts +1 -1
  124. package/src/helpers/imageCompression.ts +230 -0
  125. package/src/locales/de.json +7 -4
  126. package/src/locales/en.json +8 -5
  127. package/src/locales/es.json +7 -4
  128. package/src/locales/fr.json +7 -4
  129. package/src/locales/it.json +8 -5
  130. package/src/styles.css +1 -2
  131. package/src/components/UploadMenu/UploadMenu.css +0 -47
  132. package/src/components/UploadMenu/UploadMenu.stories.tsx +0 -66
  133. package/src/components/UploadMenu/UploadMenu.test.tsx +0 -34
  134. package/src/components/UploadMenu/UploadMenu.tsx +0 -68
  135. 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 image count
78
- const currentImageCount = documentPreviewFiles.filter(
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 this file would exceed the limit
95
- if (currentImageCount >= maxImages) {
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
- t('upload.maxImagesReached') ??
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
- const file = files[0]; // Only handle the first file
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 (!validateImageFile(file)) {
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
- // Set file and create preview
115
- setSelectedFile(file);
116
- setFilePreview(URL.createObjectURL(file));
117
-
118
- // Set initial title as filename without extension
119
- const fileName = file.name.split('.').slice(0, -1).join('.');
120
- setImageTitle(fileName);
121
-
122
- // Show upload modal with preview
123
- setShowUploadModal(true);
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
- selectedFile.name,
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
- selectedFile.name,
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(selectedFile);
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 || currentImageCount >= maxImages
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 || currentImageCount >= maxImages
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
@@ -68,7 +68,7 @@
68
68
  height: 100%;
69
69
  max-height: 100%;
70
70
  flex: 1;
71
- padding-bottom: 3rem;
71
+ /* padding-bottom: 1.5rem; */
72
72
  transition: all 0.05s ease-in-out;
73
73
  }
74
74
 
@@ -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
- // export const MAX_TOTAL_MESSAGE_PAYLOAD = 200000; // 200KB total payload limit
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
+ };
@@ -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
- "uploadImage": "Bild hochladen",
242
- "uploadDocument": "Dokument hochladen",
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",
@@ -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
- "uploadImage": "Upload image",
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
- "uploadDocument": "Upload document",
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",