@memori.ai/memori-react 8.19.1 → 8.19.3

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 (61) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/components/Chat/Chat.d.ts +0 -1
  3. package/dist/components/Chat/Chat.js +3 -2
  4. package/dist/components/Chat/Chat.js.map +1 -1
  5. package/dist/components/ChatInputs/ChatInputs.d.ts +4 -1
  6. package/dist/components/ChatInputs/ChatInputs.js +30 -13
  7. package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
  8. package/dist/components/MemoriWidget/MemoriWidget.d.ts +1 -2
  9. package/dist/components/MemoriWidget/MemoriWidget.js +1 -2
  10. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  11. package/dist/components/UploadButton/UploadButton.d.ts +2 -0
  12. package/dist/components/UploadButton/UploadButton.js +9 -11
  13. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  14. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +1 -0
  15. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +3 -4
  16. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  17. package/dist/helpers/constants.d.ts +5 -5
  18. package/dist/helpers/constants.js +6 -6
  19. package/dist/helpers/constants.js.map +1 -1
  20. package/dist/index.d.ts +0 -1
  21. package/dist/index.js +2 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/version.d.ts +1 -1
  24. package/dist/version.js +1 -1
  25. package/esm/components/Chat/Chat.d.ts +0 -1
  26. package/esm/components/Chat/Chat.js +3 -2
  27. package/esm/components/Chat/Chat.js.map +1 -1
  28. package/esm/components/ChatInputs/ChatInputs.d.ts +4 -1
  29. package/esm/components/ChatInputs/ChatInputs.js +30 -13
  30. package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
  31. package/esm/components/MemoriWidget/MemoriWidget.d.ts +1 -2
  32. package/esm/components/MemoriWidget/MemoriWidget.js +1 -2
  33. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  34. package/esm/components/UploadButton/UploadButton.d.ts +2 -0
  35. package/esm/components/UploadButton/UploadButton.js +9 -11
  36. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  37. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +1 -0
  38. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +3 -4
  39. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  40. package/esm/helpers/constants.d.ts +5 -5
  41. package/esm/helpers/constants.js +5 -5
  42. package/esm/helpers/constants.js.map +1 -1
  43. package/esm/index.d.ts +0 -1
  44. package/esm/index.js +2 -3
  45. package/esm/index.js.map +1 -1
  46. package/esm/version.d.ts +1 -1
  47. package/esm/version.js +1 -1
  48. package/package.json +1 -1
  49. package/src/components/Chat/Chat.tsx +13 -7
  50. package/src/components/ChatInputs/ChatInputs.tsx +51 -20
  51. package/src/components/MemoriWidget/MemoriWidget.tsx +1 -5
  52. package/src/components/UploadButton/UploadButton.tsx +17 -13
  53. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +5 -8
  54. package/src/components/layouts/layouts.stories.tsx +0 -1
  55. package/src/helpers/constants.ts +5 -16
  56. package/src/index.tsx +1 -6
  57. package/src/version.ts +1 -1
  58. package/dist/components/layouts/fullpage.css +0 -114
  59. package/esm/components/layouts/fullpage.css +0 -114
  60. package/src/components/ChatInputs/ChatInputs.test.tsx +0 -468
  61. package/src/components/ChatInputs/__snapshots__/ChatInputs.test.tsx.snap +0 -607
@@ -1,114 +0,0 @@
1
- /* FullPage layout – spacing, typography and structure using global design tokens */
2
-
3
- .memori-widget.memori-layout-fullpage {
4
- max-width: var(--memori-layout-max-width);
5
- padding: var(--memori-spacing-md);
6
- margin-right: auto;
7
- margin-left: auto;
8
- }
9
-
10
- .memori-widget.memori-layout-fullpage > .memori-spin {
11
- display: flex;
12
- height: 100%;
13
- flex-direction: column;
14
- gap: var(--memori-spacing-lg);
15
- }
16
-
17
- /* Header spacing */
18
- .memori-widget.memori-layout-fullpage .memori-chat-layout--header {
19
- flex-shrink: 0;
20
- margin-bottom: 0;
21
- }
22
-
23
- /* Grid: main + left column (avatar) */
24
- .memori-widget.memori-layout-fullpage .memori--grid {
25
- display: flex;
26
- height: auto;
27
- height: 85vh;
28
- min-height: 0;
29
- flex: 1 1 auto;
30
- align-items: stretch;
31
- justify-content: center;
32
- gap: var(--memori-spacing-xl);
33
- }
34
-
35
- .memori-widget.memori-layout-fullpage .memori--grid-column-left {
36
- width: 50%;
37
- max-width: 420px;
38
- flex-shrink: 0;
39
- margin-right: 0;
40
- }
41
-
42
- .memori-widget.memori-layout-fullpage .memori-chat-layout--main {
43
- display: flex;
44
- min-width: 0;
45
- flex: 1;
46
- flex-direction: column;
47
- align-items: center;
48
- justify-content: center;
49
- }
50
-
51
- .memori-widget.memori-layout-fullpage .memori-chat-layout--controls {
52
- display: flex;
53
- width: 100%;
54
- max-width: 720px;
55
- flex-direction: column;
56
- align-items: center;
57
- padding: var(--memori-spacing-md);
58
- }
59
-
60
- .memori-widget.memori-layout-fullpage .memori-chat-layout--controls .memori--start-panel,
61
- .memori-widget.memori-layout-fullpage .memori-chat-layout--controls .memori-chat--wrapper {
62
- width: 100%;
63
- max-width: 100%;
64
- padding: 0;
65
- margin-right: 0;
66
- margin-left: 0;
67
- }
68
-
69
- /* Typography: title and description in fullpage left column */
70
- .memori-widget.memori-layout-fullpage .memori--title {
71
- margin-bottom: var(--memori-spacing-xs);
72
- font-size: var(--memori-text-size-heading-large);
73
- font-weight: var(--memori-text-weight-semibold);
74
- line-height: var(--memori-text-line-tight);
75
- }
76
-
77
- .memori-widget.memori-layout-fullpage .memori--description,
78
- .memori-widget.memori-layout-fullpage .memori--needsPosition {
79
- color: var(--memori-text-color);
80
- font-size: var(--memori-text-size-base);
81
- line-height: var(--memori-text-line-relaxed);
82
- }
83
-
84
- /* Powered-by position in fullpage */
85
- .memori-widget.memori-layout-fullpage .memori--powered-by {
86
- flex-shrink: 0;
87
- padding: var(--memori-spacing-sm) var(--memori-spacing-md);
88
- border-radius: var(--memori-radius-box);
89
- font-size: var(--memori-text-size-small);
90
- }
91
-
92
- @media (max-width: 870px) {
93
- .memori-widget.memori-layout-fullpage {
94
- padding: var(--memori-spacing-sm);
95
- }
96
-
97
- .memori-widget.memori-layout-fullpage > .memori-spin {
98
- gap: var(--memori-spacing-md);
99
- }
100
-
101
- .memori-widget.memori-layout-fullpage .memori--grid-column-left {
102
- display: none;
103
- }
104
-
105
- .memori-widget.memori-layout-fullpage .memori-chat-layout--controls {
106
- padding: var(--memori-spacing-sm);
107
- }
108
- }
109
-
110
- @media (max-width: 480px) {
111
- .memori-widget.memori-layout-fullpage .memori-chat-layout--controls {
112
- padding: var(--memori-spacing-xs);
113
- }
114
- }
@@ -1,114 +0,0 @@
1
- /* FullPage layout – spacing, typography and structure using global design tokens */
2
-
3
- .memori-widget.memori-layout-fullpage {
4
- max-width: var(--memori-layout-max-width);
5
- padding: var(--memori-spacing-md);
6
- margin-right: auto;
7
- margin-left: auto;
8
- }
9
-
10
- .memori-widget.memori-layout-fullpage > .memori-spin {
11
- display: flex;
12
- height: 100%;
13
- flex-direction: column;
14
- gap: var(--memori-spacing-lg);
15
- }
16
-
17
- /* Header spacing */
18
- .memori-widget.memori-layout-fullpage .memori-chat-layout--header {
19
- flex-shrink: 0;
20
- margin-bottom: 0;
21
- }
22
-
23
- /* Grid: main + left column (avatar) */
24
- .memori-widget.memori-layout-fullpage .memori--grid {
25
- display: flex;
26
- height: auto;
27
- height: 85vh;
28
- min-height: 0;
29
- flex: 1 1 auto;
30
- align-items: stretch;
31
- justify-content: center;
32
- gap: var(--memori-spacing-xl);
33
- }
34
-
35
- .memori-widget.memori-layout-fullpage .memori--grid-column-left {
36
- width: 50%;
37
- max-width: 420px;
38
- flex-shrink: 0;
39
- margin-right: 0;
40
- }
41
-
42
- .memori-widget.memori-layout-fullpage .memori-chat-layout--main {
43
- display: flex;
44
- min-width: 0;
45
- flex: 1;
46
- flex-direction: column;
47
- align-items: center;
48
- justify-content: center;
49
- }
50
-
51
- .memori-widget.memori-layout-fullpage .memori-chat-layout--controls {
52
- display: flex;
53
- width: 100%;
54
- max-width: 720px;
55
- flex-direction: column;
56
- align-items: center;
57
- padding: var(--memori-spacing-md);
58
- }
59
-
60
- .memori-widget.memori-layout-fullpage .memori-chat-layout--controls .memori--start-panel,
61
- .memori-widget.memori-layout-fullpage .memori-chat-layout--controls .memori-chat--wrapper {
62
- width: 100%;
63
- max-width: 100%;
64
- padding: 0;
65
- margin-right: 0;
66
- margin-left: 0;
67
- }
68
-
69
- /* Typography: title and description in fullpage left column */
70
- .memori-widget.memori-layout-fullpage .memori--title {
71
- margin-bottom: var(--memori-spacing-xs);
72
- font-size: var(--memori-text-size-heading-large);
73
- font-weight: var(--memori-text-weight-semibold);
74
- line-height: var(--memori-text-line-tight);
75
- }
76
-
77
- .memori-widget.memori-layout-fullpage .memori--description,
78
- .memori-widget.memori-layout-fullpage .memori--needsPosition {
79
- color: var(--memori-text-color);
80
- font-size: var(--memori-text-size-base);
81
- line-height: var(--memori-text-line-relaxed);
82
- }
83
-
84
- /* Powered-by position in fullpage */
85
- .memori-widget.memori-layout-fullpage .memori--powered-by {
86
- flex-shrink: 0;
87
- padding: var(--memori-spacing-sm) var(--memori-spacing-md);
88
- border-radius: var(--memori-radius-box);
89
- font-size: var(--memori-text-size-small);
90
- }
91
-
92
- @media (max-width: 870px) {
93
- .memori-widget.memori-layout-fullpage {
94
- padding: var(--memori-spacing-sm);
95
- }
96
-
97
- .memori-widget.memori-layout-fullpage > .memori-spin {
98
- gap: var(--memori-spacing-md);
99
- }
100
-
101
- .memori-widget.memori-layout-fullpage .memori--grid-column-left {
102
- display: none;
103
- }
104
-
105
- .memori-widget.memori-layout-fullpage .memori-chat-layout--controls {
106
- padding: var(--memori-spacing-sm);
107
- }
108
- }
109
-
110
- @media (max-width: 480px) {
111
- .memori-widget.memori-layout-fullpage .memori-chat-layout--controls {
112
- padding: var(--memori-spacing-xs);
113
- }
114
- }
@@ -1,468 +0,0 @@
1
- import React from 'react';
2
- import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
- import ChatInputs from './ChatInputs';
4
- import { dialogState } from '../../mocks/data';
5
- import {
6
- PASTE_AS_CARD_CHAR_THRESHOLD,
7
- PASTE_AS_CARD_LINE_THRESHOLD,
8
- } from '../../helpers/constants';
9
-
10
- jest.mock('react-hot-toast', () => ({
11
- __esModule: true,
12
- default: Object.assign(jest.fn(), {
13
- success: jest.fn(),
14
- error: jest.fn(),
15
- }),
16
- }));
17
-
18
- // jsdom does not define DataTransfer; UploadButton's paste handler uses it when clipboard has files
19
- if (typeof globalThis.DataTransfer === 'undefined') {
20
- (globalThis as any).DataTransfer = class DataTransfer {
21
- _files: File[] = [];
22
- get files(): File[] {
23
- return this._files;
24
- }
25
- items = {
26
- add: (file: File) => {
27
- this._files.push(file);
28
- },
29
- };
30
- };
31
- }
32
-
33
- // jsdom does not define speechSynthesis/SpeechSynthesisUtterance; ChatInputs uses them in onSendMessage
34
- if (typeof (globalThis as any).speechSynthesis === 'undefined') {
35
- (globalThis as any).speechSynthesis = { speak: jest.fn() };
36
- }
37
- if (typeof (globalThis as any).SpeechSynthesisUtterance === 'undefined') {
38
- (globalThis as any).SpeechSynthesisUtterance = function () {};
39
- }
40
-
41
- const defaultProps = {
42
- dialogState,
43
- userMessage: '',
44
- onChangeUserMessage: jest.fn(),
45
- sendMessage: jest.fn(),
46
- onTextareaFocus: jest.fn(),
47
- onTextareaBlur: jest.fn(),
48
- setAttachmentsMenuOpen: jest.fn(),
49
- setSendOnEnter: jest.fn(),
50
- listening: false,
51
- isPlayingAudio: false,
52
- stopAudio: jest.fn(),
53
- startListening: jest.fn(),
54
- stopListening: jest.fn(),
55
- showMicrophone: true,
56
- };
57
-
58
- function createPasteEvent(plainText: string, files: File[] = []) {
59
- return {
60
- clipboardData: {
61
- files,
62
- getData: (type: string) => (type === 'text/plain' ? plainText : ''),
63
- },
64
- preventDefault: jest.fn(),
65
- };
66
- }
67
-
68
- it('renders ChatInputs unchanged', () => {
69
- const { container } = render(
70
- <ChatInputs
71
- dialogState={dialogState}
72
- userMessage=""
73
- onChangeUserMessage={jest.fn()}
74
- sendMessage={jest.fn()}
75
- onTextareaFocus={jest.fn()}
76
- onTextareaBlur={jest.fn()}
77
- setAttachmentsMenuOpen={jest.fn()}
78
- setSendOnEnter={jest.fn()}
79
- listening={false}
80
- isPlayingAudio={false}
81
- stopAudio={jest.fn()}
82
- startListening={jest.fn()}
83
- stopListening={jest.fn()}
84
- showMicrophone={true}
85
- />
86
- );
87
- expect(container).toMatchSnapshot();
88
- });
89
-
90
- it('renders ChatInputs with user message unchanged', () => {
91
- const { container } = render(
92
- <ChatInputs
93
- userMessage="Lorem ipsum"
94
- onChangeUserMessage={jest.fn()}
95
- dialogState={dialogState}
96
- sendMessage={jest.fn()}
97
- onTextareaFocus={jest.fn()}
98
- onTextareaBlur={jest.fn()}
99
- setAttachmentsMenuOpen={jest.fn()}
100
- setSendOnEnter={jest.fn()}
101
- listening={false}
102
- isPlayingAudio={false}
103
- stopAudio={jest.fn()}
104
- startListening={jest.fn()}
105
- stopListening={jest.fn()}
106
- showMicrophone={true}
107
- />
108
- );
109
- expect(container).toMatchSnapshot();
110
- });
111
-
112
- it('renders ChatInputs on instruct unchanged', () => {
113
- const { container } = render(
114
- <ChatInputs
115
- userMessage="Lorem ipsum"
116
- onChangeUserMessage={jest.fn()}
117
- dialogState={{
118
- ...dialogState,
119
- acceptsMedia: true,
120
- }}
121
- sendMessage={jest.fn()}
122
- onTextareaFocus={jest.fn()}
123
- onTextareaBlur={jest.fn()}
124
- setAttachmentsMenuOpen={jest.fn()}
125
- setSendOnEnter={jest.fn()}
126
- instruct
127
- listening={false}
128
- isPlayingAudio={false}
129
- stopAudio={jest.fn()}
130
- startListening={jest.fn()}
131
- stopListening={jest.fn()}
132
- showMicrophone={true}
133
- />
134
- );
135
- expect(container).toMatchSnapshot();
136
- });
137
-
138
- it('renders ChatInputs listening unchanged', () => {
139
- const { container } = render(
140
- <ChatInputs
141
- userMessage="Lorem ipsum"
142
- onChangeUserMessage={jest.fn()}
143
- dialogState={dialogState}
144
- sendMessage={jest.fn()}
145
- onTextareaFocus={jest.fn()}
146
- onTextareaBlur={jest.fn()}
147
- setAttachmentsMenuOpen={jest.fn()}
148
- setSendOnEnter={jest.fn()}
149
- listening={true}
150
- isPlayingAudio={false}
151
- stopAudio={jest.fn()}
152
- startListening={jest.fn()}
153
- stopListening={jest.fn()}
154
- showMicrophone={true}
155
- />
156
- );
157
- expect(container).toMatchSnapshot();
158
- });
159
-
160
- it('renders ChatInputs without microphone button unchanged', () => {
161
- const { container } = render(
162
- <ChatInputs
163
- userMessage="Lorem ipsum"
164
- onChangeUserMessage={jest.fn()}
165
- dialogState={dialogState}
166
- sendMessage={jest.fn()}
167
- onTextareaFocus={jest.fn()}
168
- onTextareaBlur={jest.fn()}
169
- setAttachmentsMenuOpen={jest.fn()}
170
- setSendOnEnter={jest.fn()}
171
- listening={true}
172
- isPlayingAudio={false}
173
- stopAudio={jest.fn()}
174
- startListening={jest.fn()}
175
- stopListening={jest.fn()}
176
- showMicrophone={false}
177
- />
178
- );
179
- expect(container).toMatchSnapshot();
180
- });
181
-
182
- it('renders ChatInputs disabled unchanged', () => {
183
- const { container } = render(
184
- <ChatInputs
185
- userMessage="Lorem ipsum"
186
- onChangeUserMessage={jest.fn()}
187
- dialogState={{
188
- ...dialogState,
189
- state: 'X3',
190
- }}
191
- sendMessage={jest.fn()}
192
- onTextareaFocus={jest.fn()}
193
- onTextareaBlur={jest.fn()}
194
- setAttachmentsMenuOpen={jest.fn()}
195
- setSendOnEnter={jest.fn()}
196
- listening={false}
197
- isPlayingAudio={false}
198
- stopAudio={jest.fn()}
199
- startListening={jest.fn()}
200
- stopListening={jest.fn()}
201
- showMicrophone={true}
202
- />
203
- );
204
- expect(container).toMatchSnapshot();
205
- });
206
-
207
- describe('paste as card (long text becomes attachment)', () => {
208
- beforeEach(() => {
209
- jest.clearAllMocks();
210
- });
211
-
212
- it('adds pasted long text (by chars) as attachment when showUpload is true', async () => {
213
- const longText = 'x'.repeat(PASTE_AS_CARD_CHAR_THRESHOLD + 1);
214
- render(
215
- <ChatInputs
216
- {...defaultProps}
217
- showUpload={true}
218
- dialogState={{ ...dialogState, acceptsMedia: true }}
219
- />
220
- );
221
- const textarea = document.querySelector('textarea');
222
- expect(textarea).toBeTruthy();
223
-
224
- const paste = createPasteEvent(longText);
225
- fireEvent.paste(textarea!, paste);
226
-
227
- await waitFor(() => {
228
- expect(screen.getByText('upload.pastedText')).toBeTruthy();
229
- });
230
- });
231
-
232
- it('adds pasted long text (by lines) as attachment when showUpload is true', async () => {
233
- const lines = Array(PASTE_AS_CARD_LINE_THRESHOLD + 1)
234
- .fill('line')
235
- .join('\n');
236
- render(
237
- <ChatInputs
238
- {...defaultProps}
239
- showUpload={true}
240
- dialogState={{ ...dialogState, acceptsMedia: true }}
241
- />
242
- );
243
- const textarea = document.querySelector('textarea');
244
- const paste = createPasteEvent(lines);
245
- fireEvent.paste(textarea!, paste);
246
-
247
- await waitFor(() => {
248
- expect(screen.getByText('upload.pastedText')).toBeTruthy();
249
- });
250
- });
251
-
252
- it('does not add card for short paste (pastes inline)', async () => {
253
- const shortText = 'hello';
254
- render(
255
- <ChatInputs
256
- {...defaultProps}
257
- showUpload={true}
258
- dialogState={{ ...dialogState, acceptsMedia: true }}
259
- />
260
- );
261
- const textarea = document.querySelector('textarea');
262
- const paste = createPasteEvent(shortText);
263
- fireEvent.paste(textarea!, paste);
264
-
265
- expect(screen.queryByText('upload.pastedText')).toBeNull();
266
- });
267
-
268
- it('adds card even when showUpload is false (paste-as-card always enabled)', async () => {
269
- const longText = 'x'.repeat(PASTE_AS_CARD_CHAR_THRESHOLD + 1);
270
- render(
271
- <ChatInputs
272
- {...defaultProps}
273
- showUpload={false}
274
- />
275
- );
276
- const textarea = document.querySelector('textarea');
277
- fireEvent.paste(textarea!, createPasteEvent(longText));
278
-
279
- await waitFor(() => {
280
- expect(screen.getByText('upload.pastedText')).toBeTruthy();
281
- });
282
- });
283
-
284
- it('calls toast.error when max attachments reached', async () => {
285
- const { MAX_DOCUMENTS_PER_MESSAGE } = require('../../helpers/constants');
286
- const longText = 'x'.repeat(PASTE_AS_CARD_CHAR_THRESHOLD + 1);
287
- render(
288
- <ChatInputs
289
- {...defaultProps}
290
- showUpload={true}
291
- dialogState={{ ...dialogState, acceptsMedia: true }}
292
- />
293
- );
294
- const textarea = document.querySelector('textarea');
295
- for (let i = 0; i < MAX_DOCUMENTS_PER_MESSAGE; i++) {
296
- fireEvent.paste(textarea!, createPasteEvent(longText + i));
297
- await waitFor(() => {
298
- expect(document.querySelectorAll('.memori--preview-filename').length).toBe(i + 1);
299
- });
300
- }
301
-
302
- fireEvent.paste(textarea!, createPasteEvent(longText + ' over'));
303
-
304
- await waitFor(() => {
305
- const toast = require('react-hot-toast').default;
306
- expect(toast.error).toHaveBeenCalled();
307
- });
308
- expect(document.querySelectorAll('.memori--preview-filename').length).toBe(
309
- MAX_DOCUMENTS_PER_MESSAGE
310
- );
311
- });
312
-
313
- it('shows warning toast and prevents paste when content exceeds size limit', async () => {
314
- const { MAX_DOCUMENT_CONTENT_LENGTH } = require('../../helpers/constants');
315
- const tooLongText = 'x'.repeat(MAX_DOCUMENT_CONTENT_LENGTH + 1);
316
- render(
317
- <ChatInputs
318
- {...defaultProps}
319
- showUpload={true}
320
- dialogState={{ ...dialogState, acceptsMedia: true }}
321
- />
322
- );
323
- const textarea = document.querySelector('textarea');
324
- const paste = createPasteEvent(tooLongText);
325
- fireEvent.paste(textarea!, paste);
326
-
327
- await waitFor(() => {
328
- const toast = require('react-hot-toast').default;
329
- expect(toast).toHaveBeenCalled();
330
- });
331
- expect(screen.queryByText('upload.pastedText')).toBeNull();
332
- });
333
-
334
- it('adds card when paste exceeds line threshold', async () => {
335
- const manyLines = Array(PASTE_AS_CARD_LINE_THRESHOLD + 1)
336
- .fill('line')
337
- .join('\n');
338
- render(
339
- <ChatInputs
340
- {...defaultProps}
341
- showUpload={true}
342
- dialogState={{ ...dialogState, acceptsMedia: true }}
343
- />
344
- );
345
- const textarea = document.querySelector('textarea');
346
- fireEvent.paste(textarea!, createPasteEvent(manyLines));
347
- await waitFor(() => {
348
- expect(screen.getByText('upload.pastedText')).toBeTruthy();
349
- });
350
- });
351
-
352
- it('does not add card when paste has few chars (pastes inline)', () => {
353
- const fewChars = 'hi';
354
- render(
355
- <ChatInputs
356
- {...defaultProps}
357
- showUpload={true}
358
- dialogState={{ ...dialogState, acceptsMedia: true }}
359
- />
360
- );
361
- const textarea = document.querySelector('textarea');
362
- fireEvent.paste(textarea!, createPasteEvent(fewChars));
363
- expect(screen.queryByText('upload.pastedText')).toBeNull();
364
- });
365
-
366
- it('does not add card when clipboard has files (handler returns early)', () => {
367
- const longText = 'x'.repeat(PASTE_AS_CARD_CHAR_THRESHOLD + 1);
368
- const file = new File(['a'], 'a.txt', { type: 'text/plain' });
369
- const paste = createPasteEvent(longText, [file]);
370
- render(
371
- <ChatInputs
372
- {...defaultProps}
373
- showUpload={true}
374
- dialogState={{ ...dialogState, acceptsMedia: true }}
375
- />
376
- );
377
- const textarea = document.querySelector('textarea');
378
- fireEvent.paste(textarea!, paste);
379
- expect(screen.queryByText('upload.pastedText')).toBeNull();
380
- });
381
-
382
- it('does not add card for empty or whitespace-only paste', () => {
383
- render(
384
- <ChatInputs
385
- {...defaultProps}
386
- showUpload={true}
387
- dialogState={{ ...dialogState, acceptsMedia: true }}
388
- />
389
- );
390
- const textarea = document.querySelector('textarea');
391
- fireEvent.paste(textarea!, createPasteEvent(''));
392
- fireEvent.paste(textarea!, createPasteEvent(' \n\t '));
393
- expect(screen.queryByText('upload.pastedText')).toBeNull();
394
- });
395
-
396
- it('adds two cards when same long text is pasted twice (duplicate paste)', async () => {
397
- const longText = 'x'.repeat(PASTE_AS_CARD_CHAR_THRESHOLD + 1);
398
- render(
399
- <ChatInputs
400
- {...defaultProps}
401
- showUpload={true}
402
- dialogState={{ ...dialogState, acceptsMedia: true }}
403
- />
404
- );
405
- const textarea = document.querySelector('textarea');
406
- fireEvent.paste(textarea!, createPasteEvent(longText));
407
- await waitFor(() => {
408
- expect(screen.getByText('upload.pastedText')).toBeTruthy();
409
- });
410
- fireEvent.paste(textarea!, createPasteEvent(longText));
411
- await waitFor(() => {
412
- expect(document.querySelectorAll('.memori--preview-filename').length).toBe(2);
413
- });
414
- });
415
-
416
- it('shows FilePreview when showUpload is false but pasted card was added', async () => {
417
- const longText = 'x'.repeat(PASTE_AS_CARD_CHAR_THRESHOLD + 1);
418
- render(
419
- <ChatInputs
420
- {...defaultProps}
421
- showUpload={false}
422
- />
423
- );
424
- expect(document.querySelector('.memori--preview-container')).toBeNull();
425
- const textarea = document.querySelector('textarea');
426
- fireEvent.paste(textarea!, createPasteEvent(longText));
427
- await waitFor(() => {
428
- expect(screen.getByText('upload.pastedText')).toBeTruthy();
429
- });
430
- expect(document.querySelector('.memori--preview-container')).toBeTruthy();
431
- });
432
-
433
- it('calls sendMessage with pasted card in media when user sends', async () => {
434
- const sendMessageMock = jest.fn();
435
- const pastedText = 'pasted content here' + 'x'.repeat(PASTE_AS_CARD_CHAR_THRESHOLD);
436
- const { container } = render(
437
- <ChatInputs
438
- {...defaultProps}
439
- showUpload={true}
440
- userMessage="hello"
441
- sendMessage={sendMessageMock}
442
- dialogState={{ ...dialogState, acceptsMedia: true }}
443
- />
444
- );
445
- const textarea = document.querySelector('textarea');
446
- fireEvent.paste(textarea!, createPasteEvent(pastedText));
447
- await waitFor(() => {
448
- expect(screen.getByText('upload.pastedText')).toBeTruthy();
449
- });
450
- const sendButton = container.querySelector('.memori-chat-inputs--send-btn');
451
- expect(sendButton).toBeTruthy();
452
- fireEvent.click(sendButton!);
453
-
454
- expect(sendMessageMock).toHaveBeenCalledTimes(1);
455
- const [, media] = sendMessageMock.mock.calls[0];
456
- expect(media).toHaveLength(1);
457
- expect(media![0]).toMatchObject({
458
- mimeType: 'text/plain',
459
- title: 'upload.pastedText',
460
- type: 'document',
461
- properties: expect.objectContaining({ isAttachedFile: true }),
462
- });
463
- expect(media![0].mediumID).toBeDefined();
464
- expect(media![0].content).toContain('<document_attachment');
465
- expect(media![0].content).toContain('</document_attachment>');
466
- expect(media![0].content).toContain(pastedText);
467
- });
468
- });