@memori.ai/memori-react 8.17.2 → 8.18.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 +30 -0
- package/README.md +1 -0
- package/dist/components/Chat/Chat.d.ts +1 -0
- package/dist/components/Chat/Chat.js +2 -2
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/ChatBubble/ChatBubble.js +2 -2
- package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
- package/dist/components/ChatInputs/ChatInputs.css +9 -3
- package/dist/components/ChatInputs/ChatInputs.d.ts +1 -0
- package/dist/components/ChatInputs/ChatInputs.js +69 -3
- package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
- package/dist/components/ChatTextArea/ChatTextArea.d.ts +1 -0
- package/dist/components/ChatTextArea/ChatTextArea.js +3 -3
- package/dist/components/ChatTextArea/ChatTextArea.js.map +1 -1
- package/dist/components/ContentPreviewModal/ContentPreviewModal.css +15 -0
- package/dist/components/ContentPreviewModal/ContentPreviewModal.d.ts +1 -0
- package/dist/components/ContentPreviewModal/ContentPreviewModal.js +1 -0
- package/dist/components/ContentPreviewModal/ContentPreviewModal.js.map +1 -1
- package/dist/components/FilePreview/FilePreview.css +17 -9
- package/dist/components/FilePreview/FilePreview.js +1 -2
- package/dist/components/FilePreview/FilePreview.js.map +1 -1
- package/dist/components/MediaWidget/MediaItemWidget.js +7 -1
- package/dist/components/MediaWidget/MediaItemWidget.js.map +1 -1
- package/dist/components/MemoriWidget/MemoriWidget.d.ts +2 -1
- package/dist/components/MemoriWidget/MemoriWidget.js +3 -2
- package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/dist/components/UploadButton/UploadButton.d.ts +1 -0
- package/dist/components/UploadButton/UploadButton.js +50 -24
- package/dist/components/UploadButton/UploadButton.js.map +1 -1
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +5 -1
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +76 -21
- package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
- package/dist/components/UploadButton/UploadImages/UploadImages.js +23 -4
- package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
- package/dist/components/layouts/fullpage.css +114 -0
- package/dist/components/layouts/website-assistant.css +17 -7
- package/dist/helpers/constants.d.ts +3 -1
- package/dist/helpers/constants.js +4 -2
- package/dist/helpers/constants.js.map +1 -1
- package/dist/helpers/tts/useTTS.js +3 -1
- package/dist/helpers/tts/useTTS.js.map +1 -1
- package/dist/helpers/utils.d.ts +1 -0
- package/dist/helpers/utils.js +16 -1
- package/dist/helpers/utils.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/locales/de.json +12 -1
- package/dist/locales/en.json +15 -1
- package/dist/locales/es.json +12 -1
- package/dist/locales/fr.json +12 -1
- package/dist/locales/it.json +15 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/esm/components/Chat/Chat.d.ts +1 -0
- package/esm/components/Chat/Chat.js +2 -2
- package/esm/components/Chat/Chat.js.map +1 -1
- package/esm/components/ChatBubble/ChatBubble.js +2 -2
- package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
- package/esm/components/ChatInputs/ChatInputs.css +9 -3
- package/esm/components/ChatInputs/ChatInputs.d.ts +1 -0
- package/esm/components/ChatInputs/ChatInputs.js +70 -4
- package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
- package/esm/components/ChatTextArea/ChatTextArea.d.ts +1 -0
- package/esm/components/ChatTextArea/ChatTextArea.js +4 -4
- package/esm/components/ChatTextArea/ChatTextArea.js.map +1 -1
- package/esm/components/ContentPreviewModal/ContentPreviewModal.css +15 -0
- package/esm/components/ContentPreviewModal/ContentPreviewModal.d.ts +1 -0
- package/esm/components/ContentPreviewModal/ContentPreviewModal.js +1 -0
- package/esm/components/ContentPreviewModal/ContentPreviewModal.js.map +1 -1
- package/esm/components/FilePreview/FilePreview.css +17 -9
- package/esm/components/FilePreview/FilePreview.js +1 -2
- package/esm/components/FilePreview/FilePreview.js.map +1 -1
- package/esm/components/MediaWidget/MediaItemWidget.js +7 -1
- package/esm/components/MediaWidget/MediaItemWidget.js.map +1 -1
- package/esm/components/MemoriWidget/MemoriWidget.d.ts +2 -1
- package/esm/components/MemoriWidget/MemoriWidget.js +3 -2
- package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/esm/components/UploadButton/UploadButton.d.ts +1 -0
- package/esm/components/UploadButton/UploadButton.js +50 -24
- package/esm/components/UploadButton/UploadButton.js.map +1 -1
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +5 -1
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +77 -22
- package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
- package/esm/components/UploadButton/UploadImages/UploadImages.js +23 -4
- package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
- package/esm/components/layouts/fullpage.css +114 -0
- package/esm/components/layouts/website-assistant.css +17 -7
- package/esm/helpers/constants.d.ts +3 -1
- package/esm/helpers/constants.js +3 -1
- package/esm/helpers/constants.js.map +1 -1
- package/esm/helpers/tts/useTTS.js +3 -1
- package/esm/helpers/tts/useTTS.js.map +1 -1
- package/esm/helpers/utils.d.ts +1 -0
- package/esm/helpers/utils.js +14 -0
- package/esm/helpers/utils.js.map +1 -1
- package/esm/index.d.ts +1 -0
- package/esm/index.js +3 -2
- package/esm/index.js.map +1 -1
- package/esm/locales/de.json +12 -1
- package/esm/locales/en.json +15 -1
- package/esm/locales/es.json +12 -1
- package/esm/locales/fr.json +12 -1
- package/esm/locales/it.json +15 -1
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/package.json +1 -1
- package/src/components/Chat/Chat.tsx +4 -0
- package/src/components/Chat/__snapshots__/Chat.test.tsx.snap +27 -81
- package/src/components/ChatBubble/ChatBubble.tsx +2 -2
- package/src/components/ChatBubble/__snapshots__/ChatBubble.test.tsx.snap +7 -21
- package/src/components/ChatInputs/ChatInputs.css +9 -3
- package/src/components/ChatInputs/ChatInputs.test.tsx +328 -1
- package/src/components/ChatInputs/ChatInputs.tsx +130 -8
- package/src/components/ChatTextArea/ChatTextArea.tsx +7 -3
- package/src/components/ContentPreviewModal/ContentPreviewModal.css +15 -0
- package/src/components/ContentPreviewModal/ContentPreviewModal.tsx +2 -0
- package/src/components/FilePreview/FilePreview.css +17 -9
- package/src/components/FilePreview/FilePreview.tsx +1 -7
- package/src/components/FilePreview/__snapshots__/FilePreview.test.tsx.snap +3 -3
- package/src/components/MediaWidget/MediaItemWidget.tsx +20 -2
- package/src/components/MemoriWidget/MemoriWidget.tsx +10 -2
- package/src/components/UploadButton/UploadButton.tsx +58 -31
- package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +98 -30
- package/src/components/UploadButton/UploadImages/UploadImages.tsx +26 -5
- package/src/components/layouts/layouts.stories.tsx +1 -31
- package/src/components/layouts/website-assistant.css +17 -7
- package/src/helpers/constants.ts +16 -4
- package/src/helpers/tts/useTTS.ts +8 -2
- package/src/helpers/utils.ts +28 -0
- package/src/index.stories.tsx +3 -2
- package/src/index.tsx +5 -0
- package/src/locales/de.json +12 -1
- package/src/locales/en.json +15 -1
- package/src/locales/es.json +12 -1
- package/src/locales/fr.json +12 -1
- package/src/locales/it.json +15 -1
- package/src/version.ts +1 -1
- package/dist/components/AttachmentLinkModal/AttachmentLinkModal.css +0 -68
- package/dist/components/AttachmentLinkModal/AttachmentLinkModal.d.ts +0 -12
- package/dist/components/AttachmentLinkModal/AttachmentLinkModal.js +0 -59
- package/dist/components/AttachmentLinkModal/AttachmentLinkModal.js.map +0 -1
- package/dist/components/MediaWidget/LinkItemWidget.css +0 -46
- package/dist/components/MediaWidget/LinkItemWidget.d.ts +0 -28
- package/dist/components/MediaWidget/LinkItemWidget.js +0 -81
- package/dist/components/MediaWidget/LinkItemWidget.js.map +0 -1
- package/esm/components/AttachmentLinkModal/AttachmentLinkModal.css +0 -68
- package/esm/components/AttachmentLinkModal/AttachmentLinkModal.d.ts +0 -12
- package/esm/components/AttachmentLinkModal/AttachmentLinkModal.js +0 -56
- package/esm/components/AttachmentLinkModal/AttachmentLinkModal.js.map +0 -1
- package/esm/components/MediaWidget/LinkItemWidget.css +0 -46
- package/esm/components/MediaWidget/LinkItemWidget.d.ts +0 -28
- package/esm/components/MediaWidget/LinkItemWidget.js +0 -76
- package/esm/components/MediaWidget/LinkItemWidget.js.map +0 -1
|
@@ -26,8 +26,6 @@ FilePreviewProps) => {
|
|
|
26
26
|
type?: string;
|
|
27
27
|
} | null>(null);
|
|
28
28
|
|
|
29
|
-
const [hoveredId, setHoveredId] = useState<string | null>(null);
|
|
30
|
-
|
|
31
29
|
const getFileType = (filename: string, type?: string) => {
|
|
32
30
|
// If type is explicitly provided, use it first
|
|
33
31
|
if (type === 'image') {
|
|
@@ -136,8 +134,6 @@ FilePreviewProps) => {
|
|
|
136
134
|
? 'memori--preview-item--image'
|
|
137
135
|
: 'memori--preview-item--document'
|
|
138
136
|
}`}
|
|
139
|
-
onMouseEnter={() => setHoveredId(file.id)}
|
|
140
|
-
onMouseLeave={() => setHoveredId(null)}
|
|
141
137
|
onClick={() => setSelectedFile(file)}
|
|
142
138
|
>
|
|
143
139
|
{isImageContent(file.content, file.type) ? (
|
|
@@ -160,9 +156,7 @@ FilePreviewProps) => {
|
|
|
160
156
|
shape="rounded"
|
|
161
157
|
icon={<CloseIcon />}
|
|
162
158
|
danger
|
|
163
|
-
className=
|
|
164
|
-
hoveredId === file.id ? 'visible' : ''
|
|
165
|
-
}`}
|
|
159
|
+
className="memori--remove-button"
|
|
166
160
|
onClick={e => {
|
|
167
161
|
e.stopPropagation();
|
|
168
162
|
removeFile(file.id, file?.mediumID);
|
|
@@ -40,7 +40,7 @@ exports[`renders FilePreview with one file 1`] = `
|
|
|
40
40
|
</span>
|
|
41
41
|
</div>
|
|
42
42
|
<button
|
|
43
|
-
class="memori-button memori-button--rounded memori-button--padded memori-button--icon-only memori-button--danger memori--remove-button
|
|
43
|
+
class="memori-button memori-button--rounded memori-button--padded memori-button--icon-only memori-button--danger memori--remove-button"
|
|
44
44
|
>
|
|
45
45
|
<span
|
|
46
46
|
class="memori-button--icon"
|
|
@@ -105,7 +105,7 @@ exports[`renders FilePreview with two files 1`] = `
|
|
|
105
105
|
</span>
|
|
106
106
|
</div>
|
|
107
107
|
<button
|
|
108
|
-
class="memori-button memori-button--rounded memori-button--padded memori-button--icon-only memori-button--danger memori--remove-button
|
|
108
|
+
class="memori-button memori-button--rounded memori-button--padded memori-button--icon-only memori-button--danger memori--remove-button"
|
|
109
109
|
>
|
|
110
110
|
<span
|
|
111
111
|
class="memori-button--icon"
|
|
@@ -157,7 +157,7 @@ exports[`renders FilePreview with two files 1`] = `
|
|
|
157
157
|
</span>
|
|
158
158
|
</div>
|
|
159
159
|
<button
|
|
160
|
-
class="memori-button memori-button--rounded memori-button--padded memori-button--icon-only memori-button--danger memori--remove-button
|
|
160
|
+
class="memori-button memori-button--rounded memori-button--padded memori-button--icon-only memori-button--danger memori--remove-button"
|
|
161
161
|
>
|
|
162
162
|
<span
|
|
163
163
|
class="memori-button--icon"
|
|
@@ -265,6 +265,22 @@ export const RenderMediaItem = memo(function RenderMediaItem({
|
|
|
265
265
|
</div>
|
|
266
266
|
);
|
|
267
267
|
|
|
268
|
+
// Plain text: snippet preview (same UX as other media / code)
|
|
269
|
+
case 'text/plain':
|
|
270
|
+
return medium.content ? (
|
|
271
|
+
<Snippet preview medium={medium} />
|
|
272
|
+
) : (
|
|
273
|
+
<DocumentCard
|
|
274
|
+
title={medium.title || 'File'}
|
|
275
|
+
badge={getFileExtensionFromMime(medium.mimeType)}
|
|
276
|
+
meta={(() => {
|
|
277
|
+
const size = getContentSize(medium);
|
|
278
|
+
return size != null && size > 0 ? formatBytes(size) : null;
|
|
279
|
+
})()}
|
|
280
|
+
icon={<File className="memori-media-item--document-icon-svg" />}
|
|
281
|
+
/>
|
|
282
|
+
);
|
|
283
|
+
|
|
268
284
|
// HTML files are now handled as file cards (rendered above in the isFile check)
|
|
269
285
|
// This case is kept for backwards compatibility but should not be reached
|
|
270
286
|
case 'text/html':
|
|
@@ -449,8 +465,10 @@ export const RenderMediaItem = memo(function RenderMediaItem({
|
|
|
449
465
|
);
|
|
450
466
|
}
|
|
451
467
|
|
|
452
|
-
// Inline previews for code snippets:
|
|
453
|
-
|
|
468
|
+
// Inline previews for code snippets and plain text: Card with preview, click opens modal (same as other media)
|
|
469
|
+
const isPreviewableText =
|
|
470
|
+
(isCodeSnippet || item.mimeType === 'text/plain') && !!item.content;
|
|
471
|
+
if (isPreviewableText && item.mediumID && _onClick) {
|
|
454
472
|
return (
|
|
455
473
|
<div
|
|
456
474
|
className="memori-media-item--link"
|
|
@@ -433,6 +433,8 @@ export interface Props {
|
|
|
433
433
|
showFunctionCache?: boolean;
|
|
434
434
|
authToken?: string;
|
|
435
435
|
__WEBCOMPONENT__?: boolean;
|
|
436
|
+
/** Override total document payload and per-document content limit (character count). Default from constants. */
|
|
437
|
+
maxTotalMessagePayload?: number;
|
|
436
438
|
}
|
|
437
439
|
|
|
438
440
|
const MemoriWidget = ({
|
|
@@ -488,6 +490,7 @@ const MemoriWidget = ({
|
|
|
488
490
|
autoStart = false,
|
|
489
491
|
applyVarsToRoot = false,
|
|
490
492
|
showFunctionCache = false,
|
|
493
|
+
maxTotalMessagePayload,
|
|
491
494
|
}: Props) => {
|
|
492
495
|
const { t, i18n } = useTranslation();
|
|
493
496
|
|
|
@@ -1887,9 +1890,13 @@ const MemoriWidget = ({
|
|
|
1887
1890
|
defaultSpeakerActive ?? integrationConfig?.defaultSpeakerActive ?? true
|
|
1888
1891
|
);
|
|
1889
1892
|
|
|
1890
|
-
// Helper function to check if audio should be played
|
|
1893
|
+
// Helper function to check if audio should be played.
|
|
1894
|
+
// When defaultEnableAudio is false, default to muted so we never play before the sync effect runs (avoids audio on first conversation start when audio is disabled).
|
|
1891
1895
|
const shouldPlayAudio = (text?: string) => {
|
|
1892
|
-
const currentSpeakerMuted = getLocalConfig(
|
|
1896
|
+
const currentSpeakerMuted = getLocalConfig(
|
|
1897
|
+
'muteSpeaker',
|
|
1898
|
+
!defaultEnableAudio
|
|
1899
|
+
);
|
|
1893
1900
|
console.log('[MemoriWidget] shouldPlayAudio', currentSpeakerMuted);
|
|
1894
1901
|
return (
|
|
1895
1902
|
text &&
|
|
@@ -3017,6 +3024,7 @@ const MemoriWidget = ({
|
|
|
3017
3024
|
userAvatar,
|
|
3018
3025
|
experts,
|
|
3019
3026
|
useMathFormatting: applyMathFormatting,
|
|
3027
|
+
maxTotalMessagePayload,
|
|
3020
3028
|
};
|
|
3021
3029
|
|
|
3022
3030
|
const integrationBackground =
|
|
@@ -10,9 +10,7 @@ import UploadImages from './UploadImages/UploadImages';
|
|
|
10
10
|
import { useTranslation } from 'react-i18next';
|
|
11
11
|
import memoriApiClient from '@memori.ai/memori-api-client';
|
|
12
12
|
import Tooltip from '../ui/Tooltip';
|
|
13
|
-
|
|
14
|
-
// Constants
|
|
15
|
-
const MAX_MEDIA = 10;
|
|
13
|
+
import { MAX_DOCUMENTS_PER_MESSAGE } from '../../helpers/constants';
|
|
16
14
|
|
|
17
15
|
// Props interface
|
|
18
16
|
interface UploadManagerProps {
|
|
@@ -29,6 +27,8 @@ interface UploadManagerProps {
|
|
|
29
27
|
type?: string;
|
|
30
28
|
}[];
|
|
31
29
|
memoriID?: string;
|
|
30
|
+
/** Override total document payload limit (character count). */
|
|
31
|
+
maxTotalMessagePayload?: number;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const UploadButton: React.FC<UploadManagerProps> = ({
|
|
@@ -39,6 +39,7 @@ const UploadButton: React.FC<UploadManagerProps> = ({
|
|
|
39
39
|
setDocumentPreviewFiles,
|
|
40
40
|
documentPreviewFiles,
|
|
41
41
|
memoriID = '',
|
|
42
|
+
maxTotalMessagePayload,
|
|
42
43
|
}) => {
|
|
43
44
|
// State
|
|
44
45
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -57,7 +58,7 @@ const UploadButton: React.FC<UploadManagerProps> = ({
|
|
|
57
58
|
|
|
58
59
|
// Calculate total media count
|
|
59
60
|
const currentMediaCount = documentPreviewFiles.length;
|
|
60
|
-
const remainingSlots =
|
|
61
|
+
const remainingSlots = MAX_DOCUMENTS_PER_MESSAGE - currentMediaCount;
|
|
61
62
|
const hasReachedMediaLimit = remainingSlots <= 0;
|
|
62
63
|
|
|
63
64
|
// Error handling
|
|
@@ -104,15 +105,12 @@ const UploadButton: React.FC<UploadManagerProps> = ({
|
|
|
104
105
|
const fileArray = Array.from(files);
|
|
105
106
|
if (fileArray.length === 0) return;
|
|
106
107
|
|
|
107
|
-
const
|
|
108
|
-
const documentFiles: File[] = [];
|
|
109
|
-
|
|
110
|
-
// Separate files by type
|
|
108
|
+
const supportedFiles: File[] = [];
|
|
111
109
|
fileArray.forEach(file => {
|
|
112
110
|
if (isImageFile(file)) {
|
|
113
|
-
|
|
111
|
+
supportedFiles.push(file);
|
|
114
112
|
} else if (isDocumentFile(file)) {
|
|
115
|
-
|
|
113
|
+
supportedFiles.push(file);
|
|
116
114
|
} else {
|
|
117
115
|
addErrorRef.current({
|
|
118
116
|
message: `File "${file.name}" is not a supported image or document type`,
|
|
@@ -121,25 +119,44 @@ const UploadButton: React.FC<UploadManagerProps> = ({
|
|
|
121
119
|
}
|
|
122
120
|
});
|
|
123
121
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const newTotalCount = currentMediaCountRef.current + totalFilesToAdd;
|
|
122
|
+
const totalSupported = supportedFiles.length;
|
|
123
|
+
if (totalSupported === 0) return;
|
|
127
124
|
|
|
128
|
-
|
|
129
|
-
if (
|
|
125
|
+
const remainingSlots = MAX_DOCUMENTS_PER_MESSAGE - currentMediaCountRef.current;
|
|
126
|
+
if (remainingSlots <= 0) {
|
|
130
127
|
addErrorRef.current({
|
|
131
|
-
message: `Maximum ${
|
|
128
|
+
message: `Maximum ${MAX_DOCUMENTS_PER_MESSAGE} media files allowed.`,
|
|
132
129
|
severity: 'error',
|
|
133
130
|
});
|
|
134
131
|
return;
|
|
135
132
|
}
|
|
136
133
|
|
|
134
|
+
const toProcess = supportedFiles.slice(0, remainingSlots);
|
|
135
|
+
const imageFiles = toProcess.filter(f => isImageFile(f));
|
|
136
|
+
const documentFiles = toProcess.filter(f => isDocumentFile(f));
|
|
137
|
+
|
|
138
|
+
if (totalSupported > remainingSlots) {
|
|
139
|
+
const skipped = totalSupported - remainingSlots;
|
|
140
|
+
addErrorRef.current({
|
|
141
|
+
message:
|
|
142
|
+
t('upload.filesNotAddedMaxAllowed', {
|
|
143
|
+
count: skipped,
|
|
144
|
+
max: MAX_DOCUMENTS_PER_MESSAGE,
|
|
145
|
+
defaultValue: `${skipped} file(s) not added (maximum ${MAX_DOCUMENTS_PER_MESSAGE} files allowed).`,
|
|
146
|
+
}) ?? `${skipped} file(s) not added (maximum ${MAX_DOCUMENTS_PER_MESSAGE} files allowed).`,
|
|
147
|
+
severity: 'info',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
137
151
|
// Process images
|
|
138
152
|
if (imageFiles.length > 0) {
|
|
139
153
|
if (!isMediaAcceptedRef.current) {
|
|
140
154
|
addErrorRef.current({
|
|
141
155
|
message:
|
|
142
|
-
t('upload.
|
|
156
|
+
t('upload.imagesNotAddedMediaNotAccepted', {
|
|
157
|
+
count: imageFiles.length,
|
|
158
|
+
defaultValue: `${imageFiles.length} image(s) not added (media uploads not accepted).`,
|
|
159
|
+
}) ?? `${imageFiles.length} image(s) not added (media uploads not accepted).`,
|
|
143
160
|
severity: 'info',
|
|
144
161
|
});
|
|
145
162
|
} else {
|
|
@@ -157,7 +174,11 @@ const UploadButton: React.FC<UploadManagerProps> = ({
|
|
|
157
174
|
|
|
158
175
|
// Only proceed if we successfully added files
|
|
159
176
|
if (dataTransfer.files.length > 0) {
|
|
160
|
-
|
|
177
|
+
try {
|
|
178
|
+
imageInput.files = dataTransfer.files;
|
|
179
|
+
} catch {
|
|
180
|
+
// JSDOM and some environments do not allow assigning to input.files
|
|
181
|
+
}
|
|
161
182
|
const changeEvent = new Event('change', { bubbles: true });
|
|
162
183
|
imageInput.dispatchEvent(changeEvent);
|
|
163
184
|
}
|
|
@@ -181,7 +202,11 @@ const UploadButton: React.FC<UploadManagerProps> = ({
|
|
|
181
202
|
|
|
182
203
|
// Only proceed if we successfully added files
|
|
183
204
|
if (dataTransfer.files.length > 0) {
|
|
184
|
-
|
|
205
|
+
try {
|
|
206
|
+
documentInput.files = dataTransfer.files;
|
|
207
|
+
} catch {
|
|
208
|
+
// JSDOM and some environments do not allow assigning to input.files
|
|
209
|
+
}
|
|
185
210
|
const changeEvent = new Event('change', { bubbles: true });
|
|
186
211
|
documentInput.dispatchEvent(changeEvent);
|
|
187
212
|
}
|
|
@@ -410,7 +435,7 @@ ${file.content}
|
|
|
410
435
|
return true;
|
|
411
436
|
};
|
|
412
437
|
|
|
413
|
-
// Validate total payload size
|
|
438
|
+
// Validate total payload size. Returns result so caller can avoid showing this error when truncation was already shown.
|
|
414
439
|
const validatePayloadSize = (
|
|
415
440
|
newDocuments: {
|
|
416
441
|
name: string;
|
|
@@ -418,8 +443,9 @@ ${file.content}
|
|
|
418
443
|
content: string;
|
|
419
444
|
mimeType: string;
|
|
420
445
|
}[]
|
|
421
|
-
): boolean => {
|
|
446
|
+
): { valid: boolean; message?: string } => {
|
|
422
447
|
const { MAX_TOTAL_MESSAGE_PAYLOAD } = require('../../helpers/constants');
|
|
448
|
+
const limit = maxTotalMessagePayload ?? MAX_TOTAL_MESSAGE_PAYLOAD;
|
|
423
449
|
|
|
424
450
|
const existingDocuments = documentPreviewFiles.filter(
|
|
425
451
|
(file: any) => file.type === 'document'
|
|
@@ -431,15 +457,15 @@ ${file.content}
|
|
|
431
457
|
0
|
|
432
458
|
);
|
|
433
459
|
|
|
434
|
-
if (totalPayloadSize >
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
460
|
+
if (totalPayloadSize > limit) {
|
|
461
|
+
const msg = t('upload.contextSizeExceedsLimit', {
|
|
462
|
+
defaultValue:
|
|
463
|
+
'Context size exceeds the limit. Try reducing the number of files or content in the conversation.',
|
|
438
464
|
});
|
|
439
|
-
return false;
|
|
465
|
+
return { valid: false, message: typeof msg === 'string' ? msg : String(msg) };
|
|
440
466
|
}
|
|
441
467
|
|
|
442
|
-
return true;
|
|
468
|
+
return { valid: true };
|
|
443
469
|
};
|
|
444
470
|
|
|
445
471
|
// Handle document upload errors
|
|
@@ -544,7 +570,7 @@ ${file.content}
|
|
|
544
570
|
'memori--document-count-full': hasReachedMediaLimit,
|
|
545
571
|
})}
|
|
546
572
|
>
|
|
547
|
-
{currentMediaCount}/{
|
|
573
|
+
{currentMediaCount}/{MAX_DOCUMENTS_PER_MESSAGE}
|
|
548
574
|
</div>
|
|
549
575
|
)}
|
|
550
576
|
|
|
@@ -552,12 +578,13 @@ ${file.content}
|
|
|
552
578
|
<div className="memori--hidden-uploader" ref={documentRef}>
|
|
553
579
|
<UploadDocuments
|
|
554
580
|
setDocumentPreviewFiles={handleDocumentFiles}
|
|
555
|
-
maxDocuments={
|
|
581
|
+
maxDocuments={MAX_DOCUMENTS_PER_MESSAGE}
|
|
556
582
|
documentPreviewFiles={documentPreviewFiles}
|
|
557
583
|
onLoadingChange={handleLoadingChange}
|
|
558
584
|
onDocumentError={handleDocumentError}
|
|
559
585
|
onValidateFile={validateDocumentFile}
|
|
560
586
|
onValidatePayloadSize={validatePayloadSize}
|
|
587
|
+
maxTotalMessagePayload={maxTotalMessagePayload}
|
|
561
588
|
/>
|
|
562
589
|
</div>
|
|
563
590
|
|
|
@@ -570,7 +597,7 @@ ${file.content}
|
|
|
570
597
|
documentPreviewFiles={documentPreviewFiles}
|
|
571
598
|
isMediaAccepted={isMediaAccepted}
|
|
572
599
|
onLoadingChange={handleLoadingChange}
|
|
573
|
-
maxImages={
|
|
600
|
+
maxImages={MAX_DOCUMENTS_PER_MESSAGE}
|
|
574
601
|
memoriID={memoriID}
|
|
575
602
|
onImageError={handleImageError}
|
|
576
603
|
onValidateImageFile={validateImageFile}
|
|
@@ -585,7 +612,7 @@ ${file.content}
|
|
|
585
612
|
key={`${error.message}-${index}`}
|
|
586
613
|
open={true}
|
|
587
614
|
type={error.severity}
|
|
588
|
-
title={'Upload notification'}
|
|
615
|
+
title={t('upload.uploadNotification', { defaultValue: 'Upload notification' })}
|
|
589
616
|
description={error.message}
|
|
590
617
|
onClose={() => removeError(error.message)}
|
|
591
618
|
width="350px"
|
|
@@ -3,7 +3,11 @@ import cx from 'classnames';
|
|
|
3
3
|
import Spin from '../../ui/Spin';
|
|
4
4
|
import { DocumentIcon } from '../../icons/Document';
|
|
5
5
|
import Modal from '../../ui/Modal';
|
|
6
|
-
import {
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import {
|
|
8
|
+
MAX_DOCUMENT_CONTENT_LENGTH,
|
|
9
|
+
MAX_TOTAL_MESSAGE_PAYLOAD,
|
|
10
|
+
} from '../../../helpers/constants';
|
|
7
11
|
|
|
8
12
|
// Types
|
|
9
13
|
type PreviewFile = {
|
|
@@ -51,7 +55,9 @@ interface UploadDocumentsProps {
|
|
|
51
55
|
content: string;
|
|
52
56
|
mimeType: string;
|
|
53
57
|
}[]
|
|
54
|
-
) => boolean;
|
|
58
|
+
) => boolean | { valid: boolean; message?: string };
|
|
59
|
+
/** Same as total payload: overrides per-document content limit (character count). */
|
|
60
|
+
maxTotalMessagePayload?: number;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
@@ -62,7 +68,10 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
62
68
|
onDocumentError,
|
|
63
69
|
onValidateFile,
|
|
64
70
|
onValidatePayloadSize,
|
|
71
|
+
maxTotalMessagePayload,
|
|
65
72
|
}) => {
|
|
73
|
+
const { t } = useTranslation();
|
|
74
|
+
|
|
66
75
|
// State
|
|
67
76
|
const [isLoading, setIsLoading] = useState(false);
|
|
68
77
|
const [selectedFile, setSelectedFile] = useState<PreviewFile | null>(null);
|
|
@@ -85,7 +94,7 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
85
94
|
return true;
|
|
86
95
|
};
|
|
87
96
|
|
|
88
|
-
// Validate total payload size
|
|
97
|
+
// Validate total payload size (returns result object for conditional error display)
|
|
89
98
|
const validatePayloadSize = (
|
|
90
99
|
newDocuments: {
|
|
91
100
|
name: string;
|
|
@@ -93,11 +102,15 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
93
102
|
content: string;
|
|
94
103
|
mimeType: string;
|
|
95
104
|
}[]
|
|
96
|
-
): boolean => {
|
|
105
|
+
): { valid: boolean; message?: string } => {
|
|
97
106
|
if (onValidatePayloadSize) {
|
|
98
|
-
|
|
107
|
+
const result = onValidatePayloadSize(newDocuments);
|
|
108
|
+
if (typeof result === 'boolean') {
|
|
109
|
+
return result ? { valid: true } : { valid: false, message: '' };
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
99
112
|
}
|
|
100
|
-
return true;
|
|
113
|
+
return { valid: true };
|
|
101
114
|
};
|
|
102
115
|
|
|
103
116
|
const extractTextFromPDF = async (file: File): Promise<string> => {
|
|
@@ -212,7 +225,9 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
212
225
|
}
|
|
213
226
|
};
|
|
214
227
|
|
|
215
|
-
const processDocumentFile = async (
|
|
228
|
+
const processDocumentFile = async (
|
|
229
|
+
file: File
|
|
230
|
+
): Promise<{ text: string | null; wasTruncated: boolean }> => {
|
|
216
231
|
const fileExt = file.name.split('.').pop()?.toLowerCase() || '';
|
|
217
232
|
|
|
218
233
|
try {
|
|
@@ -226,22 +241,28 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
226
241
|
text = await extractTextFromXLSX(file);
|
|
227
242
|
}
|
|
228
243
|
|
|
229
|
-
|
|
244
|
+
const perDocumentLimit = maxTotalMessagePayload ?? MAX_DOCUMENT_CONTENT_LENGTH;
|
|
245
|
+
let wasTruncated = false;
|
|
246
|
+
if (text && text.length > perDocumentLimit) {
|
|
230
247
|
console.warn(
|
|
231
248
|
'Document content exceeds length limit:',
|
|
232
249
|
text.length,
|
|
233
250
|
'>',
|
|
234
|
-
|
|
251
|
+
perDocumentLimit
|
|
235
252
|
);
|
|
236
253
|
onDocumentError?.({
|
|
237
|
-
message:
|
|
238
|
-
|
|
254
|
+
message: t('upload.contextSizeExceedsLimit', {
|
|
255
|
+
defaultValue:
|
|
256
|
+
'Context size exceeds the limit. Try reducing the number of files or content in the conversation.',
|
|
257
|
+
}),
|
|
258
|
+
severity: 'error',
|
|
239
259
|
});
|
|
260
|
+
wasTruncated = true;
|
|
240
261
|
text =
|
|
241
|
-
text.substring(0,
|
|
262
|
+
text.substring(0, perDocumentLimit) +
|
|
242
263
|
'\n\n[Content truncated due to size limits]';
|
|
243
264
|
}
|
|
244
|
-
return text;
|
|
265
|
+
return { text, wasTruncated };
|
|
245
266
|
} catch (error) {
|
|
246
267
|
console.error('Document processing failed:', error);
|
|
247
268
|
throw new Error(
|
|
@@ -260,13 +281,25 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
260
281
|
|
|
261
282
|
// Check current total media count (images + documents)
|
|
262
283
|
const currentMediaCount = documentPreviewFiles.length;
|
|
284
|
+
const remainingSlots = maxDocuments
|
|
285
|
+
? Math.max(0, maxDocuments - currentMediaCount)
|
|
286
|
+
: files.length;
|
|
287
|
+
const filesToProcess = files.slice(0, remainingSlots);
|
|
263
288
|
|
|
264
|
-
|
|
265
|
-
|
|
289
|
+
if (files.length > filesToProcess.length) {
|
|
290
|
+
const skipped = files.length - filesToProcess.length;
|
|
266
291
|
onDocumentError?.({
|
|
267
|
-
message:
|
|
268
|
-
|
|
292
|
+
message:
|
|
293
|
+
t('upload.documentsNotAddedMaxAllowed', {
|
|
294
|
+
count: skipped,
|
|
295
|
+
max: maxDocuments ?? 10,
|
|
296
|
+
defaultValue: `${skipped} document(s) not added (maximum ${maxDocuments ?? 10} files allowed).`,
|
|
297
|
+
}) ?? `${skipped} document(s) not added (maximum ${maxDocuments ?? 10} files allowed).`,
|
|
298
|
+
severity: 'info',
|
|
269
299
|
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (filesToProcess.length === 0) {
|
|
270
303
|
if (documentInputRef.current) {
|
|
271
304
|
documentInputRef.current.value = '';
|
|
272
305
|
}
|
|
@@ -282,8 +315,15 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
282
315
|
content: string;
|
|
283
316
|
mimeType: string;
|
|
284
317
|
}[] = [];
|
|
285
|
-
|
|
286
|
-
|
|
318
|
+
let hadTruncation = false;
|
|
319
|
+
let skippedDueToPayload = 0;
|
|
320
|
+
const payloadLimit =
|
|
321
|
+
maxTotalMessagePayload ?? MAX_TOTAL_MESSAGE_PAYLOAD;
|
|
322
|
+
const existingTotal = documentPreviewFiles
|
|
323
|
+
.filter((f: any) => f.type === 'document')
|
|
324
|
+
.reduce((sum: number, f: any) => sum + (f.content?.length ?? 0), 0);
|
|
325
|
+
|
|
326
|
+
for (const file of filesToProcess) {
|
|
287
327
|
if (!validateDocumentFile(file)) {
|
|
288
328
|
continue;
|
|
289
329
|
}
|
|
@@ -291,15 +331,32 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
291
331
|
const fileId = Math.random().toString(36).substr(2, 9);
|
|
292
332
|
|
|
293
333
|
try {
|
|
294
|
-
const text = await processDocumentFile(file);
|
|
334
|
+
const { text, wasTruncated } = await processDocumentFile(file);
|
|
335
|
+
if (wasTruncated) hadTruncation = true;
|
|
295
336
|
|
|
296
337
|
if (text) {
|
|
338
|
+
const processedSum = processedFiles.reduce(
|
|
339
|
+
(s, d) => s + d.content.length,
|
|
340
|
+
0
|
|
341
|
+
);
|
|
342
|
+
if (existingTotal + processedSum + text.length > payloadLimit) {
|
|
343
|
+
skippedDueToPayload += 1;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
297
346
|
processedFiles.push({
|
|
298
347
|
name: file.name,
|
|
299
348
|
id: fileId,
|
|
300
349
|
content: text,
|
|
301
350
|
mimeType: file.type,
|
|
302
351
|
});
|
|
352
|
+
} else {
|
|
353
|
+
onDocumentError?.({
|
|
354
|
+
message: t('upload.documentNotReadable', {
|
|
355
|
+
name: file.name,
|
|
356
|
+
defaultValue: `Document "${file.name}" could not be read or was empty.`,
|
|
357
|
+
}),
|
|
358
|
+
severity: 'info',
|
|
359
|
+
});
|
|
303
360
|
}
|
|
304
361
|
} catch (error) {
|
|
305
362
|
console.error('File processing error:', error);
|
|
@@ -312,17 +369,28 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
|
|
|
312
369
|
}
|
|
313
370
|
}
|
|
314
371
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
372
|
+
if (skippedDueToPayload > 0) {
|
|
373
|
+
onDocumentError?.({
|
|
374
|
+
message: t('upload.documentsNotAddedContextSize', {
|
|
375
|
+
count: skippedDueToPayload,
|
|
376
|
+
defaultValue: `${skippedDueToPayload} document(s) not added (context size limit).`,
|
|
377
|
+
}),
|
|
378
|
+
severity: 'info',
|
|
379
|
+
});
|
|
380
|
+
}
|
|
325
381
|
|
|
382
|
+
if (processedFiles.length === 0 && filesToProcess.length > 0) {
|
|
383
|
+
onDocumentError?.({
|
|
384
|
+
message: t('upload.noDocumentsAdded', {
|
|
385
|
+
defaultValue:
|
|
386
|
+
'No documents could be added. Check file type, size (max 10MB), and that the file is readable.',
|
|
387
|
+
}),
|
|
388
|
+
severity: 'info',
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Add new documents to existing ones (only those that fit within payload)
|
|
393
|
+
if (processedFiles.length > 0) {
|
|
326
394
|
const existingDocuments = documentPreviewFiles.filter(
|
|
327
395
|
(file: any) => file.type === 'document'
|
|
328
396
|
);
|
|
@@ -90,18 +90,31 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
90
90
|
const files = Array.from(e.target.files || []);
|
|
91
91
|
if (files.length === 0) return;
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const remainingSlots = Math.max(0, maxImages - currentMediaCount);
|
|
94
|
+
const filesToProcess = files.slice(0, remainingSlots);
|
|
95
|
+
|
|
96
|
+
if (files.length > filesToProcess.length) {
|
|
97
|
+
const skipped = files.length - filesToProcess.length;
|
|
95
98
|
onImageError?.({
|
|
96
99
|
message:
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
t('upload.imagesNotAddedMaxAllowed', {
|
|
101
|
+
count: skipped,
|
|
102
|
+
max: maxImages,
|
|
103
|
+
defaultValue: `${skipped} image(s) not added (maximum ${maxImages} files allowed).`,
|
|
104
|
+
}) ?? `${skipped} image(s) not added (maximum ${maxImages} files allowed).`,
|
|
105
|
+
severity: 'info',
|
|
99
106
|
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (filesToProcess.length === 0) {
|
|
110
|
+
if (imageInputRef.current) {
|
|
111
|
+
imageInputRef.current.value = '';
|
|
112
|
+
}
|
|
100
113
|
return;
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
// Validate all files first
|
|
104
|
-
const validFiles =
|
|
117
|
+
const validFiles = filesToProcess.filter(file => {
|
|
105
118
|
if (!validateImageFile(file)) {
|
|
106
119
|
return false;
|
|
107
120
|
}
|
|
@@ -109,6 +122,14 @@ const UploadImages: React.FC<UploadImagesProps> = ({
|
|
|
109
122
|
});
|
|
110
123
|
|
|
111
124
|
if (validFiles.length === 0) {
|
|
125
|
+
onImageError?.({
|
|
126
|
+
message:
|
|
127
|
+
t('upload.noImagesAdded', {
|
|
128
|
+
defaultValue:
|
|
129
|
+
'No images could be added. Check file type (.jpg, .jpeg, .png) and size (max 10MB).',
|
|
130
|
+
}) ?? 'No images could be added. Check file type (.jpg, .jpeg, .png) and size (max 10MB).',
|
|
131
|
+
severity: 'info',
|
|
132
|
+
});
|
|
112
133
|
if (imageInputRef.current) {
|
|
113
134
|
imageInputRef.current.value = '';
|
|
114
135
|
}
|
|
@@ -49,40 +49,10 @@ DefaultLayout.args = {
|
|
|
49
49
|
sessionID: '' as string | undefined,
|
|
50
50
|
showUpload: true,
|
|
51
51
|
showReasoning: false,
|
|
52
|
-
showLogin: true
|
|
52
|
+
showLogin: true,
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
const EnglishLayout = Template.bind({});
|
|
57
|
-
EnglishLayout.args = {
|
|
58
|
-
memoriName: 'Prova34',
|
|
59
|
-
ownerUserName: 'andrea.patini',
|
|
60
|
-
memoriID: 'a37fd990-e159-421e-888d-38a1bb5df294',
|
|
61
|
-
tenantID: 'aisuru-staging.aclambda.online',
|
|
62
|
-
ownerUserID: '91dbc9ba-b684-4fbe-9828-b5980af6cda9',
|
|
63
|
-
engineURL: 'https://engine-staging.memori.ai/memori/v2',
|
|
64
|
-
apiURL: 'https://backend-staging.memori.ai/api/v2',
|
|
65
|
-
layout: 'FULLPAGE',
|
|
66
|
-
uiLang: 'EN',
|
|
67
|
-
spokenLang: 'EN',
|
|
68
|
-
integrationID: '658fe9ac-136d-401f-bc57-f5af4d8d7012',
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export const EnglishLayoutStory = Template.bind({});
|
|
72
|
-
EnglishLayoutStory.args = {
|
|
73
|
-
memoriName: 'Prova34',
|
|
74
|
-
ownerUserName: 'andrea.patini',
|
|
75
|
-
memoriID: 'a37fd990-e159-421e-888d-38a1bb5df294',
|
|
76
|
-
tenantID: 'aisuru-staging.aclambda.online',
|
|
77
|
-
ownerUserID: '91dbc9ba-b684-4fbe-9828-b5980af6cda9',
|
|
78
|
-
engineURL: 'https://engine-staging.memori.ai/memori/v2',
|
|
79
|
-
apiURL: 'https://backend-staging.memori.ai/api/v2',
|
|
80
|
-
layout: 'FULLPAGE',
|
|
81
|
-
uiLang: 'EN',
|
|
82
|
-
spokenLang: 'EN',
|
|
83
|
-
integrationID: '658fe9ac-136d-401f-bc57-f5af4d8d7012',
|
|
84
|
-
};
|
|
85
|
-
|
|
86
56
|
|
|
87
57
|
export const Default = Template.bind({});
|
|
88
58
|
Default.args = {
|