@memori.ai/memori-react 8.18.1 → 8.19.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 (82) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +2 -0
  3. package/dist/components/Chat/Chat.d.ts +2 -0
  4. package/dist/components/Chat/Chat.js +2 -2
  5. package/dist/components/Chat/Chat.js.map +1 -1
  6. package/dist/components/ChatInputs/ChatInputs.d.ts +2 -0
  7. package/dist/components/ChatInputs/ChatInputs.js +12 -25
  8. package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
  9. package/dist/components/ChatTextArea/ChatTextArea.css +19 -1
  10. package/dist/components/ChatTextArea/ChatTextArea.d.ts +1 -0
  11. package/dist/components/ChatTextArea/ChatTextArea.js +46 -27
  12. package/dist/components/ChatTextArea/ChatTextArea.js.map +1 -1
  13. package/dist/components/FilePreview/FilePreview.css +0 -1
  14. package/dist/components/MemoriWidget/MemoriWidget.d.ts +3 -1
  15. package/dist/components/MemoriWidget/MemoriWidget.js +3 -1
  16. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  17. package/dist/components/UploadButton/UploadButton.js +10 -16
  18. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  19. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +3 -26
  20. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  21. package/dist/components/UploadButton/UploadImages/UploadImages.js +8 -14
  22. package/dist/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +4 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/locales/de.json +1 -5
  27. package/dist/locales/en.json +2 -5
  28. package/dist/locales/es.json +1 -5
  29. package/dist/locales/fr.json +1 -5
  30. package/dist/locales/it.json +2 -6
  31. package/dist/version.d.ts +1 -1
  32. package/dist/version.js +1 -1
  33. package/esm/components/Chat/Chat.d.ts +2 -0
  34. package/esm/components/Chat/Chat.js +2 -2
  35. package/esm/components/Chat/Chat.js.map +1 -1
  36. package/esm/components/ChatInputs/ChatInputs.d.ts +2 -0
  37. package/esm/components/ChatInputs/ChatInputs.js +12 -25
  38. package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
  39. package/esm/components/ChatTextArea/ChatTextArea.css +19 -1
  40. package/esm/components/ChatTextArea/ChatTextArea.d.ts +1 -0
  41. package/esm/components/ChatTextArea/ChatTextArea.js +47 -28
  42. package/esm/components/ChatTextArea/ChatTextArea.js.map +1 -1
  43. package/esm/components/FilePreview/FilePreview.css +0 -1
  44. package/esm/components/MemoriWidget/MemoriWidget.d.ts +3 -1
  45. package/esm/components/MemoriWidget/MemoriWidget.js +3 -1
  46. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  47. package/esm/components/UploadButton/UploadButton.js +10 -16
  48. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  49. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +3 -26
  50. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  51. package/esm/components/UploadButton/UploadImages/UploadImages.js +8 -14
  52. package/esm/components/UploadButton/UploadImages/UploadImages.js.map +1 -1
  53. package/esm/index.d.ts +2 -0
  54. package/esm/index.js +4 -2
  55. package/esm/index.js.map +1 -1
  56. package/esm/locales/de.json +1 -5
  57. package/esm/locales/en.json +2 -5
  58. package/esm/locales/es.json +1 -5
  59. package/esm/locales/fr.json +1 -5
  60. package/esm/locales/it.json +2 -6
  61. package/esm/version.d.ts +1 -1
  62. package/esm/version.js +1 -1
  63. package/package.json +1 -1
  64. package/src/components/Chat/Chat.tsx +8 -0
  65. package/src/components/ChatInputs/ChatInputs.test.tsx +4 -4
  66. package/src/components/ChatInputs/ChatInputs.tsx +19 -41
  67. package/src/components/ChatTextArea/ChatTextArea.css +19 -1
  68. package/src/components/ChatTextArea/ChatTextArea.tsx +38 -7
  69. package/src/components/FilePreview/FilePreview.css +0 -1
  70. package/src/components/MemoriWidget/MemoriWidget.tsx +8 -0
  71. package/src/components/UploadButton/UploadButton.tsx +10 -17
  72. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +3 -28
  73. package/src/components/UploadButton/UploadImages/UploadImages.tsx +7 -15
  74. package/src/components/layouts/layouts.stories.tsx +1 -0
  75. package/src/index.stories.tsx +15 -0
  76. package/src/index.tsx +11 -1
  77. package/src/locales/de.json +1 -5
  78. package/src/locales/en.json +2 -5
  79. package/src/locales/es.json +1 -5
  80. package/src/locales/fr.json +1 -5
  81. package/src/locales/it.json +2 -6
  82. package/src/version.ts +1 -1
package/esm/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const version = '8.18.1';
1
+ export const version = '8.19.0';
2
2
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "8.18.1",
2
+ "version": "8.19.0",
3
3
  "name": "@memori.ai/memori-react",
4
4
  "author": "Memori Srl",
5
5
  "main": "dist/index.js",
@@ -76,6 +76,10 @@ export interface Props {
76
76
  showFunctionCache?: boolean;
77
77
  /** Override total document payload and per-document content limit (character count). */
78
78
  maxTotalMessagePayload?: number;
79
+ /** When true, pasted text is not added as a document attachment. Default false. */
80
+ disablePastedText?: boolean;
81
+ /** Max characters in chat textarea; shows counter when set. */
82
+ maxTextareaCharacters?: number;
79
83
  }
80
84
 
81
85
  const Chat: React.FC<Props> = ({
@@ -128,6 +132,8 @@ const Chat: React.FC<Props> = ({
128
132
  isChatlogPanel = false,
129
133
  showFunctionCache = false,
130
134
  maxTotalMessagePayload,
135
+ disablePastedText = false,
136
+ maxTextareaCharacters,
131
137
  }) => {
132
138
  const [isTextareaExpanded, setIsTextareaExpanded] = useState(false);
133
139
  const [isDragging, setIsDragging] = useState(false);
@@ -546,6 +552,8 @@ const Chat: React.FC<Props> = ({
546
552
  showMicrophone={showMicrophone}
547
553
  memoriID={memori?.memoriID}
548
554
  maxTotalMessagePayload={maxTotalMessagePayload}
555
+ disablePastedText={disablePastedText}
556
+ maxTextareaCharacters={maxTextareaCharacters}
549
557
  />
550
558
  )}
551
559
  </div>
@@ -9,10 +9,10 @@ import {
9
9
 
10
10
  jest.mock('react-hot-toast', () => ({
11
11
  __esModule: true,
12
- default: {
12
+ default: Object.assign(jest.fn(), {
13
13
  success: jest.fn(),
14
14
  error: jest.fn(),
15
- },
15
+ }),
16
16
  }));
17
17
 
18
18
  // jsdom does not define DataTransfer; UploadButton's paste handler uses it when clipboard has files
@@ -312,7 +312,7 @@ describe('paste as card (long text becomes attachment)', () => {
312
312
  );
313
313
  });
314
314
 
315
- it('calls toast.error when pasted content exceeds size limit', async () => {
315
+ it('shows warning toast and prevents paste when content exceeds size limit', async () => {
316
316
  const { MAX_DOCUMENT_CONTENT_LENGTH } = require('../../helpers/constants');
317
317
  const tooLongText = 'x'.repeat(MAX_DOCUMENT_CONTENT_LENGTH + 1);
318
318
  render(
@@ -328,7 +328,7 @@ describe('paste as card (long text becomes attachment)', () => {
328
328
 
329
329
  await waitFor(() => {
330
330
  const toast = require('react-hot-toast').default;
331
- expect(toast.error).toHaveBeenCalled();
331
+ expect(toast).toHaveBeenCalled();
332
332
  });
333
333
  expect(screen.queryByText('upload.pastedText')).toBeNull();
334
334
  });
@@ -45,6 +45,10 @@ export interface Props {
45
45
  onTextareaExpanded?: (expanded: boolean) => void;
46
46
  /** Override total document payload limit (character count). */
47
47
  maxTotalMessagePayload?: number;
48
+ /** When true, pasted text is not added as a document attachment (normal paste only). Default false. */
49
+ disablePastedText?: boolean;
50
+ /** Max characters in textarea; shows counter (e.g. "0 / 500") when set. */
51
+ maxTextareaCharacters?: number;
48
52
  }
49
53
 
50
54
  const ChatInputs: React.FC<Props> = ({
@@ -69,6 +73,8 @@ const ChatInputs: React.FC<Props> = ({
69
73
  client,
70
74
  onTextareaExpanded,
71
75
  maxTotalMessagePayload,
76
+ disablePastedText = false,
77
+ maxTextareaCharacters,
72
78
  }) => {
73
79
  const { t } = useTranslation();
74
80
 
@@ -93,8 +99,6 @@ const ChatInputs: React.FC<Props> = ({
93
99
  dialog: { postMediumDeselectedEvent: null },
94
100
  };
95
101
 
96
- const totalPayloadLimit = maxTotalMessagePayload ?? MAX_TOTAL_MESSAGE_PAYLOAD;
97
-
98
102
  /**
99
103
  * Handles sending a message, including any attached files
100
104
  */
@@ -111,17 +115,6 @@ const ChatInputs: React.FC<Props> = ({
111
115
  ) => {
112
116
  if (isTyping) return;
113
117
 
114
- const totalContentLength = files.reduce((sum, f) => sum + f.content.length, 0);
115
- if (totalContentLength > totalPayloadLimit) {
116
- toast.error(
117
- t('upload.contextSizeExceedsLimit', {
118
- defaultValue:
119
- 'Context size exceeds the limit. Try reducing the number of files or content in the conversation.',
120
- })
121
- );
122
- return;
123
- }
124
-
125
118
  const mediaWithIds = files.map((file, index) => {
126
119
  const generatedMediumID =
127
120
  file.mediumID ||
@@ -164,19 +157,6 @@ const ChatInputs: React.FC<Props> = ({
164
157
 
165
158
  if (sendOnEnter === 'keypress' && userMessage?.length > 0) {
166
159
  stopListening();
167
- const totalContentLength = documentPreviewFiles.reduce(
168
- (sum, f) => sum + f.content.length,
169
- 0
170
- );
171
- if (totalContentLength > totalPayloadLimit) {
172
- toast.error(
173
- t('upload.contextSizeExceedsLimit', {
174
- defaultValue:
175
- 'Context size exceeds the limit. Try reducing the number of files or content in the conversation.',
176
- })
177
- );
178
- return;
179
- }
180
160
  const mediaWithIds = documentPreviewFiles.map((file, index) => {
181
161
  const generatedMediumID =
182
162
  file.mediumID ||
@@ -240,6 +220,7 @@ const ChatInputs: React.FC<Props> = ({
240
220
  */
241
221
  const handleTextareaPaste = useCallback(
242
222
  (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
223
+ if (disablePastedText) return;
243
224
  if (e.clipboardData.files?.length) return;
244
225
  const text = e.clipboardData.getData('text/plain');
245
226
  if (!text?.trim()) return;
@@ -256,32 +237,28 @@ const ChatInputs: React.FC<Props> = ({
256
237
  return;
257
238
  }
258
239
 
259
- // Critical: pasted content exceeds single-document size limit – reject and inform (same prop as total limit)
240
+ const totalPayloadLimit = maxTotalMessagePayload ?? MAX_TOTAL_MESSAGE_PAYLOAD;
260
241
  const perDocumentLimit = maxTotalMessagePayload ?? MAX_DOCUMENT_CONTENT_LENGTH;
242
+
261
243
  if (text.length > perDocumentLimit) {
262
244
  e.preventDefault();
263
- toast.error(
264
- t('upload.contextSizeExceedsLimit', {
265
- defaultValue:
266
- 'Context size exceeds the limit. Try reducing the number of files or content in the conversation.',
267
- })
268
- );
245
+ toast(t('upload.pasteContentExceedsLimit', {
246
+ defaultValue:
247
+ 'Pasted content exceeds the size limit. Try shortening the text or splitting it into smaller parts.',
248
+ }), { icon: '⚠️' });
269
249
  return;
270
250
  }
271
251
 
272
- const totalPayloadLimit = maxTotalMessagePayload ?? MAX_TOTAL_MESSAGE_PAYLOAD;
273
252
  const currentTotal = documentPreviewFiles.reduce(
274
253
  (sum, f) => sum + f.content.length,
275
254
  0
276
255
  );
277
256
  if (currentTotal + text.length > totalPayloadLimit) {
278
257
  e.preventDefault();
279
- toast.error(
280
- t('upload.contextSizeExceedsLimit', {
281
- defaultValue:
282
- 'Context size exceeds the limit. Try reducing the number of files or content in the conversation.',
283
- })
284
- );
258
+ toast(t('upload.pasteContentExceedsLimit', {
259
+ defaultValue:
260
+ 'Pasted content exceeds the size limit. Try shortening the text or splitting it into smaller parts.',
261
+ }), { icon: '⚠️' });
285
262
  return;
286
263
  }
287
264
 
@@ -314,7 +291,7 @@ ${text}
314
291
  ) => [...prev, newFile]
315
292
  );
316
293
  },
317
- [documentPreviewFiles, maxTotalMessagePayload, t]
294
+ [documentPreviewFiles, disablePastedText, maxTotalMessagePayload, t]
318
295
  );
319
296
 
320
297
  const isDisabled =
@@ -371,6 +348,7 @@ ${text}
371
348
  onBlur={onTextareaBlur}
372
349
  onExpandedChange={handleTextareaExpanded}
373
350
  disabled={textareaDisabled}
351
+ maxTextareaCharacters={maxTextareaCharacters}
374
352
  />
375
353
  </div>
376
354
 
@@ -5,10 +5,23 @@
5
5
  min-height: 36px;
6
6
  box-sizing: border-box;
7
7
  flex: 1;
8
- align-items: center;
8
+ flex-direction: column;
9
9
  transition: height 0.2s ease-in-out;
10
10
  }
11
11
 
12
+ .memori-chat-textarea--with-counter {
13
+ align-items: stretch;
14
+ }
15
+
16
+ .memori-chat-textarea--counter {
17
+ min-height: 18px;
18
+ flex-shrink: 0;
19
+ padding: 2px 0 4px 0;
20
+ color: var(--memori-text-color, #666);
21
+ font-size: 12px;
22
+ line-height: 1.2;
23
+ }
24
+
12
25
  .memori-chat-textarea--expanded {
13
26
  min-height: 36px;
14
27
  }
@@ -40,6 +53,11 @@
40
53
  transition: height 0.2s ease-in-out;
41
54
  }
42
55
 
56
+ .memori-chat-textarea--with-counter .memori-chat-textarea--inner {
57
+ min-height: 36px;
58
+ flex: 1;
59
+ }
60
+
43
61
  .memori-chat-textarea--expanded .memori-chat-textarea--inner {
44
62
  max-height: 208px;
45
63
  }
@@ -15,6 +15,8 @@ export interface Props {
15
15
  onFocus?: (e: React.FocusEvent) => void;
16
16
  onBlur?: (e: React.FocusEvent) => void;
17
17
  onExpandedChange?: (expanded: boolean) => void;
18
+ /** When set, shows a character counter (e.g. "0 / 500") above the textarea and enforces the max length. */
19
+ maxTextareaCharacters?: number;
18
20
  }
19
21
 
20
22
  const ChatTextArea: React.FC<Props> = ({
@@ -26,6 +28,7 @@ const ChatTextArea: React.FC<Props> = ({
26
28
  onFocus,
27
29
  onBlur,
28
30
  onExpandedChange,
31
+ maxTextareaCharacters,
29
32
  }) => {
30
33
  const { t } = useTranslation();
31
34
  const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -78,22 +81,48 @@ const ChatTextArea: React.FC<Props> = ({
78
81
  }
79
82
  }, [value]);
80
83
 
84
+ const displayValue =
85
+ maxTextareaCharacters != null
86
+ ? value.slice(0, maxTextareaCharacters)
87
+ : value;
88
+
89
+ // Keep parent state in sync when value exceeds limit (e.g. paste or prop change)
90
+ useEffect(() => {
91
+ if (
92
+ maxTextareaCharacters != null &&
93
+ value.length > maxTextareaCharacters
94
+ ) {
95
+ onChange(value.slice(0, maxTextareaCharacters));
96
+ }
97
+ }, [maxTextareaCharacters, value, onChange]);
98
+
81
99
  return (
82
100
  <div
83
101
  data-testid="chat-textarea"
84
102
  className={cx('memori-chat-textarea', {
85
103
  'memori-chat-textarea--disabled': disabled,
104
+ 'memori-chat-textarea--with-counter': maxTextareaCharacters != null,
86
105
  })}
87
106
  >
107
+ {maxTextareaCharacters != null && (
108
+ <div className="memori-chat-textarea--counter" aria-live="polite">
109
+ {displayValue.length} / {maxTextareaCharacters}
110
+ </div>
111
+ )}
88
112
  <div ref={innerRef} className="memori-chat-textarea--inner">
89
113
  <textarea
90
114
  ref={textareaRef}
91
115
  className="memori-chat-textarea--input"
92
116
  disabled={disabled}
93
- value={value}
117
+ value={displayValue}
94
118
  placeholder={t('placeholder', 'Ask a question') || 'Ask a question'}
95
119
  onChange={e => {
96
- onChange(e.target.value);
120
+ const next = e.target.value;
121
+ if (maxTextareaCharacters != null) {
122
+ onChange(next.slice(0, maxTextareaCharacters));
123
+ } else {
124
+ onChange(next);
125
+ }
97
126
  }}
98
127
  onKeyDownCapture={e => {
99
128
  // On mobile/tablet only: Enter creates a new line instead of sending.
@@ -106,10 +135,12 @@ const ChatTextArea: React.FC<Props> = ({
106
135
  e.preventDefault();
107
136
 
108
137
  const el = textareaRef.current;
109
- const start = el?.selectionStart ?? value.length;
110
- const end = el?.selectionEnd ?? value.length;
111
- const nextValue = `${value.slice(0, start)}\n${value.slice(end)}`;
112
-
138
+ const start = el?.selectionStart ?? displayValue.length;
139
+ const end = el?.selectionEnd ?? displayValue.length;
140
+ let nextValue = `${displayValue.slice(0, start)}\n${displayValue.slice(end)}`;
141
+ if (maxTextareaCharacters != null) {
142
+ nextValue = nextValue.slice(0, maxTextareaCharacters);
143
+ }
113
144
  onChange(nextValue);
114
145
 
115
146
  // Restore caret right after the inserted newline
@@ -130,7 +161,7 @@ const ChatTextArea: React.FC<Props> = ({
130
161
  onPaste={onPaste}
131
162
  onFocus={onFocus}
132
163
  onBlur={onBlur}
133
- maxLength={100000}
164
+ maxLength={maxTextareaCharacters ?? 100000}
134
165
  />
135
166
  </div>
136
167
  </div>
@@ -228,7 +228,6 @@
228
228
  transform: scale(1.1);
229
229
  }
230
230
 
231
-
232
231
  /* Show remove button only when user hovers the preview item */
233
232
  .memori--preview-item:hover .memori--remove-button {
234
233
  opacity: 1;
@@ -435,6 +435,10 @@ export interface Props {
435
435
  __WEBCOMPONENT__?: boolean;
436
436
  /** Override total document payload and per-document content limit (character count). Default from constants. */
437
437
  maxTotalMessagePayload?: number;
438
+ /** When true, pasted text is not added as a document attachment (normal paste only). Default false. */
439
+ disablePastedText?: boolean;
440
+ /** Max characters in chat textarea; shows counter and disables paste-as-attachment when set. */
441
+ maxTextareaCharacters?: number;
438
442
  }
439
443
 
440
444
  const MemoriWidget = ({
@@ -491,6 +495,8 @@ const MemoriWidget = ({
491
495
  applyVarsToRoot = false,
492
496
  showFunctionCache = false,
493
497
  maxTotalMessagePayload,
498
+ disablePastedText = false,
499
+ maxTextareaCharacters,
494
500
  }: Props) => {
495
501
  const { t, i18n } = useTranslation();
496
502
 
@@ -3025,6 +3031,8 @@ const MemoriWidget = ({
3025
3031
  experts,
3026
3032
  useMathFormatting: applyMathFormatting,
3027
3033
  maxTotalMessagePayload,
3034
+ disablePastedText,
3035
+ maxTextareaCharacters,
3028
3036
  };
3029
3037
 
3030
3038
  const integrationBackground =
@@ -114,7 +114,7 @@ const UploadButton: React.FC<UploadManagerProps> = ({
114
114
  } else {
115
115
  addErrorRef.current({
116
116
  message: `File "${file.name}" is not a supported image or document type`,
117
- severity: 'error',
117
+ severity: 'warning',
118
118
  });
119
119
  }
120
120
  });
@@ -126,7 +126,7 @@ const UploadButton: React.FC<UploadManagerProps> = ({
126
126
  if (remainingSlots <= 0) {
127
127
  addErrorRef.current({
128
128
  message: `Maximum ${MAX_DOCUMENTS_PER_MESSAGE} media files allowed.`,
129
- severity: 'error',
129
+ severity: 'warning',
130
130
  });
131
131
  return;
132
132
  }
@@ -144,7 +144,7 @@ const UploadButton: React.FC<UploadManagerProps> = ({
144
144
  max: MAX_DOCUMENTS_PER_MESSAGE,
145
145
  defaultValue: `${skipped} file(s) not added (maximum ${MAX_DOCUMENTS_PER_MESSAGE} files allowed).`,
146
146
  }) ?? `${skipped} file(s) not added (maximum ${MAX_DOCUMENTS_PER_MESSAGE} files allowed).`,
147
- severity: 'info',
147
+ severity: 'warning',
148
148
  });
149
149
  }
150
150
 
@@ -153,11 +153,8 @@ const UploadButton: React.FC<UploadManagerProps> = ({
153
153
  if (!isMediaAcceptedRef.current) {
154
154
  addErrorRef.current({
155
155
  message:
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).`,
160
- severity: 'info',
156
+ t('upload.mediaNotAccepted') ?? 'Media uploads are not accepted',
157
+ severity: 'warning',
161
158
  });
162
159
  } else {
163
160
  // Trigger image upload by creating a synthetic event
@@ -417,7 +414,7 @@ ${file.content}
417
414
  message: `File type "${fileExt}" is not supported. Please use: ${ALLOWED_FILE_TYPES.join(
418
415
  ', '
419
416
  )}`,
420
- severity: 'error',
417
+ severity: 'warning',
421
418
  });
422
419
  return false;
423
420
  }
@@ -427,7 +424,7 @@ ${file.content}
427
424
  message: `File "${file.name}" exceeds ${
428
425
  MAX_FILE_SIZE / 1024 / 1024
429
426
  }MB limit`,
430
- severity: 'error',
427
+ severity: 'warning',
431
428
  });
432
429
  return false;
433
430
  }
@@ -458,11 +455,7 @@ ${file.content}
458
455
  );
459
456
 
460
457
  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.',
464
- });
465
- return { valid: false, message: typeof msg === 'string' ? msg : String(msg) };
458
+ return { valid: false, message: '' };
466
459
  }
467
460
 
468
461
  return { valid: true };
@@ -490,7 +483,7 @@ ${file.content}
490
483
  message: `File type "${fileExt}" is not supported. Please use: ${ALLOWED_FILE_TYPES.join(
491
484
  ', '
492
485
  )}`,
493
- severity: 'error',
486
+ severity: 'warning',
494
487
  });
495
488
  return false;
496
489
  }
@@ -500,7 +493,7 @@ ${file.content}
500
493
  message: `File "${file.name}" exceeds ${
501
494
  MAX_FILE_SIZE / 1024 / 1024
502
495
  }MB limit`,
503
- severity: 'error',
496
+ severity: 'warning',
504
497
  });
505
498
  return false;
506
499
  }
@@ -250,13 +250,6 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
250
250
  '>',
251
251
  perDocumentLimit
252
252
  );
253
- onDocumentError?.({
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',
259
- });
260
253
  wasTruncated = true;
261
254
  text =
262
255
  text.substring(0, perDocumentLimit) +
@@ -295,7 +288,7 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
295
288
  max: maxDocuments ?? 10,
296
289
  defaultValue: `${skipped} document(s) not added (maximum ${maxDocuments ?? 10} files allowed).`,
297
290
  }) ?? `${skipped} document(s) not added (maximum ${maxDocuments ?? 10} files allowed).`,
298
- severity: 'info',
291
+ severity: 'warning',
299
292
  });
300
293
  }
301
294
 
@@ -349,14 +342,6 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
349
342
  content: text,
350
343
  mimeType: file.type,
351
344
  });
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
- });
360
345
  }
361
346
  } catch (error) {
362
347
  console.error('File processing error:', error);
@@ -364,7 +349,7 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
364
349
  message: `${
365
350
  error instanceof Error ? error.message : 'Unknown error'
366
351
  }`,
367
- severity: 'error',
352
+ severity: 'warning',
368
353
  });
369
354
  }
370
355
  }
@@ -375,17 +360,7 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
375
360
  count: skippedDueToPayload,
376
361
  defaultValue: `${skippedDueToPayload} document(s) not added (context size limit).`,
377
362
  }),
378
- severity: 'info',
379
- });
380
- }
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',
363
+ severity: 'warning',
389
364
  });
390
365
  }
391
366
 
@@ -102,7 +102,7 @@ const UploadImages: React.FC<UploadImagesProps> = ({
102
102
  max: maxImages,
103
103
  defaultValue: `${skipped} image(s) not added (maximum ${maxImages} files allowed).`,
104
104
  }) ?? `${skipped} image(s) not added (maximum ${maxImages} files allowed).`,
105
- severity: 'info',
105
+ severity: 'warning',
106
106
  });
107
107
  }
108
108
 
@@ -122,14 +122,6 @@ const UploadImages: React.FC<UploadImagesProps> = ({
122
122
  });
123
123
 
124
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
- });
133
125
  if (imageInputRef.current) {
134
126
  imageInputRef.current.value = '';
135
127
  }
@@ -251,7 +243,7 @@ const UploadImages: React.FC<UploadImagesProps> = ({
251
243
  } catch (error) {
252
244
  onImageError?.({
253
245
  message: t('upload.uploadFailed') ?? 'Upload failed',
254
- severity: 'error',
246
+ severity: 'warning',
255
247
  });
256
248
  resolve(null);
257
249
  }
@@ -269,7 +261,7 @@ const UploadImages: React.FC<UploadImagesProps> = ({
269
261
  reader.onerror = () => {
270
262
  onImageError?.({
271
263
  message: t('upload.fileReadingFailed') ?? 'File reading failed',
272
- severity: 'error',
264
+ severity: 'warning',
273
265
  });
274
266
  resolve(null);
275
267
  };
@@ -290,7 +282,7 @@ const UploadImages: React.FC<UploadImagesProps> = ({
290
282
  } catch (error) {
291
283
  onImageError?.({
292
284
  message: t('upload.uploadFailed') ?? 'Upload failed',
293
- severity: 'error',
285
+ severity: 'warning',
294
286
  });
295
287
  } finally {
296
288
  setIsLoading(false);
@@ -398,7 +390,7 @@ const UploadImages: React.FC<UploadImagesProps> = ({
398
390
  } catch (error) {
399
391
  onImageError?.({
400
392
  message: t('upload.uploadFailed') ?? 'Upload failed',
401
- severity: 'error',
393
+ severity: 'warning',
402
394
  });
403
395
  }
404
396
  } else {
@@ -416,7 +408,7 @@ const UploadImages: React.FC<UploadImagesProps> = ({
416
408
  reader.onerror = () => {
417
409
  onImageError?.({
418
410
  message: t('upload.fileReadingFailed') ?? 'File reading failed',
419
- severity: 'error',
411
+ severity: 'warning',
420
412
  });
421
413
  setIsLoading(false);
422
414
  };
@@ -425,7 +417,7 @@ const UploadImages: React.FC<UploadImagesProps> = ({
425
417
  } catch (error) {
426
418
  onImageError?.({
427
419
  message: t('upload.uploadFailed') ?? 'Upload failed',
428
- severity: 'error',
420
+ severity: 'warning',
429
421
  });
430
422
  setIsLoading(false);
431
423
  }
@@ -167,6 +167,7 @@ WebsiteAssistant3.args = {
167
167
  showOnlyLastMessages: false,
168
168
  showTranslationOriginal: false,
169
169
  showCopyButton: false,
170
+ disablePastedText: true,
170
171
  };
171
172
 
172
173
 
@@ -101,6 +101,21 @@ WithUploadWithMaxTotalMessagePayload.args = {
101
101
  maxTotalMessagePayload: 300000,
102
102
  };
103
103
 
104
+ export const WithMaxTextareaCharacters = Template.bind({});
105
+ WithMaxTextareaCharacters.args = {
106
+ ownerUserName: 'nzambello',
107
+ memoriName: 'Nicola',
108
+ tenantID: 'www.aisuru.com',
109
+ engineURL: 'https://engine.memori.ai',
110
+ apiURL: 'https://backend.memori.ai',
111
+ baseURL: 'https://www.aisuru.com',
112
+ uiLang: 'IT',
113
+ spokenLang: 'IT',
114
+ enableAudio: true,
115
+ showUpload: true,
116
+ maxTextareaCharacters: 500,
117
+ };
118
+
104
119
  export const WithPrivateAgent = Template.bind({});
105
120
  WithPrivateAgent.args = {
106
121
  memoriName: 'Test Private',
package/src/index.tsx CHANGED
@@ -48,7 +48,7 @@ export interface Props {
48
48
  __WEBCOMPONENT__?: boolean;
49
49
  showClear?: boolean;
50
50
  showOnlyLastMessages?: boolean;
51
- showTypingText?: boolean;
51
+ showTypingText?: boolean;
52
52
  showLogin?: boolean;
53
53
  showUpload?: boolean;
54
54
  showReasoning?: boolean;
@@ -77,6 +77,10 @@ export interface Props {
77
77
  applyVarsToRoot?: boolean;
78
78
  /** Override total document payload and per-document content limit (character count). Default from constants (200000). */
79
79
  maxTotalMessagePayload?: number;
80
+ /** When true, pasted text is not added as a document attachment (normal paste only). Default false. */
81
+ disablePastedText?: boolean;
82
+ /** Max characters allowed in the chat textarea. When set, shows a counter (e.g. "0 / 500") and disables paste-as-attachment by default. */
83
+ maxTextareaCharacters?: number;
80
84
  }
81
85
 
82
86
  const getPreferredLanguages = () => {
@@ -156,6 +160,8 @@ const Memori: React.FC<Props> = ({
156
160
  applyVarsToRoot = false,
157
161
  __WEBCOMPONENT__ = false,
158
162
  maxTotalMessagePayload,
163
+ disablePastedText = false,
164
+ maxTextareaCharacters,
159
165
  }) => {
160
166
  const [memori, setMemori] = useState<IMemori>();
161
167
  const [tenant, setTenant] = useState<Tenant>();
@@ -469,6 +475,8 @@ const Memori: React.FC<Props> = ({
469
475
  userAvatar={userAvatar}
470
476
  applyVarsToRoot={applyVarsToRoot}
471
477
  maxTotalMessagePayload={maxTotalMessagePayload}
478
+ disablePastedText={disablePastedText ?? (maxTextareaCharacters != null)}
479
+ maxTextareaCharacters={maxTextareaCharacters}
472
480
  disableTextEnteredEvents={disableTextEnteredEvents}
473
481
  // From layout, from client if allowed
474
482
  {...clientAttributes}
@@ -570,5 +578,7 @@ Memori.propTypes = {
570
578
  autoStart: PropTypes.bool,
571
579
  applyVarsToRoot: PropTypes.bool,
572
580
  maxTotalMessagePayload: PropTypes.number,
581
+ disablePastedText: PropTypes.bool,
582
+ maxTextareaCharacters: PropTypes.number,
573
583
  };
574
584
  export default Memori;