@aws-amplify/ui-react-ai 1.0.0 → 1.1.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 (26) hide show
  1. package/dist/esm/components/AIConversation/AIConversationProvider.mjs +2 -2
  2. package/dist/esm/components/AIConversation/context/AttachmentContext.mjs +12 -3
  3. package/dist/esm/components/AIConversation/context/ConversationInputContext.mjs +2 -1
  4. package/dist/esm/components/AIConversation/context/elements/IconElement.mjs +2 -2
  5. package/dist/esm/components/AIConversation/context/elements/definitions.mjs +12 -12
  6. package/dist/esm/components/AIConversation/displayText.mjs +6 -0
  7. package/dist/esm/components/AIConversation/utils.mjs +42 -13
  8. package/dist/esm/components/AIConversation/views/Controls/FormControl.mjs +35 -6
  9. package/dist/esm/components/AIConversation/views/default/Form.mjs +10 -16
  10. package/dist/esm/version.mjs +1 -1
  11. package/dist/index.js +117 -49
  12. package/dist/types/components/AIConversation/AIConversationProvider.d.ts +1 -1
  13. package/dist/types/components/AIConversation/context/AttachmentContext.d.ts +5 -5
  14. package/dist/types/components/AIConversation/context/ControlsContext.d.ts +4 -3
  15. package/dist/types/components/AIConversation/context/ConversationInputContext.d.ts +4 -2
  16. package/dist/types/components/AIConversation/context/DisplayTextContext.d.ts +1 -1
  17. package/dist/types/components/AIConversation/context/MessageRenderContext.d.ts +1 -1
  18. package/dist/types/components/AIConversation/context/elements/IconElement.d.ts +2 -2
  19. package/dist/types/components/AIConversation/context/elements/definitions.d.ts +11 -11
  20. package/dist/types/components/AIConversation/context/index.d.ts +2 -2
  21. package/dist/types/components/AIConversation/displayText.d.ts +2 -0
  22. package/dist/types/components/AIConversation/types.d.ts +2 -0
  23. package/dist/types/components/AIConversation/utils.d.ts +10 -0
  24. package/dist/types/components/AIConversation/views/default/Attachments.d.ts +2 -2
  25. package/dist/types/version.d.ts +1 -1
  26. package/package.json +6 -6
@@ -18,7 +18,7 @@ import { WelcomeMessageProvider } from './context/WelcomeMessageContext.mjs';
18
18
  import { FallbackComponentProvider } from './context/FallbackComponentContext.mjs';
19
19
  import './context/elements/definitions.mjs';
20
20
 
21
- const AIConversationProvider = ({ aiContext, actions, allowAttachments, avatars, children, controls, displayText, handleSendMessage, isLoading, messages, messageRenderer, responseComponents, suggestedPrompts, variant, welcomeMessage, FallbackResponseComponent, }) => {
21
+ const AIConversationProvider = ({ aiContext, actions, allowAttachments, avatars, children, controls, displayText, handleSendMessage, isLoading, maxAttachmentSize, maxAttachments, messages, messageRenderer, responseComponents, suggestedPrompts, variant, welcomeMessage, FallbackResponseComponent, }) => {
22
22
  const _displayText = {
23
23
  ...defaultAIConversationDisplayTextEn,
24
24
  ...displayText,
@@ -29,7 +29,7 @@ const AIConversationProvider = ({ aiContext, actions, allowAttachments, avatars,
29
29
  React__default.createElement(FallbackComponentProvider, { FallbackComponent: FallbackResponseComponent },
30
30
  React__default.createElement(MessageRendererProvider, { ...messageRenderer },
31
31
  React__default.createElement(ResponseComponentsProvider, { responseComponents: responseComponents },
32
- React__default.createElement(AttachmentProvider, { allowAttachments: allowAttachments },
32
+ React__default.createElement(AttachmentProvider, { allowAttachments: allowAttachments, maxAttachmentSize: maxAttachmentSize, maxAttachments: maxAttachments },
33
33
  React__default.createElement(ConversationDisplayTextProvider, { ..._displayText },
34
34
  React__default.createElement(ConversationInputContextProvider, null,
35
35
  React__default.createElement(SendMessageContextProvider, { handleSendMessage: handleSendMessage },
@@ -1,8 +1,17 @@
1
1
  import * as React from 'react';
2
2
 
3
- const AttachmentContext = React.createContext(false);
4
- const AttachmentProvider = ({ children, allowAttachments, }) => {
5
- return (React.createElement(AttachmentContext.Provider, { value: allowAttachments ?? false }, children));
3
+ const AttachmentContext = React.createContext({
4
+ allowAttachments: false,
5
+ // We save attachments as base64 strings into dynamodb for conversation history
6
+ // DynamoDB has a max size of 400kb for records
7
+ // This can be overridden so cutsomers could provide a lower number
8
+ // or a higher number if in the future we support larger sizes.
9
+ maxAttachmentSize: 400000,
10
+ maxAttachments: 20,
11
+ });
12
+ const AttachmentProvider = ({ children, allowAttachments = false, maxAttachmentSize = 400000, maxAttachments = 20, }) => {
13
+ const providerValue = React.useMemo(() => ({ maxAttachmentSize, maxAttachments, allowAttachments }), [maxAttachmentSize, maxAttachments, allowAttachments]);
14
+ return (React.createElement(AttachmentContext.Provider, { value: providerValue }, children));
6
15
  };
7
16
 
8
17
  export { AttachmentContext, AttachmentProvider };
@@ -3,7 +3,8 @@ import React__default from 'react';
3
3
  const ConversationInputContext = React__default.createContext({});
4
4
  const ConversationInputContextProvider = ({ children, }) => {
5
5
  const [input, setInput] = React__default.useState();
6
- const providerValue = React__default.useMemo(() => ({ input, setInput }), [input, setInput]);
6
+ const [error, setError] = React__default.useState();
7
+ const providerValue = React__default.useMemo(() => ({ input, setInput, error, setError }), [input, setInput, error, setError]);
7
8
  return (React__default.createElement(ConversationInputContext.Provider, { value: providerValue }, children));
8
9
  };
9
10
 
@@ -1,4 +1,4 @@
1
- import { defineBaseElement, withBaseElementProps } from '@aws-amplify/ui-react-core/elements';
1
+ import { defineBaseElementWithRef, withBaseElementProps } from '@aws-amplify/ui-react-core/elements';
2
2
  import React__default from 'react';
3
3
 
4
4
  const DEFAULT_ICON_PATHS = {
@@ -17,7 +17,7 @@ const DEFAULT_ICON_ATTRIBUTES = {
17
17
  fill: 'none',
18
18
  xmlns: 'http://www.w3.org/2000/svg',
19
19
  };
20
- const BaseIconElement = defineBaseElement({
20
+ const BaseIconElement = defineBaseElementWithRef({
21
21
  type: 'svg',
22
22
  displayName: 'Icon',
23
23
  });
@@ -1,44 +1,44 @@
1
- import { defineBaseElement } from '@aws-amplify/ui-react-core/elements';
1
+ import { defineBaseElementWithRef } from '@aws-amplify/ui-react-core/elements';
2
2
  import { IconElement } from './IconElement.mjs';
3
3
 
4
- const LabelElement = defineBaseElement({
4
+ const LabelElement = defineBaseElementWithRef({
5
5
  type: 'label',
6
6
  displayName: 'Label',
7
7
  });
8
- const TextElement = defineBaseElement({
8
+ const TextElement = defineBaseElementWithRef({
9
9
  type: 'p',
10
10
  displayName: 'Text',
11
11
  });
12
- const UnorderedListElement = defineBaseElement({
12
+ const UnorderedListElement = defineBaseElementWithRef({
13
13
  type: 'ul',
14
14
  displayName: 'UnorderedList',
15
15
  });
16
- const ListItemElement = defineBaseElement({
16
+ const ListItemElement = defineBaseElementWithRef({
17
17
  type: 'li',
18
18
  displayName: 'ListItem',
19
19
  });
20
- const HeadingElement = defineBaseElement({
20
+ const HeadingElement = defineBaseElementWithRef({
21
21
  type: 'h2',
22
22
  displayName: 'Title',
23
23
  });
24
- const ImageElement = defineBaseElement({
24
+ const ImageElement = defineBaseElementWithRef({
25
25
  type: 'img',
26
26
  displayName: 'Image',
27
27
  });
28
- const InputElement = defineBaseElement({
28
+ const InputElement = defineBaseElementWithRef({
29
29
  type: 'input',
30
30
  displayName: 'Input',
31
31
  });
32
- const ButtonElement = defineBaseElement({ type: 'button', displayName: 'Button' });
33
- const ViewElement = defineBaseElement({
32
+ const ButtonElement = defineBaseElementWithRef({ type: 'button', displayName: 'Button' });
33
+ const ViewElement = defineBaseElementWithRef({
34
34
  type: 'div',
35
35
  displayName: 'View',
36
36
  });
37
- const SpanElement = defineBaseElement({
37
+ const SpanElement = defineBaseElementWithRef({
38
38
  type: 'span',
39
39
  displayName: 'Span',
40
40
  });
41
- const TextAreaElement = defineBaseElement({
41
+ const TextAreaElement = defineBaseElementWithRef({
42
42
  type: 'textarea',
43
43
  displayName: 'TextArea',
44
44
  });
@@ -2,6 +2,12 @@ import { formatDate } from './utils.mjs';
2
2
 
3
3
  const defaultAIConversationDisplayTextEn = {
4
4
  getMessageTimestampText: (date) => formatDate(date),
5
+ getMaxAttachmentErrorText(count) {
6
+ return `Cannot choose more than ${count} ${count === 1 ? 'file' : 'files'}. `;
7
+ },
8
+ getAttachmentSizeErrorText(sizeText) {
9
+ return `File size must be below ${sizeText}.`;
10
+ },
5
11
  };
6
12
 
7
13
  export { defaultAIConversationDisplayTextEn };
@@ -12,28 +12,57 @@ function formatDate(date) {
12
12
  return `${dateString} at ${timeString}`;
13
13
  }
14
14
  function arrayBufferToBase64(buffer) {
15
- let binary = '';
16
- const bytes = new Uint8Array(buffer);
17
- const len = bytes.byteLength;
18
- for (let i = 0; i < len; i++) {
19
- binary += String.fromCharCode(bytes[i]);
20
- }
21
- return window.btoa(binary);
22
- }
23
- function convertBufferToBase64(buffer, format) {
24
- let base64string = '';
25
15
  // Use node-based buffer if available
26
16
  // fall back on browser if not
27
17
  if (typeof Buffer !== 'undefined') {
28
- base64string = Buffer.from(new Uint8Array(buffer)).toString('base64');
18
+ return Buffer.from(new Uint8Array(buffer)).toString('base64');
29
19
  }
30
20
  else {
31
- base64string = arrayBufferToBase64(buffer);
21
+ let binary = '';
22
+ const bytes = new Uint8Array(buffer);
23
+ const len = bytes.byteLength;
24
+ for (let i = 0; i < len; i++) {
25
+ binary += String.fromCharCode(bytes[i]);
26
+ }
27
+ return window.btoa(binary);
32
28
  }
29
+ }
30
+ function convertBufferToBase64(buffer, format) {
31
+ const base64string = arrayBufferToBase64(buffer);
33
32
  return `data:image/${format};base64,${base64string}`;
34
33
  }
35
34
  function getImageTypeFromMimeType(mimeType) {
36
35
  return mimeType.split('/')[1];
37
36
  }
37
+ async function attachmentsValidator({ files, maxAttachments, maxAttachmentSize, }) {
38
+ const acceptedFiles = [];
39
+ const rejectedFiles = [];
40
+ let hasMaxSizeError = false;
41
+ for (const file of files) {
42
+ const arrayBuffer = await file.arrayBuffer();
43
+ const base64 = arrayBufferToBase64(arrayBuffer);
44
+ if (base64.length < maxAttachmentSize) {
45
+ acceptedFiles.push(file);
46
+ }
47
+ else {
48
+ rejectedFiles.push(file);
49
+ hasMaxSizeError = true;
50
+ }
51
+ }
52
+ if (acceptedFiles.length > maxAttachments) {
53
+ return {
54
+ acceptedFiles: acceptedFiles.slice(0, maxAttachments),
55
+ rejectedFiles: [...acceptedFiles.slice(maxAttachments), ...rejectedFiles],
56
+ hasMaxAttachmentsError: true,
57
+ hasMaxAttachmentSizeError: hasMaxSizeError,
58
+ };
59
+ }
60
+ return {
61
+ acceptedFiles,
62
+ rejectedFiles,
63
+ hasMaxAttachmentsError: false,
64
+ hasMaxAttachmentSizeError: hasMaxSizeError,
65
+ };
66
+ }
38
67
 
39
- export { convertBufferToBase64, formatDate, getImageTypeFromMimeType };
68
+ export { attachmentsValidator, convertBufferToBase64, formatDate, getImageTypeFromMimeType };
@@ -7,7 +7,7 @@ import { ConversationInputContext } from '../../context/ConversationInputContext
7
7
  import { MessagesContext } from '../../context/MessagesContext.mjs';
8
8
  import '../../context/SuggestedPromptsContext.mjs';
9
9
  import '../../context/MessageVariantContext.mjs';
10
- import '../../context/DisplayTextContext.mjs';
10
+ import { useConversationDisplayText } from '../../context/DisplayTextContext.mjs';
11
11
  import { ControlsContext } from '../../context/ControlsContext.mjs';
12
12
  import { LoadingContext } from '../../context/LoadingContext.mjs';
13
13
  import { ResponseComponentsContext, convertResponseComponentsToToolConfiguration } from '../../context/ResponseComponentsContext.mjs';
@@ -19,8 +19,8 @@ import '../../context/FallbackComponentContext.mjs';
19
19
  import { AIConversationElements } from '../../context/elements/definitions.mjs';
20
20
  import { AttachFileControl } from './AttachFileControl.mjs';
21
21
  import { AttachmentListControl } from './AttachmentListControl.mjs';
22
- import { getImageTypeFromMimeType } from '../../utils.mjs';
23
- import { isFunction } from '@aws-amplify/ui';
22
+ import { attachmentsValidator, getImageTypeFromMimeType } from '../../utils.mjs';
23
+ import { humanFileSize, isFunction } from '@aws-amplify/ui';
24
24
 
25
25
  const { Button, Icon, Label: LabelElement, TextArea, View, } = AIConversationElements;
26
26
  const FIELD_BLOCK = 'ai-field';
@@ -103,9 +103,10 @@ const InputContainer = withBaseElementProps(View, {
103
103
  className: `${FIELD_BLOCK}__input-container`,
104
104
  });
105
105
  const FormControl = () => {
106
- const { input, setInput } = React__default.useContext(ConversationInputContext);
106
+ const { input, setInput, error, setError } = React__default.useContext(ConversationInputContext);
107
107
  const handleSendMessage = React__default.useContext(SendMessageContext);
108
- const allowAttachments = React__default.useContext(AttachmentContext);
108
+ const { allowAttachments, maxAttachmentSize, maxAttachments } = React__default.useContext(AttachmentContext);
109
+ const displayText = useConversationDisplayText();
109
110
  const responseComponents = React__default.useContext(ResponseComponentsContext);
110
111
  const isLoading = React__default.useContext(LoadingContext);
111
112
  const aiContext = React__default.useContext(AIContextContext);
@@ -157,8 +158,36 @@ const FormControl = () => {
157
158
  }
158
159
  }
159
160
  };
161
+ const onValidate = React__default.useCallback(async (files) => {
162
+ const previousFiles = input?.files ?? [];
163
+ const { acceptedFiles, hasMaxAttachmentsError, hasMaxAttachmentSizeError, } = await attachmentsValidator({
164
+ files: [...files, ...previousFiles],
165
+ maxAttachments,
166
+ maxAttachmentSize,
167
+ });
168
+ if (hasMaxAttachmentsError || hasMaxAttachmentSizeError) {
169
+ const errors = [];
170
+ if (hasMaxAttachmentsError) {
171
+ errors.push(displayText.getMaxAttachmentErrorText(maxAttachments));
172
+ }
173
+ if (hasMaxAttachmentSizeError) {
174
+ errors.push(displayText.getAttachmentSizeErrorText(
175
+ // base64 size is about 137% that of the file size
176
+ // https://en.wikipedia.org/wiki/Base64#MIME
177
+ humanFileSize((maxAttachmentSize - 814) / 1.37, true)));
178
+ }
179
+ setError?.(errors.join(' '));
180
+ }
181
+ else {
182
+ setError?.(undefined);
183
+ }
184
+ setInput?.((prevValue) => ({
185
+ ...prevValue,
186
+ files: acceptedFiles,
187
+ }));
188
+ }, [setInput, input, displayText, maxAttachmentSize, maxAttachments, setError]);
160
189
  if (controls?.Form) {
161
- return (React__default.createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, allowAttachments: allowAttachments, isLoading: isLoading }));
190
+ return (React__default.createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, onValidate: onValidate, allowAttachments: allowAttachments, isLoading: isLoading, error: error, setError: setError }));
162
191
  }
163
192
  return (React__default.createElement("form", { className: `${FIELD_BLOCK}__form`, onSubmit: handleSubmit, method: "post", ref: ref },
164
193
  allowAttachments ? React__default.createElement(AttachFileControl, null) : null,
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { View, Button, VisuallyHidden, TextAreaField, DropZone } from '@aws-amplify/ui-react';
2
+ import { View, Button, VisuallyHidden, TextAreaField, Message, DropZone } from '@aws-amplify/ui-react';
3
3
  import { useIcons, IconSend, IconAttach } from '@aws-amplify/ui-react/internal';
4
4
  import { ComponentClassName } from '@aws-amplify/ui';
5
5
  import { Attachments } from './Attachments.mjs';
@@ -11,27 +11,24 @@ function isHTMLFormElement(target) {
11
11
  * Will conditionally render the DropZone if allowAttachments
12
12
  * is true
13
13
  */
14
- const FormWrapper = ({ children, allowAttachments, setInput, }) => {
14
+ const FormWrapper = ({ children, allowAttachments, onValidate, }) => {
15
15
  if (allowAttachments) {
16
16
  return (React.createElement(DropZone, { className: ComponentClassName.AIConversationFormDropzone, onDropComplete: ({ acceptedFiles }) => {
17
- setInput?.((prevInput) => ({
18
- ...prevInput,
19
- files: [...(prevInput?.files ?? []), ...acceptedFiles],
20
- }));
17
+ onValidate(acceptedFiles);
21
18
  } }, children));
22
19
  }
23
20
  else {
24
21
  return children;
25
22
  }
26
23
  };
27
- const Form = ({ setInput, input, handleSubmit, allowAttachments, isLoading, }) => {
24
+ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isLoading, error, }) => {
28
25
  const icons = useIcons('aiConversation');
29
26
  const sendIcon = icons?.send ?? React.createElement(IconSend, null);
30
27
  const attachIcon = icons?.attach ?? React.createElement(IconAttach, null);
31
28
  const hiddenInput = React.useRef(null);
32
29
  const [composing, setComposing] = React.useState(false);
33
30
  const isInputEmpty = !input?.text?.length && !input?.files?.length;
34
- return (React.createElement(FormWrapper, { allowAttachments: allowAttachments, setInput: setInput },
31
+ return (React.createElement(FormWrapper, { onValidate: onValidate, allowAttachments: allowAttachments },
35
32
  React.createElement(View, { as: "form", className: ComponentClassName.AIConversationForm, onSubmit: handleSubmit },
36
33
  allowAttachments ? (React.createElement(Button, { className: ComponentClassName.AIConversationFormAttach, onClick: () => {
37
34
  hiddenInput?.current?.click();
@@ -42,15 +39,11 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, isLoading, }) =
42
39
  React.createElement("span", null, attachIcon),
43
40
  React.createElement(VisuallyHidden, null,
44
41
  React.createElement("input", { type: "file", tabIndex: -1, ref: hiddenInput, onChange: (e) => {
45
- const { files } = e.target;
46
- if (!files || files.length === 0) {
42
+ if (!e.target.files || e.target.files.length === 0) {
47
43
  return;
48
44
  }
49
- setInput((prevValue) => ({
50
- ...prevValue,
51
- files: [...(prevValue?.files ?? []), ...Array.from(files)],
52
- }));
53
- }, multiple: true, accept: "*", "data-testid": "hidden-file-input" })))) : null,
45
+ onValidate(Array.from(e.target.files));
46
+ }, multiple: true, accept: ".jpeg,.png,.webp,.gif", "data-testid": "hidden-file-input" })))) : null,
54
47
  React.createElement(TextAreaField, { className: ComponentClassName.AIConversationFormField, label: "input", labelHidden: true, autoResize: true, flex: "1", rows: 1, value: input?.text ?? '', testId: "text-input", onCompositionStart: () => setComposing(true), onCompositionEnd: () => setComposing(false), onKeyDown: (e) => {
55
48
  // Submit on enter key if shift is not pressed also
56
49
  const shouldSubmit = !e.shiftKey && e.key === 'Enter' && !composing;
@@ -59,7 +52,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, isLoading, }) =
59
52
  e.preventDefault();
60
53
  }
61
54
  }, onChange: (e) => {
62
- setInput((prevValue) => ({
55
+ setInput?.((prevValue) => ({
63
56
  ...prevValue,
64
57
  text: e.target.value,
65
58
  }));
@@ -69,6 +62,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, isLoading, }) =
69
62
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
70
63
  isDisabled: isLoading || isInputEmpty },
71
64
  React.createElement("span", null, sendIcon))),
65
+ error ? (React.createElement(Message, { className: ComponentClassName.AIConversationFormError, variation: "plain", colorTheme: "warning" }, error)) : null,
72
66
  React.createElement(Attachments, { setInput: setInput, files: input?.files })));
73
67
  };
74
68
 
@@ -1,3 +1,3 @@
1
- const VERSION = '1.0.0';
1
+ const VERSION = '1.1.0';
2
2
 
3
3
  export { VERSION };
package/dist/index.js CHANGED
@@ -47,7 +47,8 @@ const AvatarsProvider = ({ children, avatars, }) => {
47
47
  const ConversationInputContext = React__namespace["default"].createContext({});
48
48
  const ConversationInputContextProvider = ({ children, }) => {
49
49
  const [input, setInput] = React__namespace["default"].useState();
50
- const providerValue = React__namespace["default"].useMemo(() => ({ input, setInput }), [input, setInput]);
50
+ const [error, setError] = React__namespace["default"].useState();
51
+ const providerValue = React__namespace["default"].useMemo(() => ({ input, setInput, error, setError }), [input, setInput, error, setError]);
51
52
  return (React__namespace["default"].createElement(ConversationInputContext.Provider, { value: providerValue }, children));
52
53
  };
53
54
 
@@ -82,32 +83,67 @@ function formatDate(date) {
82
83
  return `${dateString} at ${timeString}`;
83
84
  }
84
85
  function arrayBufferToBase64(buffer) {
85
- let binary = '';
86
- const bytes = new Uint8Array(buffer);
87
- const len = bytes.byteLength;
88
- for (let i = 0; i < len; i++) {
89
- binary += String.fromCharCode(bytes[i]);
90
- }
91
- return window.btoa(binary);
92
- }
93
- function convertBufferToBase64(buffer, format) {
94
- let base64string = '';
95
86
  // Use node-based buffer if available
96
87
  // fall back on browser if not
97
88
  if (typeof Buffer !== 'undefined') {
98
- base64string = Buffer.from(new Uint8Array(buffer)).toString('base64');
89
+ return Buffer.from(new Uint8Array(buffer)).toString('base64');
99
90
  }
100
91
  else {
101
- base64string = arrayBufferToBase64(buffer);
92
+ let binary = '';
93
+ const bytes = new Uint8Array(buffer);
94
+ const len = bytes.byteLength;
95
+ for (let i = 0; i < len; i++) {
96
+ binary += String.fromCharCode(bytes[i]);
97
+ }
98
+ return window.btoa(binary);
102
99
  }
100
+ }
101
+ function convertBufferToBase64(buffer, format) {
102
+ const base64string = arrayBufferToBase64(buffer);
103
103
  return `data:image/${format};base64,${base64string}`;
104
104
  }
105
105
  function getImageTypeFromMimeType(mimeType) {
106
106
  return mimeType.split('/')[1];
107
107
  }
108
+ async function attachmentsValidator({ files, maxAttachments, maxAttachmentSize, }) {
109
+ const acceptedFiles = [];
110
+ const rejectedFiles = [];
111
+ let hasMaxSizeError = false;
112
+ for (const file of files) {
113
+ const arrayBuffer = await file.arrayBuffer();
114
+ const base64 = arrayBufferToBase64(arrayBuffer);
115
+ if (base64.length < maxAttachmentSize) {
116
+ acceptedFiles.push(file);
117
+ }
118
+ else {
119
+ rejectedFiles.push(file);
120
+ hasMaxSizeError = true;
121
+ }
122
+ }
123
+ if (acceptedFiles.length > maxAttachments) {
124
+ return {
125
+ acceptedFiles: acceptedFiles.slice(0, maxAttachments),
126
+ rejectedFiles: [...acceptedFiles.slice(maxAttachments), ...rejectedFiles],
127
+ hasMaxAttachmentsError: true,
128
+ hasMaxAttachmentSizeError: hasMaxSizeError,
129
+ };
130
+ }
131
+ return {
132
+ acceptedFiles,
133
+ rejectedFiles,
134
+ hasMaxAttachmentsError: false,
135
+ hasMaxAttachmentSizeError: hasMaxSizeError,
136
+ };
137
+ }
108
138
 
109
139
  const defaultAIConversationDisplayTextEn = {
110
140
  getMessageTimestampText: (date) => formatDate(date),
141
+ getMaxAttachmentErrorText(count) {
142
+ return `Cannot choose more than ${count} ${count === 1 ? 'file' : 'files'}. `;
143
+ },
144
+ getAttachmentSizeErrorText(sizeText) {
145
+ return `File size must be below ${sizeText}.`;
146
+ },
111
147
  };
112
148
 
113
149
  const { ConversationDisplayTextContext, ConversationDisplayTextProvider, useConversationDisplayText, } = uiReactCore.createContextUtilities({
@@ -180,9 +216,18 @@ const { MessageRendererContext, MessageRendererProvider, useMessageRenderer, } =
180
216
  errorMessage: '`useMessageRenderer` must be used with an AIConversation component',
181
217
  });
182
218
 
183
- const AttachmentContext = React__namespace.createContext(false);
184
- const AttachmentProvider = ({ children, allowAttachments, }) => {
185
- return (React__namespace.createElement(AttachmentContext.Provider, { value: allowAttachments ?? false }, children));
219
+ const AttachmentContext = React__namespace.createContext({
220
+ allowAttachments: false,
221
+ // We save attachments as base64 strings into dynamodb for conversation history
222
+ // DynamoDB has a max size of 400kb for records
223
+ // This can be overridden so cutsomers could provide a lower number
224
+ // or a higher number if in the future we support larger sizes.
225
+ maxAttachmentSize: 400000,
226
+ maxAttachments: 20,
227
+ });
228
+ const AttachmentProvider = ({ children, allowAttachments = false, maxAttachmentSize = 400000, maxAttachments = 20, }) => {
229
+ const providerValue = React__namespace.useMemo(() => ({ maxAttachmentSize, maxAttachments, allowAttachments }), [maxAttachmentSize, maxAttachments, allowAttachments]);
230
+ return (React__namespace.createElement(AttachmentContext.Provider, { value: providerValue }, children));
186
231
  };
187
232
 
188
233
  const WelcomeMessageContext = React__namespace.createContext(undefined);
@@ -211,7 +256,7 @@ const DEFAULT_ICON_ATTRIBUTES = {
211
256
  fill: 'none',
212
257
  xmlns: 'http://www.w3.org/2000/svg',
213
258
  };
214
- const BaseIconElement = elements.defineBaseElement({
259
+ const BaseIconElement = elements.defineBaseElementWithRef({
215
260
  type: 'svg',
216
261
  displayName: 'Icon',
217
262
  });
@@ -227,44 +272,44 @@ const getIconProps = ({ variant, ...props }) => {
227
272
  };
228
273
  const IconElement = elements.withBaseElementProps(BaseIconElement, getIconProps);
229
274
 
230
- const LabelElement$1 = elements.defineBaseElement({
275
+ const LabelElement$1 = elements.defineBaseElementWithRef({
231
276
  type: 'label',
232
277
  displayName: 'Label',
233
278
  });
234
- const TextElement = elements.defineBaseElement({
279
+ const TextElement = elements.defineBaseElementWithRef({
235
280
  type: 'p',
236
281
  displayName: 'Text',
237
282
  });
238
- const UnorderedListElement = elements.defineBaseElement({
283
+ const UnorderedListElement = elements.defineBaseElementWithRef({
239
284
  type: 'ul',
240
285
  displayName: 'UnorderedList',
241
286
  });
242
- const ListItemElement = elements.defineBaseElement({
287
+ const ListItemElement = elements.defineBaseElementWithRef({
243
288
  type: 'li',
244
289
  displayName: 'ListItem',
245
290
  });
246
- const HeadingElement = elements.defineBaseElement({
291
+ const HeadingElement = elements.defineBaseElementWithRef({
247
292
  type: 'h2',
248
293
  displayName: 'Title',
249
294
  });
250
- const ImageElement = elements.defineBaseElement({
295
+ const ImageElement = elements.defineBaseElementWithRef({
251
296
  type: 'img',
252
297
  displayName: 'Image',
253
298
  });
254
- const InputElement = elements.defineBaseElement({
299
+ const InputElement = elements.defineBaseElementWithRef({
255
300
  type: 'input',
256
301
  displayName: 'Input',
257
302
  });
258
- const ButtonElement = elements.defineBaseElement({ type: 'button', displayName: 'Button' });
259
- const ViewElement = elements.defineBaseElement({
303
+ const ButtonElement = elements.defineBaseElementWithRef({ type: 'button', displayName: 'Button' });
304
+ const ViewElement = elements.defineBaseElementWithRef({
260
305
  type: 'div',
261
306
  displayName: 'View',
262
307
  });
263
- const SpanElement = elements.defineBaseElement({
308
+ const SpanElement = elements.defineBaseElementWithRef({
264
309
  type: 'span',
265
310
  displayName: 'Span',
266
311
  });
267
- const TextAreaElement = elements.defineBaseElement({
312
+ const TextAreaElement = elements.defineBaseElementWithRef({
268
313
  type: 'textarea',
269
314
  displayName: 'TextArea',
270
315
  });
@@ -561,9 +606,10 @@ const InputContainer = elements.withBaseElementProps(View$2, {
561
606
  className: `${FIELD_BLOCK}__input-container`,
562
607
  });
563
608
  const FormControl = () => {
564
- const { input, setInput } = React__namespace["default"].useContext(ConversationInputContext);
609
+ const { input, setInput, error, setError } = React__namespace["default"].useContext(ConversationInputContext);
565
610
  const handleSendMessage = React__namespace["default"].useContext(SendMessageContext);
566
- const allowAttachments = React__namespace["default"].useContext(AttachmentContext);
611
+ const { allowAttachments, maxAttachmentSize, maxAttachments } = React__namespace["default"].useContext(AttachmentContext);
612
+ const displayText = useConversationDisplayText();
567
613
  const responseComponents = React__namespace["default"].useContext(ResponseComponentsContext);
568
614
  const isLoading = React__namespace["default"].useContext(LoadingContext);
569
615
  const aiContext = React__namespace["default"].useContext(AIContextContext);
@@ -615,8 +661,36 @@ const FormControl = () => {
615
661
  }
616
662
  }
617
663
  };
664
+ const onValidate = React__namespace["default"].useCallback(async (files) => {
665
+ const previousFiles = input?.files ?? [];
666
+ const { acceptedFiles, hasMaxAttachmentsError, hasMaxAttachmentSizeError, } = await attachmentsValidator({
667
+ files: [...files, ...previousFiles],
668
+ maxAttachments,
669
+ maxAttachmentSize,
670
+ });
671
+ if (hasMaxAttachmentsError || hasMaxAttachmentSizeError) {
672
+ const errors = [];
673
+ if (hasMaxAttachmentsError) {
674
+ errors.push(displayText.getMaxAttachmentErrorText(maxAttachments));
675
+ }
676
+ if (hasMaxAttachmentSizeError) {
677
+ errors.push(displayText.getAttachmentSizeErrorText(
678
+ // base64 size is about 137% that of the file size
679
+ // https://en.wikipedia.org/wiki/Base64#MIME
680
+ ui.humanFileSize((maxAttachmentSize - 814) / 1.37, true)));
681
+ }
682
+ setError?.(errors.join(' '));
683
+ }
684
+ else {
685
+ setError?.(undefined);
686
+ }
687
+ setInput?.((prevValue) => ({
688
+ ...prevValue,
689
+ files: acceptedFiles,
690
+ }));
691
+ }, [setInput, input, displayText, maxAttachmentSize, maxAttachments, setError]);
618
692
  if (controls?.Form) {
619
- return (React__namespace["default"].createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, allowAttachments: allowAttachments, isLoading: isLoading }));
693
+ return (React__namespace["default"].createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, onValidate: onValidate, allowAttachments: allowAttachments, isLoading: isLoading, error: error, setError: setError }));
620
694
  }
621
695
  return (React__namespace["default"].createElement("form", { className: `${FIELD_BLOCK}__form`, onSubmit: handleSubmit, method: "post", ref: ref },
622
696
  allowAttachments ? React__namespace["default"].createElement(AttachFileControl, null) : null,
@@ -795,7 +869,7 @@ PromptControl.Container = Container;
795
869
  PromptControl.PromptGroup = PromptGroup;
796
870
  PromptControl.PromptCard = PromptCard;
797
871
 
798
- const AIConversationProvider = ({ aiContext, actions, allowAttachments, avatars, children, controls, displayText, handleSendMessage, isLoading, messages, messageRenderer, responseComponents, suggestedPrompts, variant, welcomeMessage, FallbackResponseComponent, }) => {
872
+ const AIConversationProvider = ({ aiContext, actions, allowAttachments, avatars, children, controls, displayText, handleSendMessage, isLoading, maxAttachmentSize, maxAttachments, messages, messageRenderer, responseComponents, suggestedPrompts, variant, welcomeMessage, FallbackResponseComponent, }) => {
799
873
  const _displayText = {
800
874
  ...defaultAIConversationDisplayTextEn,
801
875
  ...displayText,
@@ -806,7 +880,7 @@ const AIConversationProvider = ({ aiContext, actions, allowAttachments, avatars,
806
880
  React__namespace["default"].createElement(FallbackComponentProvider, { FallbackComponent: FallbackResponseComponent },
807
881
  React__namespace["default"].createElement(MessageRendererProvider, { ...messageRenderer },
808
882
  React__namespace["default"].createElement(ResponseComponentsProvider, { responseComponents: responseComponents },
809
- React__namespace["default"].createElement(AttachmentProvider, { allowAttachments: allowAttachments },
883
+ React__namespace["default"].createElement(AttachmentProvider, { allowAttachments: allowAttachments, maxAttachmentSize: maxAttachmentSize, maxAttachments: maxAttachments },
810
884
  React__namespace["default"].createElement(ConversationDisplayTextProvider, { ..._displayText },
811
885
  React__namespace["default"].createElement(ConversationInputContextProvider, null,
812
886
  React__namespace["default"].createElement(SendMessageContextProvider, { handleSendMessage: handleSendMessage },
@@ -950,27 +1024,24 @@ function isHTMLFormElement(target) {
950
1024
  * Will conditionally render the DropZone if allowAttachments
951
1025
  * is true
952
1026
  */
953
- const FormWrapper = ({ children, allowAttachments, setInput, }) => {
1027
+ const FormWrapper = ({ children, allowAttachments, onValidate, }) => {
954
1028
  if (allowAttachments) {
955
1029
  return (React__namespace.createElement(uiReact.DropZone, { className: ui.ComponentClassName.AIConversationFormDropzone, onDropComplete: ({ acceptedFiles }) => {
956
- setInput?.((prevInput) => ({
957
- ...prevInput,
958
- files: [...(prevInput?.files ?? []), ...acceptedFiles],
959
- }));
1030
+ onValidate(acceptedFiles);
960
1031
  } }, children));
961
1032
  }
962
1033
  else {
963
1034
  return children;
964
1035
  }
965
1036
  };
966
- const Form = ({ setInput, input, handleSubmit, allowAttachments, isLoading, }) => {
1037
+ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isLoading, error, }) => {
967
1038
  const icons = internal.useIcons('aiConversation');
968
1039
  const sendIcon = icons?.send ?? React__namespace.createElement(internal.IconSend, null);
969
1040
  const attachIcon = icons?.attach ?? React__namespace.createElement(internal.IconAttach, null);
970
1041
  const hiddenInput = React__namespace.useRef(null);
971
1042
  const [composing, setComposing] = React__namespace.useState(false);
972
1043
  const isInputEmpty = !input?.text?.length && !input?.files?.length;
973
- return (React__namespace.createElement(FormWrapper, { allowAttachments: allowAttachments, setInput: setInput },
1044
+ return (React__namespace.createElement(FormWrapper, { onValidate: onValidate, allowAttachments: allowAttachments },
974
1045
  React__namespace.createElement(uiReact.View, { as: "form", className: ui.ComponentClassName.AIConversationForm, onSubmit: handleSubmit },
975
1046
  allowAttachments ? (React__namespace.createElement(uiReact.Button, { className: ui.ComponentClassName.AIConversationFormAttach, onClick: () => {
976
1047
  hiddenInput?.current?.click();
@@ -981,15 +1052,11 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, isLoading, }) =
981
1052
  React__namespace.createElement("span", null, attachIcon),
982
1053
  React__namespace.createElement(uiReact.VisuallyHidden, null,
983
1054
  React__namespace.createElement("input", { type: "file", tabIndex: -1, ref: hiddenInput, onChange: (e) => {
984
- const { files } = e.target;
985
- if (!files || files.length === 0) {
1055
+ if (!e.target.files || e.target.files.length === 0) {
986
1056
  return;
987
1057
  }
988
- setInput((prevValue) => ({
989
- ...prevValue,
990
- files: [...(prevValue?.files ?? []), ...Array.from(files)],
991
- }));
992
- }, multiple: true, accept: "*", "data-testid": "hidden-file-input" })))) : null,
1058
+ onValidate(Array.from(e.target.files));
1059
+ }, multiple: true, accept: ".jpeg,.png,.webp,.gif", "data-testid": "hidden-file-input" })))) : null,
993
1060
  React__namespace.createElement(uiReact.TextAreaField, { className: ui.ComponentClassName.AIConversationFormField, label: "input", labelHidden: true, autoResize: true, flex: "1", rows: 1, value: input?.text ?? '', testId: "text-input", onCompositionStart: () => setComposing(true), onCompositionEnd: () => setComposing(false), onKeyDown: (e) => {
994
1061
  // Submit on enter key if shift is not pressed also
995
1062
  const shouldSubmit = !e.shiftKey && e.key === 'Enter' && !composing;
@@ -998,7 +1065,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, isLoading, }) =
998
1065
  e.preventDefault();
999
1066
  }
1000
1067
  }, onChange: (e) => {
1001
- setInput((prevValue) => ({
1068
+ setInput?.((prevValue) => ({
1002
1069
  ...prevValue,
1003
1070
  text: e.target.value,
1004
1071
  }));
@@ -1008,6 +1075,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, isLoading, }) =
1008
1075
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1009
1076
  isDisabled: isLoading || isInputEmpty },
1010
1077
  React__namespace.createElement("span", null, sendIcon))),
1078
+ error ? (React__namespace.createElement(uiReact.Message, { className: ui.ComponentClassName.AIConversationFormError, variation: "plain", colorTheme: "warning" }, error)) : null,
1011
1079
  React__namespace.createElement(Attachments, { setInput: setInput, files: input?.files })));
1012
1080
  };
1013
1081
 
@@ -1022,7 +1090,7 @@ const PromptList = ({ setInput, suggestedPrompts = [], }) => {
1022
1090
  })));
1023
1091
  };
1024
1092
 
1025
- const VERSION = '1.0.0';
1093
+ const VERSION = '1.1.0';
1026
1094
 
1027
1095
  function AIConversationBase({ avatars, controls, ...rest }) {
1028
1096
  uiReactCore.useSetUserAgent({
@@ -3,4 +3,4 @@ import { AIConversationInput, AIConversationProps } from './types';
3
3
  export interface AIConversationProviderProps extends AIConversationInput, AIConversationProps {
4
4
  children?: React.ReactNode;
5
5
  }
6
- export declare const AIConversationProvider: ({ aiContext, actions, allowAttachments, avatars, children, controls, displayText, handleSendMessage, isLoading, messages, messageRenderer, responseComponents, suggestedPrompts, variant, welcomeMessage, FallbackResponseComponent, }: AIConversationProviderProps) => React.JSX.Element;
6
+ export declare const AIConversationProvider: ({ aiContext, actions, allowAttachments, avatars, children, controls, displayText, handleSendMessage, isLoading, maxAttachmentSize, maxAttachments, messages, messageRenderer, responseComponents, suggestedPrompts, variant, welcomeMessage, FallbackResponseComponent, }: AIConversationProviderProps) => React.JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
- export declare const AttachmentContext: React.Context<boolean>;
3
- export declare const AttachmentProvider: ({ children, allowAttachments, }: {
4
- children?: React.ReactNode;
5
- allowAttachments?: boolean | undefined;
6
- }) => JSX.Element;
2
+ import { AIConversationInput } from '../types';
3
+ export interface AttachmentContextProps extends Pick<AIConversationInput, 'allowAttachments' | 'maxAttachments' | 'maxAttachmentSize'> {
4
+ }
5
+ export declare const AttachmentContext: React.Context<Required<AttachmentContextProps>>;
6
+ export declare const AttachmentProvider: ({ children, allowAttachments, maxAttachmentSize, maxAttachments, }: React.PropsWithChildren<AttachmentContextProps>) => JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { ConversationInputContext } from './ConversationInputContext';
2
+ import { ConversationInputContextProps } from './ConversationInputContext';
3
3
  import { SuggestedPrompt } from '../types';
4
4
  import { ConversationMessage } from '../../../types';
5
5
  export interface ControlsContextProps {
@@ -7,13 +7,14 @@ export interface ControlsContextProps {
7
7
  handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
8
8
  allowAttachments?: boolean;
9
9
  isLoading?: boolean;
10
- } & Required<ConversationInputContext>>;
10
+ onValidate: (files: File[]) => Promise<void>;
11
+ } & ConversationInputContextProps>;
11
12
  MessageList?: React.ComponentType<{
12
13
  messages: ConversationMessage[];
13
14
  }>;
14
15
  PromptList?: React.ComponentType<{
15
16
  suggestedPrompts?: SuggestedPrompt[];
16
- setInput: ConversationInputContext['setInput'];
17
+ setInput: ConversationInputContextProps['setInput'];
17
18
  }>;
18
19
  }
19
20
  export declare const ControlsContext: React.Context<ControlsContextProps | undefined>;
@@ -3,11 +3,13 @@ export interface ConversationInput {
3
3
  text?: string;
4
4
  files?: File[];
5
5
  }
6
- export interface ConversationInputContext {
6
+ export interface ConversationInputContextProps {
7
7
  input?: ConversationInput;
8
8
  setInput?: React.Dispatch<React.SetStateAction<ConversationInput | undefined>>;
9
+ error?: string;
10
+ setError?: React.Dispatch<React.SetStateAction<string | undefined>>;
9
11
  }
10
- export declare const ConversationInputContext: React.Context<ConversationInputContext>;
12
+ export declare const ConversationInputContext: React.Context<ConversationInputContextProps>;
11
13
  export declare const ConversationInputContextProvider: ({ children, }: {
12
14
  children?: React.ReactNode;
13
15
  }) => JSX.Element;
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
2
  import { ConversationDisplayText } from '../displayText';
3
- export declare const ConversationDisplayTextContext: import("react").Context<Required<ConversationDisplayText> | undefined>, ConversationDisplayTextProvider: import("react").ComponentType<import("react").PropsWithChildren<Required<ConversationDisplayText>>>, useConversationDisplayText: (params?: {
3
+ export declare const ConversationDisplayTextContext: import("react").Context<Required<ConversationDisplayText>>, ConversationDisplayTextProvider: import("react").ComponentType<import("react").PropsWithChildren<Required<ConversationDisplayText>>>, useConversationDisplayText: (params?: {
4
4
  errorMessage?: string | undefined;
5
5
  } | undefined) => Required<ConversationDisplayText>;
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
2
  import { MessageRenderer } from '../types';
3
- export declare const MessageRendererContext: import("react").Context<MessageRenderer | undefined>, MessageRendererProvider: import("react").ComponentType<import("react").PropsWithChildren<MessageRenderer>>, useMessageRenderer: (params?: {
3
+ export declare const MessageRendererContext: import("react").Context<MessageRenderer>, MessageRendererProvider: import("react").ComponentType<import("react").PropsWithChildren<MessageRenderer>>, useMessageRenderer: (params?: {
4
4
  errorMessage?: string | undefined;
5
5
  } | undefined) => MessageRenderer;
@@ -2,5 +2,5 @@ import React from 'react';
2
2
  export type IconElementProps = React.ComponentProps<typeof BaseIconElement>;
3
3
  export type IconVariant = 'attach' | 'close' | 'image' | 'send-message' | 'user-avatar';
4
4
  export declare const DEFAULT_ICON_PATHS: Record<IconVariant, string>;
5
- export declare const BaseIconElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<never, IconVariant, React.SVGProps<SVGSVGElement>>, SVGSVGElement>;
6
- export declare const IconElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<Omit<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<never, IconVariant, React.SVGProps<SVGSVGElement>>, "ref"> & React.RefAttributes<SVGSVGElement>, SVGSVGElement>;
5
+ export declare const BaseIconElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<never, IconVariant, React.SVGProps<SVGSVGElement>>, SVGSVGElement>;
6
+ export declare const IconElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<Omit<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<never, IconVariant, React.SVGProps<SVGSVGElement>>, "ref"> & React.RefAttributes<SVGSVGElement>, SVGSVGElement>;
@@ -14,22 +14,22 @@ export interface AIConversationElements {
14
14
  UnorderedList: typeof UnorderedListElement;
15
15
  View: typeof ViewElement;
16
16
  }
17
- export declare const LabelElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<"htmlFor", string, import("react").DetailedHTMLProps<import("react").LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>>, HTMLLabelElement>;
18
- export declare const TextElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<never, string, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>>, HTMLParagraphElement>;
19
- export declare const UnorderedListElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<never, string, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLUListElement>, HTMLUListElement>>, HTMLUListElement>;
20
- export declare const ListItemElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<never, string, import("react").DetailedHTMLProps<import("react").LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>>, HTMLLIElement>;
21
- export declare const HeadingElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<never, string, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>>, HTMLHeadingElement>;
17
+ export declare const LabelElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<"htmlFor", string, import("react").DetailedHTMLProps<import("react").LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>>, HTMLLabelElement>;
18
+ export declare const TextElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<never, string, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>>, HTMLParagraphElement>;
19
+ export declare const UnorderedListElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<never, string, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLUListElement>, HTMLUListElement>>, HTMLUListElement>;
20
+ export declare const ListItemElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<never, string, import("react").DetailedHTMLProps<import("react").LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>>, HTMLLIElement>;
21
+ export declare const HeadingElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<never, string, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>>, HTMLHeadingElement>;
22
22
  export type IconElementProps = React.ComponentProps<typeof IconElement>;
23
23
  type ImageElementProps = 'src' | 'alt';
24
- export declare const ImageElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<ImageElementProps, string, import("react").DetailedHTMLProps<import("react").ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>>, HTMLImageElement>;
25
- export declare const InputElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<"type", string, import("react").DetailedHTMLProps<import("react").InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>>, HTMLInputElement>;
24
+ export declare const ImageElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<ImageElementProps, string, import("react").DetailedHTMLProps<import("react").ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>>, HTMLImageElement>;
25
+ export declare const InputElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<"type", string, import("react").DetailedHTMLProps<import("react").InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>>, HTMLInputElement>;
26
26
  type ButtonElementProps = 'disabled' | 'onClick' | 'type' | 'tabIndex';
27
27
  type ButtonElementVariant = 'attach' | 'remove' | 'send-message';
28
- export declare const ButtonElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<ButtonElementProps, ButtonElementVariant, import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>>, HTMLButtonElement>;
28
+ export declare const ButtonElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<ButtonElementProps, ButtonElementVariant, import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>>, HTMLButtonElement>;
29
29
  type ViewElementProps = 'onFocus' | 'tabIndex' | 'onKeyDown';
30
- export declare const ViewElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<ViewElementProps, string, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>>, HTMLDivElement>;
31
- export declare const SpanElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<never, string, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>, HTMLSpanElement>;
30
+ export declare const ViewElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<ViewElementProps, string, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>>, HTMLDivElement>;
31
+ export declare const SpanElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<never, string, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>, HTMLSpanElement>;
32
32
  type TextAreaElementProps = 'id' | 'name' | 'onChange' | 'placeholder' | 'autoFocus' | 'onCompositionStart' | 'onCompositionEnd' | 'onKeyDown';
33
- export declare const TextAreaElement: import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElement<import("@aws-amplify/ui-react-core/dist/types/elements/types").BaseElementProps<TextAreaElementProps, string, import("react").DetailedHTMLProps<import("react").TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>>, HTMLTextAreaElement>;
33
+ export declare const TextAreaElement: import("@aws-amplify/ui-react-core/elements").BaseElementWithRef<import("@aws-amplify/ui-react-core/elements").BaseElementWithRefProps<TextAreaElementProps, string, import("react").DetailedHTMLProps<import("react").TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>>, HTMLTextAreaElement>;
34
34
  export declare const AIConversationElements: AIConversationElements;
35
35
  export {};
@@ -1,7 +1,7 @@
1
1
  export { AIContextContext, AIContextProvider } from './AIContextContext';
2
2
  export { ActionsContext, ActionsProvider } from './ActionsContext';
3
3
  export { AvatarsContext, AvatarsProvider } from './AvatarsContext';
4
- export { ConversationInputContext, ConversationInput, ConversationInputContextProvider, } from './ConversationInputContext';
4
+ export { ConversationInputContextProps, ConversationInputContext, ConversationInput, ConversationInputContextProvider, } from './ConversationInputContext';
5
5
  export { MessagesContext, RoleContext, MessagesProvider, } from './MessagesContext';
6
6
  export { SuggestedPromptsContext, SuggestedPromptProvider, } from './SuggestedPromptsContext';
7
7
  export { MessageVariantContext, MessageVariantProvider, } from './MessageVariantContext';
@@ -11,7 +11,7 @@ export { LoadingContextProvider } from './LoadingContext';
11
11
  export { ResponseComponentsProvider, RESPONSE_COMPONENT_PREFIX, } from './ResponseComponentsContext';
12
12
  export { SendMessageContextProvider } from './SendMessageContext';
13
13
  export { MessageRendererProvider, MessageRendererContext, useMessageRenderer, } from './MessageRenderContext';
14
- export { AttachmentProvider, AttachmentContext } from './AttachmentContext';
14
+ export { AttachmentProvider, AttachmentContext, AttachmentContextProps, } from './AttachmentContext';
15
15
  export { WelcomeMessageContext, WelcomeMessageProvider, } from './WelcomeMessageContext';
16
16
  export { FallbackComponentContext, FallbackComponentProvider, } from './FallbackComponentContext';
17
17
  export * from './elements';
@@ -1,6 +1,8 @@
1
1
  import { DisplayTextTemplate } from '@aws-amplify/ui';
2
2
  export type ConversationDisplayText = {
3
3
  getMessageTimestampText?: (date: Date) => string;
4
+ getMaxAttachmentErrorText?: (count: number) => string;
5
+ getAttachmentSizeErrorText?: (sizeText: string) => string;
4
6
  };
5
7
  export declare const defaultAIConversationDisplayTextEn: Required<AIConversationDisplayText>;
6
8
  export type AIConversationDisplayText = DisplayTextTemplate<ConversationDisplayText>;
@@ -22,6 +22,8 @@ export interface AIConversationInput {
22
22
  variant?: MessageVariant;
23
23
  controls?: ControlsContextProps;
24
24
  allowAttachments?: boolean;
25
+ maxAttachments?: number;
26
+ maxAttachmentSize?: number;
25
27
  messageRenderer?: MessageRenderer;
26
28
  }
27
29
  export interface AIConversationProps {
@@ -2,3 +2,13 @@ import { ImageContentBlock } from '../../types';
2
2
  export declare function formatDate(date: Date): string;
3
3
  export declare function convertBufferToBase64(buffer: ArrayBuffer, format: ImageContentBlock['format']): string;
4
4
  export declare function getImageTypeFromMimeType(mimeType: string): 'png' | 'jpeg' | 'gif' | 'webp';
5
+ export declare function attachmentsValidator({ files, maxAttachments, maxAttachmentSize, }: {
6
+ files: File[];
7
+ maxAttachments: number;
8
+ maxAttachmentSize: number;
9
+ }): Promise<{
10
+ acceptedFiles: File[];
11
+ rejectedFiles: File[];
12
+ hasMaxAttachmentSizeError: boolean;
13
+ hasMaxAttachmentsError: boolean;
14
+ }>;
@@ -1,6 +1,6 @@
1
1
  /// <reference types="react" />
2
- import { ConversationInputContext } from '../../context';
2
+ import { ConversationInputContextProps } from '../../context';
3
3
  export declare const Attachments: ({ files, setInput, }: {
4
4
  files?: File[] | undefined;
5
- setInput: ConversationInputContext['setInput'];
5
+ setInput: ConversationInputContextProps['setInput'];
6
6
  }) => JSX.Element | null;
@@ -1 +1 @@
1
- export declare const VERSION = "1.0.0";
1
+ export declare const VERSION = "1.1.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws-amplify/ui-react-ai",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/esm/index.mjs",
6
6
  "exports": {
@@ -42,15 +42,15 @@
42
42
  "typecheck": "tsc --noEmit"
43
43
  },
44
44
  "peerDependencies": {
45
- "@aws-amplify/api-graphql": "^4.3.0",
46
- "aws-amplify": "^6.6.5",
45
+ "@aws-amplify/api-graphql": "unstable",
46
+ "aws-amplify": "^6.9.0",
47
47
  "react": "^16.14.0 || ^17.0 || ^18.0",
48
48
  "react-dom": "^16.14.0 || ^17.0 || ^18.0"
49
49
  },
50
50
  "dependencies": {
51
- "@aws-amplify/ui": "^6.6.6",
52
- "@aws-amplify/ui-react": "^6.6.0",
53
- "@aws-amplify/ui-react-core": "^3.0.30"
51
+ "@aws-amplify/ui": "^6.7.1",
52
+ "@aws-amplify/ui-react": "^6.7.1",
53
+ "@aws-amplify/ui-react-core": "^3.1.1"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/jest-when": "^3.5.0",