@aws-amplify/ui-react-ai 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/AIConversation/displayText.mjs +3 -0
- package/dist/esm/components/AIConversation/utils.mjs +58 -7
- package/dist/esm/components/AIConversation/views/Controls/FormControl.mjs +49 -18
- package/dist/esm/components/AIConversation/views/Controls/MessagesControl.mjs +17 -2
- package/dist/esm/components/AIConversation/views/default/Attachments.mjs +6 -2
- package/dist/esm/components/AIConversation/views/default/Form.mjs +5 -6
- package/dist/esm/components/AIConversation/views/default/MessageList.mjs +1 -0
- package/dist/esm/hooks/useAIConversation.mjs +18 -14
- package/dist/esm/version.mjs +1 -1
- package/dist/index.js +150 -45
- package/dist/types/components/AIConversation/displayText.d.ts +1 -0
- package/dist/types/components/AIConversation/utils.d.ts +9 -2
- package/dist/types/components/AIConversation/views/Controls/MessagesControl.d.ts +1 -0
- package/dist/types/hooks/shared.d.ts +8 -0
- package/dist/types/hooks/useAIGeneration.d.ts +2 -2
- package/dist/types/types.d.ts +4 -3
- package/dist/types/version.d.ts +1 -1
- package/package.json +7 -11
|
@@ -8,6 +8,9 @@ const defaultAIConversationDisplayTextEn = {
|
|
|
8
8
|
getAttachmentSizeErrorText(sizeText) {
|
|
9
9
|
return `File size must be below ${sizeText}.`;
|
|
10
10
|
},
|
|
11
|
+
getAttachmentFormatErrorText(formats) {
|
|
12
|
+
return `Files must be one of the supported types: ${formats.join(', ')}.`;
|
|
13
|
+
},
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
export { defaultAIConversationDisplayTextEn };
|
|
@@ -31,14 +31,63 @@ function convertBufferToBase64(buffer, format) {
|
|
|
31
31
|
const base64string = arrayBufferToBase64(buffer);
|
|
32
32
|
return `data:image/${format};base64,${base64string}`;
|
|
33
33
|
}
|
|
34
|
-
function
|
|
35
|
-
|
|
34
|
+
// This function will return the file extension or mime type
|
|
35
|
+
function getAttachmentFormat(file) {
|
|
36
|
+
const fileNameParts = file.name.split('.');
|
|
37
|
+
// try to get format from extension first
|
|
38
|
+
// this is because some document mime file types are very complex
|
|
39
|
+
// and don't easily map to what Bedrock needs like "doc" or "docx"
|
|
40
|
+
if (fileNameParts.length > 1) {
|
|
41
|
+
return fileNameParts[fileNameParts.length - 1];
|
|
42
|
+
}
|
|
43
|
+
// return mime type if no extension exists
|
|
44
|
+
return file.type.split('/')[1];
|
|
45
|
+
}
|
|
46
|
+
// This will take a File and return a string to be used as the document name
|
|
47
|
+
// in Bedrock. Bedrock has specific requirements around a valid name:
|
|
48
|
+
// https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_DocumentBlock.html
|
|
49
|
+
function getValidDocumentName(file) {
|
|
50
|
+
const fileNameParts = file.name.split('.');
|
|
51
|
+
const baseFileName = fileNameParts.length > 1 ? fileNameParts.slice(0, -1).join('') : file.name;
|
|
52
|
+
// This is a regex to target special characters that are not allowed in file names
|
|
53
|
+
const specialCharactersRegex = /[!@#$%^&*()+\-=[\]{};':"\\|,.<>/?]/g;
|
|
54
|
+
return baseFileName.replace(specialCharactersRegex, '').replace(/\s+/g, '_');
|
|
55
|
+
}
|
|
56
|
+
// Using Sets instead of Arrays for faster and easier lookups
|
|
57
|
+
const documentFileTypes = new Set([
|
|
58
|
+
'docx',
|
|
59
|
+
'csv',
|
|
60
|
+
'html',
|
|
61
|
+
'txt',
|
|
62
|
+
'pdf',
|
|
63
|
+
'md',
|
|
64
|
+
'doc',
|
|
65
|
+
'xlsx',
|
|
66
|
+
'xls',
|
|
67
|
+
]);
|
|
68
|
+
const imageFileTypes = new Set(['png', 'jpeg', 'gif', 'webp']);
|
|
69
|
+
const validFileTypes = new Set([
|
|
70
|
+
...documentFileTypes,
|
|
71
|
+
...imageFileTypes,
|
|
72
|
+
]);
|
|
73
|
+
function isDocumentFormat(format) {
|
|
74
|
+
return documentFileTypes.has(format);
|
|
75
|
+
}
|
|
76
|
+
function isImageFormat(format) {
|
|
77
|
+
return imageFileTypes.has(format);
|
|
36
78
|
}
|
|
37
79
|
async function attachmentsValidator({ files, maxAttachments, maxAttachmentSize, }) {
|
|
38
80
|
const acceptedFiles = [];
|
|
39
81
|
const rejectedFiles = [];
|
|
40
|
-
let
|
|
82
|
+
let hasMaxAttachmentSizeError = false;
|
|
83
|
+
let hasUnsupportedFileError = false;
|
|
41
84
|
for (const file of files) {
|
|
85
|
+
const format = getAttachmentFormat(file);
|
|
86
|
+
if (!validFileTypes.has(format)) {
|
|
87
|
+
rejectedFiles.push(file);
|
|
88
|
+
hasUnsupportedFileError = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
42
91
|
const arrayBuffer = await file.arrayBuffer();
|
|
43
92
|
const base64 = arrayBufferToBase64(arrayBuffer);
|
|
44
93
|
if (base64.length < maxAttachmentSize) {
|
|
@@ -46,23 +95,25 @@ async function attachmentsValidator({ files, maxAttachments, maxAttachmentSize,
|
|
|
46
95
|
}
|
|
47
96
|
else {
|
|
48
97
|
rejectedFiles.push(file);
|
|
49
|
-
|
|
98
|
+
hasMaxAttachmentSizeError = true;
|
|
50
99
|
}
|
|
51
100
|
}
|
|
52
101
|
if (acceptedFiles.length > maxAttachments) {
|
|
53
102
|
return {
|
|
54
103
|
acceptedFiles: acceptedFiles.slice(0, maxAttachments),
|
|
55
104
|
rejectedFiles: [...acceptedFiles.slice(maxAttachments), ...rejectedFiles],
|
|
105
|
+
hasMaxAttachmentSizeError,
|
|
106
|
+
hasUnsupportedFileError,
|
|
56
107
|
hasMaxAttachmentsError: true,
|
|
57
|
-
hasMaxAttachmentSizeError: hasMaxSizeError,
|
|
58
108
|
};
|
|
59
109
|
}
|
|
60
110
|
return {
|
|
61
111
|
acceptedFiles,
|
|
62
112
|
rejectedFiles,
|
|
113
|
+
hasMaxAttachmentSizeError,
|
|
114
|
+
hasUnsupportedFileError,
|
|
63
115
|
hasMaxAttachmentsError: false,
|
|
64
|
-
hasMaxAttachmentSizeError: hasMaxSizeError,
|
|
65
116
|
};
|
|
66
117
|
}
|
|
67
118
|
|
|
68
|
-
export { attachmentsValidator, convertBufferToBase64, formatDate,
|
|
119
|
+
export { attachmentsValidator, convertBufferToBase64, documentFileTypes, formatDate, getAttachmentFormat, getValidDocumentName, imageFileTypes, isDocumentFormat, isImageFormat, validFileTypes };
|
|
@@ -19,7 +19,7 @@ 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 { attachmentsValidator,
|
|
22
|
+
import { attachmentsValidator, validFileTypes, getAttachmentFormat, isDocumentFormat, getValidDocumentName, isImageFormat } from '../../utils.mjs';
|
|
23
23
|
import { humanFileSize, isFunction } from '@aws-amplify/ui';
|
|
24
24
|
|
|
25
25
|
const { Button, Icon, Label: LabelElement, TextArea, View, } = AIConversationElements;
|
|
@@ -102,6 +102,8 @@ const TextInput = React__default.forwardRef(function TextInput(props, ref) {
|
|
|
102
102
|
const InputContainer = withBaseElementProps(View, {
|
|
103
103
|
className: `${FIELD_BLOCK}__input-container`,
|
|
104
104
|
});
|
|
105
|
+
const isConversationInputWithText = (input) => !!input?.text;
|
|
106
|
+
const isConversationInputWithFiles = (input) => !!input?.files?.length;
|
|
105
107
|
const FormControl = () => {
|
|
106
108
|
const { input, setInput, error, setError } = React__default.useContext(ConversationInputContext);
|
|
107
109
|
const handleSendMessage = React__default.useContext(SendMessageContext);
|
|
@@ -113,25 +115,46 @@ const FormControl = () => {
|
|
|
113
115
|
const ref = React__default.useRef(null);
|
|
114
116
|
const controls = React__default.useContext(ControlsContext);
|
|
115
117
|
const [composing, setComposing] = React__default.useState(false);
|
|
118
|
+
const [isSubmitting, setIsSubmitting] = React__default.useState(false);
|
|
119
|
+
const isInputText = isConversationInputWithText(input);
|
|
120
|
+
// an empty array will resolve false when evaluating the length
|
|
121
|
+
const isInputFiles = isConversationInputWithFiles(input);
|
|
116
122
|
const submitMessage = async () => {
|
|
117
|
-
|
|
123
|
+
const hasInput = isInputFiles || isInputText;
|
|
124
|
+
// Prevent double submission and empty submission
|
|
125
|
+
if (isSubmitting || !hasInput) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
setIsSubmitting(true);
|
|
118
129
|
const submittedContent = [];
|
|
119
|
-
if (
|
|
130
|
+
if (isInputText) {
|
|
120
131
|
const textContent = {
|
|
121
132
|
text: input.text,
|
|
122
133
|
};
|
|
123
134
|
submittedContent.push(textContent);
|
|
124
135
|
}
|
|
125
|
-
if (
|
|
136
|
+
if (isInputFiles) {
|
|
126
137
|
for (const file of input.files) {
|
|
127
138
|
const buffer = await file.arrayBuffer();
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
const format = getAttachmentFormat(file);
|
|
140
|
+
const source = { bytes: new Uint8Array(buffer) };
|
|
141
|
+
if (isDocumentFormat(format)) {
|
|
142
|
+
submittedContent.push({
|
|
143
|
+
document: {
|
|
144
|
+
name: getValidDocumentName(file),
|
|
145
|
+
format,
|
|
146
|
+
source,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else if (isImageFormat(format)) {
|
|
151
|
+
submittedContent.push({
|
|
152
|
+
image: {
|
|
153
|
+
format,
|
|
154
|
+
source,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
135
158
|
}
|
|
136
159
|
}
|
|
137
160
|
if (handleSendMessage) {
|
|
@@ -141,6 +164,12 @@ const FormControl = () => {
|
|
|
141
164
|
toolConfiguration: convertResponseComponentsToToolConfiguration(responseComponents),
|
|
142
165
|
});
|
|
143
166
|
}
|
|
167
|
+
// Clear the attachment errors when submitting
|
|
168
|
+
// because the errors are not actually preventing the submission
|
|
169
|
+
// but rather notifying the user that certain files were not attached and why they weren't
|
|
170
|
+
setError?.(undefined);
|
|
171
|
+
setIsSubmitting(false);
|
|
172
|
+
ref.current?.reset();
|
|
144
173
|
if (setInput)
|
|
145
174
|
setInput({ text: '', files: [] });
|
|
146
175
|
};
|
|
@@ -152,24 +181,26 @@ const FormControl = () => {
|
|
|
152
181
|
const { key, shiftKey } = event;
|
|
153
182
|
if (key === 'Enter' && !shiftKey && !composing) {
|
|
154
183
|
event.preventDefault();
|
|
155
|
-
|
|
156
|
-
if (hasInput) {
|
|
157
|
-
submitMessage();
|
|
158
|
-
}
|
|
184
|
+
submitMessage();
|
|
159
185
|
}
|
|
160
186
|
};
|
|
161
187
|
const onValidate = React__default.useCallback(async (files) => {
|
|
162
188
|
const previousFiles = input?.files ?? [];
|
|
163
|
-
const { acceptedFiles, hasMaxAttachmentsError, hasMaxAttachmentSizeError, } = await attachmentsValidator({
|
|
189
|
+
const { acceptedFiles, hasMaxAttachmentsError, hasMaxAttachmentSizeError, hasUnsupportedFileError, } = await attachmentsValidator({
|
|
164
190
|
files: [...files, ...previousFiles],
|
|
165
191
|
maxAttachments,
|
|
166
192
|
maxAttachmentSize,
|
|
167
193
|
});
|
|
168
|
-
if (hasMaxAttachmentsError ||
|
|
194
|
+
if (hasMaxAttachmentsError ||
|
|
195
|
+
hasMaxAttachmentSizeError ||
|
|
196
|
+
hasUnsupportedFileError) {
|
|
169
197
|
const errors = [];
|
|
170
198
|
if (hasMaxAttachmentsError) {
|
|
171
199
|
errors.push(displayText.getMaxAttachmentErrorText(maxAttachments));
|
|
172
200
|
}
|
|
201
|
+
if (hasUnsupportedFileError) {
|
|
202
|
+
errors.push(displayText.getAttachmentFormatErrorText([...validFileTypes]));
|
|
203
|
+
}
|
|
173
204
|
if (hasMaxAttachmentSizeError) {
|
|
174
205
|
errors.push(displayText.getAttachmentSizeErrorText(
|
|
175
206
|
// base64 size is about 137% that of the file size
|
|
@@ -187,7 +218,7 @@ const FormControl = () => {
|
|
|
187
218
|
}));
|
|
188
219
|
}, [setInput, input, displayText, maxAttachmentSize, maxAttachments, setError]);
|
|
189
220
|
if (controls?.Form) {
|
|
190
|
-
return (React__default.createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, onValidate: onValidate, allowAttachments: allowAttachments, isLoading: isLoading, error: error, setError: setError }));
|
|
221
|
+
return (React__default.createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, onValidate: onValidate, allowAttachments: allowAttachments, isLoading: isLoading ?? isSubmitting, error: error, setError: setError }));
|
|
191
222
|
}
|
|
192
223
|
return (React__default.createElement("form", { className: `${FIELD_BLOCK}__form`, onSubmit: handleSubmit, method: "post", ref: ref },
|
|
193
224
|
allowAttachments ? React__default.createElement(AttachFileControl, null) : null,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React__default from 'react';
|
|
2
|
+
import { classNames, ComponentClassName, humanFileSize } from '@aws-amplify/ui';
|
|
2
3
|
import { Image } from '@aws-amplify/ui-react';
|
|
4
|
+
import { useIcons, IconDocument } from '@aws-amplify/ui-react/internal';
|
|
3
5
|
import { withBaseElementProps } from '@aws-amplify/ui-react-core/elements';
|
|
4
6
|
import '../../context/AIContextContext.mjs';
|
|
5
7
|
import '../../context/ActionsContext.mjs';
|
|
@@ -21,7 +23,6 @@ import { AIConversationElements } from '../../context/elements/definitions.mjs';
|
|
|
21
23
|
import { convertBufferToBase64 } from '../../utils.mjs';
|
|
22
24
|
import { ActionsBarControl } from './ActionsBarControl.mjs';
|
|
23
25
|
import { AvatarControl } from './AvatarControl.mjs';
|
|
24
|
-
import { classNames } from '@aws-amplify/ui';
|
|
25
26
|
|
|
26
27
|
const { Text, View } = AIConversationElements;
|
|
27
28
|
const MESSAGES_BLOCK = 'amplify-ai-conversation__message__list';
|
|
@@ -55,6 +56,17 @@ const ToolContent = ({ toolUse, }) => {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
};
|
|
59
|
+
const DocumentContent = ({ format, name, source, }) => {
|
|
60
|
+
const icons = useIcons('aiConversation');
|
|
61
|
+
const fileIcon = icons?.document ?? React__default.createElement(IconDocument, null);
|
|
62
|
+
return (React__default.createElement(View, { className: ComponentClassName.AIConversationAttachment },
|
|
63
|
+
fileIcon,
|
|
64
|
+
React__default.createElement(View, { className: ComponentClassName.AIConversationAttachmentName },
|
|
65
|
+
name,
|
|
66
|
+
".",
|
|
67
|
+
format),
|
|
68
|
+
React__default.createElement(View, { className: ComponentClassName.AIConversationAttachmentSize }, humanFileSize(source.bytes.length, true))));
|
|
69
|
+
};
|
|
58
70
|
const MessageControl = ({ message }) => {
|
|
59
71
|
const messageRenderer = React__default.useContext(MessageRendererContext);
|
|
60
72
|
return (React__default.createElement(React__default.Fragment, null, message.content.map((content, index) => {
|
|
@@ -64,6 +76,9 @@ const MessageControl = ({ message }) => {
|
|
|
64
76
|
else if (content.image) {
|
|
65
77
|
return messageRenderer?.image ? (React__default.createElement(React__default.Fragment, { key: index }, messageRenderer?.image({ image: content.image }))) : (React__default.createElement(MediaContent, { "data-testid": 'image-content', key: index, alt: "", src: convertBufferToBase64(content.image?.source.bytes, content.image?.format) }));
|
|
66
78
|
}
|
|
79
|
+
else if (content.document) {
|
|
80
|
+
return React__default.createElement(DocumentContent, { key: index, ...content.document });
|
|
81
|
+
}
|
|
67
82
|
else if (content.toolUse) {
|
|
68
83
|
return React__default.createElement(ToolContent, { toolUse: content.toolUse, key: index });
|
|
69
84
|
}
|
|
@@ -143,4 +158,4 @@ MessagesControl.HeaderContainer = HeaderContainer;
|
|
|
143
158
|
MessagesControl.Layout = Layout;
|
|
144
159
|
MessagesControl.Message = MessageControl;
|
|
145
160
|
|
|
146
|
-
export { MessageControl, MessagesControl };
|
|
161
|
+
export { DocumentContent, MessageControl, MessagesControl };
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { View, Image, Text, Button } from '@aws-amplify/ui-react';
|
|
3
|
-
import { useIcons, IconClose } from '@aws-amplify/ui-react/internal';
|
|
3
|
+
import { useIcons, IconClose, IconDocument } from '@aws-amplify/ui-react/internal';
|
|
4
4
|
import { ComponentClassName, humanFileSize } from '@aws-amplify/ui';
|
|
5
|
+
import { getAttachmentFormat, documentFileTypes } from '../../utils.mjs';
|
|
5
6
|
|
|
6
7
|
const Attachment = ({ file, handleRemove, }) => {
|
|
7
8
|
const icons = useIcons('aiConversation');
|
|
8
9
|
const removeIcon = icons?.remove ?? React.createElement(IconClose, null);
|
|
10
|
+
const fileIcon = icons?.document ?? React.createElement(IconDocument, null);
|
|
11
|
+
const format = getAttachmentFormat(file);
|
|
12
|
+
const isDocument = documentFileTypes.has(format);
|
|
9
13
|
return (React.createElement(View, { className: ComponentClassName.AIConversationAttachment },
|
|
10
|
-
React.createElement(Image, { className: ComponentClassName.AIConversationAttachmentImage, src: URL.createObjectURL(file), alt: file.name }),
|
|
14
|
+
isDocument ? (fileIcon) : (React.createElement(Image, { className: ComponentClassName.AIConversationAttachmentImage, src: URL.createObjectURL(file), alt: file.name })),
|
|
11
15
|
React.createElement(Text, { as: "span", className: ComponentClassName.AIConversationAttachmentName }, file.name),
|
|
12
16
|
React.createElement(Text, { as: "span", className: ComponentClassName.AIConversationAttachmentSize }, humanFileSize(file.size, true)),
|
|
13
17
|
React.createElement(Button, { size: "small", variation: "link", colorTheme: "error", className: ComponentClassName.AIConversationAttachmentRemove, onClick: handleRemove }, removeIcon)));
|
|
@@ -3,6 +3,7 @@ import { View, Button, VisuallyHidden, TextAreaField, Message, DropZone } from '
|
|
|
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';
|
|
6
|
+
import { validFileTypes } from '../../utils.mjs';
|
|
6
7
|
|
|
7
8
|
function isHTMLFormElement(target) {
|
|
8
9
|
return 'form' in target;
|
|
@@ -26,8 +27,9 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
|
|
|
26
27
|
const sendIcon = icons?.send ?? React.createElement(IconSend, null);
|
|
27
28
|
const attachIcon = icons?.attach ?? React.createElement(IconAttach, null);
|
|
28
29
|
const hiddenInput = React.useRef(null);
|
|
30
|
+
// Bedrock does not accept message that are empty or are only whitespace
|
|
31
|
+
const isInputEmpty = !input?.text?.length || !!input.text.match(/^\s+$/);
|
|
29
32
|
const [composing, setComposing] = React.useState(false);
|
|
30
|
-
const isInputEmpty = !input?.text?.length && !input?.files?.length;
|
|
31
33
|
return (React.createElement(FormWrapper, { onValidate: onValidate, allowAttachments: allowAttachments },
|
|
32
34
|
React.createElement(View, { as: "form", className: ComponentClassName.AIConversationForm, onSubmit: handleSubmit },
|
|
33
35
|
allowAttachments ? (React.createElement(Button, { className: ComponentClassName.AIConversationFormAttach, onClick: () => {
|
|
@@ -43,7 +45,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
|
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
45
47
|
onValidate(Array.from(e.target.files));
|
|
46
|
-
}, multiple: true, accept:
|
|
48
|
+
}, multiple: true, accept: [...validFileTypes].map((type) => `.${type}`).join(','), "data-testid": "hidden-file-input" })))) : null,
|
|
47
49
|
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) => {
|
|
48
50
|
// Submit on enter key if shift is not pressed also
|
|
49
51
|
const shouldSubmit = !e.shiftKey && e.key === 'Enter' && !composing;
|
|
@@ -57,10 +59,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
|
|
|
57
59
|
text: e.target.value,
|
|
58
60
|
}));
|
|
59
61
|
} }),
|
|
60
|
-
React.createElement(Button, { type: "submit", variation: "primary", className: ComponentClassName.AIConversationFormSend,
|
|
61
|
-
// we intentionally || in the case where isLoading is false we should use the value of isInputEmpty
|
|
62
|
-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
63
|
-
isDisabled: isLoading || isInputEmpty },
|
|
62
|
+
React.createElement(Button, { type: "submit", variation: "primary", className: ComponentClassName.AIConversationFormSend, isDisabled: isLoading ?? isInputEmpty },
|
|
64
63
|
React.createElement("span", null, sendIcon))),
|
|
65
64
|
error ? (React.createElement(Message, { className: ComponentClassName.AIConversationFormError, variation: "plain", colorTheme: "warning" }, error)) : null,
|
|
66
65
|
React.createElement(Attachments, { setInput: setInput, files: input?.files })));
|
|
@@ -70,6 +70,7 @@ const MessageList = ({ messages, }) => {
|
|
|
70
70
|
const isLoading = React.useContext(LoadingContext);
|
|
71
71
|
const messagesWithRenderableContent = messages?.filter((message) => message.content.some((content) => content.image ??
|
|
72
72
|
content.text ??
|
|
73
|
+
content.document ??
|
|
73
74
|
content.toolUse?.name.startsWith(RESPONSE_COMPONENT_PREFIX))) ?? [];
|
|
74
75
|
return (React.createElement(View, { className: ComponentClassName.AIConversationMessageList },
|
|
75
76
|
isLoading ? (React.createElement(React.Fragment, null,
|
|
@@ -82,16 +82,17 @@ function createUseAIConversation(client) {
|
|
|
82
82
|
// between the gen2 schema definition and
|
|
83
83
|
// whats in amplify_outputs
|
|
84
84
|
if (!clientRoute) {
|
|
85
|
+
const error = {
|
|
86
|
+
message: 'Conversation route does not exist',
|
|
87
|
+
errorInfo: null,
|
|
88
|
+
errorType: '',
|
|
89
|
+
};
|
|
85
90
|
setDataState({
|
|
86
91
|
...ERROR_STATE,
|
|
87
92
|
data: { messages: [] },
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
errorInfo: null,
|
|
92
|
-
errorType: '',
|
|
93
|
-
},
|
|
94
|
-
],
|
|
93
|
+
// TODO in MV bump: remove `messages`
|
|
94
|
+
messages: [error],
|
|
95
|
+
errors: [error],
|
|
95
96
|
});
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
@@ -205,7 +206,9 @@ function createUseAIConversation(client) {
|
|
|
205
206
|
return {
|
|
206
207
|
...prev,
|
|
207
208
|
...ERROR_STATE,
|
|
209
|
+
// TODO in MV bump: remove `messages`
|
|
208
210
|
messages: error.errors,
|
|
211
|
+
errors: error.errors,
|
|
209
212
|
};
|
|
210
213
|
});
|
|
211
214
|
},
|
|
@@ -249,16 +252,17 @@ function createUseAIConversation(client) {
|
|
|
249
252
|
conversation.sendMessage(input);
|
|
250
253
|
}
|
|
251
254
|
else {
|
|
255
|
+
const error = {
|
|
256
|
+
message: 'No conversation found',
|
|
257
|
+
errorInfo: null,
|
|
258
|
+
errorType: '',
|
|
259
|
+
};
|
|
252
260
|
setDataState((prev) => ({
|
|
253
261
|
...prev,
|
|
254
262
|
...ERROR_STATE,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
errorInfo: null,
|
|
259
|
-
errorType: '',
|
|
260
|
-
},
|
|
261
|
-
],
|
|
263
|
+
// TODO in MV bump: remove `messages`
|
|
264
|
+
messages: [error],
|
|
265
|
+
errors: [error],
|
|
262
266
|
}));
|
|
263
267
|
}
|
|
264
268
|
}, [conversation]);
|
package/dist/esm/version.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -102,14 +102,63 @@ function convertBufferToBase64(buffer, format) {
|
|
|
102
102
|
const base64string = arrayBufferToBase64(buffer);
|
|
103
103
|
return `data:image/${format};base64,${base64string}`;
|
|
104
104
|
}
|
|
105
|
-
function
|
|
106
|
-
|
|
105
|
+
// This function will return the file extension or mime type
|
|
106
|
+
function getAttachmentFormat(file) {
|
|
107
|
+
const fileNameParts = file.name.split('.');
|
|
108
|
+
// try to get format from extension first
|
|
109
|
+
// this is because some document mime file types are very complex
|
|
110
|
+
// and don't easily map to what Bedrock needs like "doc" or "docx"
|
|
111
|
+
if (fileNameParts.length > 1) {
|
|
112
|
+
return fileNameParts[fileNameParts.length - 1];
|
|
113
|
+
}
|
|
114
|
+
// return mime type if no extension exists
|
|
115
|
+
return file.type.split('/')[1];
|
|
116
|
+
}
|
|
117
|
+
// This will take a File and return a string to be used as the document name
|
|
118
|
+
// in Bedrock. Bedrock has specific requirements around a valid name:
|
|
119
|
+
// https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_DocumentBlock.html
|
|
120
|
+
function getValidDocumentName(file) {
|
|
121
|
+
const fileNameParts = file.name.split('.');
|
|
122
|
+
const baseFileName = fileNameParts.length > 1 ? fileNameParts.slice(0, -1).join('') : file.name;
|
|
123
|
+
// This is a regex to target special characters that are not allowed in file names
|
|
124
|
+
const specialCharactersRegex = /[!@#$%^&*()+\-=[\]{};':"\\|,.<>/?]/g;
|
|
125
|
+
return baseFileName.replace(specialCharactersRegex, '').replace(/\s+/g, '_');
|
|
126
|
+
}
|
|
127
|
+
// Using Sets instead of Arrays for faster and easier lookups
|
|
128
|
+
const documentFileTypes = new Set([
|
|
129
|
+
'docx',
|
|
130
|
+
'csv',
|
|
131
|
+
'html',
|
|
132
|
+
'txt',
|
|
133
|
+
'pdf',
|
|
134
|
+
'md',
|
|
135
|
+
'doc',
|
|
136
|
+
'xlsx',
|
|
137
|
+
'xls',
|
|
138
|
+
]);
|
|
139
|
+
const imageFileTypes = new Set(['png', 'jpeg', 'gif', 'webp']);
|
|
140
|
+
const validFileTypes = new Set([
|
|
141
|
+
...documentFileTypes,
|
|
142
|
+
...imageFileTypes,
|
|
143
|
+
]);
|
|
144
|
+
function isDocumentFormat(format) {
|
|
145
|
+
return documentFileTypes.has(format);
|
|
146
|
+
}
|
|
147
|
+
function isImageFormat(format) {
|
|
148
|
+
return imageFileTypes.has(format);
|
|
107
149
|
}
|
|
108
150
|
async function attachmentsValidator({ files, maxAttachments, maxAttachmentSize, }) {
|
|
109
151
|
const acceptedFiles = [];
|
|
110
152
|
const rejectedFiles = [];
|
|
111
|
-
let
|
|
153
|
+
let hasMaxAttachmentSizeError = false;
|
|
154
|
+
let hasUnsupportedFileError = false;
|
|
112
155
|
for (const file of files) {
|
|
156
|
+
const format = getAttachmentFormat(file);
|
|
157
|
+
if (!validFileTypes.has(format)) {
|
|
158
|
+
rejectedFiles.push(file);
|
|
159
|
+
hasUnsupportedFileError = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
113
162
|
const arrayBuffer = await file.arrayBuffer();
|
|
114
163
|
const base64 = arrayBufferToBase64(arrayBuffer);
|
|
115
164
|
if (base64.length < maxAttachmentSize) {
|
|
@@ -117,22 +166,24 @@ async function attachmentsValidator({ files, maxAttachments, maxAttachmentSize,
|
|
|
117
166
|
}
|
|
118
167
|
else {
|
|
119
168
|
rejectedFiles.push(file);
|
|
120
|
-
|
|
169
|
+
hasMaxAttachmentSizeError = true;
|
|
121
170
|
}
|
|
122
171
|
}
|
|
123
172
|
if (acceptedFiles.length > maxAttachments) {
|
|
124
173
|
return {
|
|
125
174
|
acceptedFiles: acceptedFiles.slice(0, maxAttachments),
|
|
126
175
|
rejectedFiles: [...acceptedFiles.slice(maxAttachments), ...rejectedFiles],
|
|
176
|
+
hasMaxAttachmentSizeError,
|
|
177
|
+
hasUnsupportedFileError,
|
|
127
178
|
hasMaxAttachmentsError: true,
|
|
128
|
-
hasMaxAttachmentSizeError: hasMaxSizeError,
|
|
129
179
|
};
|
|
130
180
|
}
|
|
131
181
|
return {
|
|
132
182
|
acceptedFiles,
|
|
133
183
|
rejectedFiles,
|
|
184
|
+
hasMaxAttachmentSizeError,
|
|
185
|
+
hasUnsupportedFileError,
|
|
134
186
|
hasMaxAttachmentsError: false,
|
|
135
|
-
hasMaxAttachmentSizeError: hasMaxSizeError,
|
|
136
187
|
};
|
|
137
188
|
}
|
|
138
189
|
|
|
@@ -144,6 +195,9 @@ const defaultAIConversationDisplayTextEn = {
|
|
|
144
195
|
getAttachmentSizeErrorText(sizeText) {
|
|
145
196
|
return `File size must be below ${sizeText}.`;
|
|
146
197
|
},
|
|
198
|
+
getAttachmentFormatErrorText(formats) {
|
|
199
|
+
return `Files must be one of the supported types: ${formats.join(', ')}.`;
|
|
200
|
+
},
|
|
147
201
|
};
|
|
148
202
|
|
|
149
203
|
const { ConversationDisplayTextContext, ConversationDisplayTextProvider, useConversationDisplayText, } = uiReactCore.createContextUtilities({
|
|
@@ -605,6 +659,8 @@ const TextInput = React__namespace["default"].forwardRef(function TextInput(prop
|
|
|
605
659
|
const InputContainer = elements.withBaseElementProps(View$2, {
|
|
606
660
|
className: `${FIELD_BLOCK}__input-container`,
|
|
607
661
|
});
|
|
662
|
+
const isConversationInputWithText = (input) => !!input?.text;
|
|
663
|
+
const isConversationInputWithFiles = (input) => !!input?.files?.length;
|
|
608
664
|
const FormControl = () => {
|
|
609
665
|
const { input, setInput, error, setError } = React__namespace["default"].useContext(ConversationInputContext);
|
|
610
666
|
const handleSendMessage = React__namespace["default"].useContext(SendMessageContext);
|
|
@@ -616,25 +672,46 @@ const FormControl = () => {
|
|
|
616
672
|
const ref = React__namespace["default"].useRef(null);
|
|
617
673
|
const controls = React__namespace["default"].useContext(ControlsContext);
|
|
618
674
|
const [composing, setComposing] = React__namespace["default"].useState(false);
|
|
675
|
+
const [isSubmitting, setIsSubmitting] = React__namespace["default"].useState(false);
|
|
676
|
+
const isInputText = isConversationInputWithText(input);
|
|
677
|
+
// an empty array will resolve false when evaluating the length
|
|
678
|
+
const isInputFiles = isConversationInputWithFiles(input);
|
|
619
679
|
const submitMessage = async () => {
|
|
620
|
-
|
|
680
|
+
const hasInput = isInputFiles || isInputText;
|
|
681
|
+
// Prevent double submission and empty submission
|
|
682
|
+
if (isSubmitting || !hasInput) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
setIsSubmitting(true);
|
|
621
686
|
const submittedContent = [];
|
|
622
|
-
if (
|
|
687
|
+
if (isInputText) {
|
|
623
688
|
const textContent = {
|
|
624
689
|
text: input.text,
|
|
625
690
|
};
|
|
626
691
|
submittedContent.push(textContent);
|
|
627
692
|
}
|
|
628
|
-
if (
|
|
693
|
+
if (isInputFiles) {
|
|
629
694
|
for (const file of input.files) {
|
|
630
695
|
const buffer = await file.arrayBuffer();
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
696
|
+
const format = getAttachmentFormat(file);
|
|
697
|
+
const source = { bytes: new Uint8Array(buffer) };
|
|
698
|
+
if (isDocumentFormat(format)) {
|
|
699
|
+
submittedContent.push({
|
|
700
|
+
document: {
|
|
701
|
+
name: getValidDocumentName(file),
|
|
702
|
+
format,
|
|
703
|
+
source,
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
else if (isImageFormat(format)) {
|
|
708
|
+
submittedContent.push({
|
|
709
|
+
image: {
|
|
710
|
+
format,
|
|
711
|
+
source,
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
}
|
|
638
715
|
}
|
|
639
716
|
}
|
|
640
717
|
if (handleSendMessage) {
|
|
@@ -644,6 +721,12 @@ const FormControl = () => {
|
|
|
644
721
|
toolConfiguration: convertResponseComponentsToToolConfiguration(responseComponents),
|
|
645
722
|
});
|
|
646
723
|
}
|
|
724
|
+
// Clear the attachment errors when submitting
|
|
725
|
+
// because the errors are not actually preventing the submission
|
|
726
|
+
// but rather notifying the user that certain files were not attached and why they weren't
|
|
727
|
+
setError?.(undefined);
|
|
728
|
+
setIsSubmitting(false);
|
|
729
|
+
ref.current?.reset();
|
|
647
730
|
if (setInput)
|
|
648
731
|
setInput({ text: '', files: [] });
|
|
649
732
|
};
|
|
@@ -655,24 +738,26 @@ const FormControl = () => {
|
|
|
655
738
|
const { key, shiftKey } = event;
|
|
656
739
|
if (key === 'Enter' && !shiftKey && !composing) {
|
|
657
740
|
event.preventDefault();
|
|
658
|
-
|
|
659
|
-
if (hasInput) {
|
|
660
|
-
submitMessage();
|
|
661
|
-
}
|
|
741
|
+
submitMessage();
|
|
662
742
|
}
|
|
663
743
|
};
|
|
664
744
|
const onValidate = React__namespace["default"].useCallback(async (files) => {
|
|
665
745
|
const previousFiles = input?.files ?? [];
|
|
666
|
-
const { acceptedFiles, hasMaxAttachmentsError, hasMaxAttachmentSizeError, } = await attachmentsValidator({
|
|
746
|
+
const { acceptedFiles, hasMaxAttachmentsError, hasMaxAttachmentSizeError, hasUnsupportedFileError, } = await attachmentsValidator({
|
|
667
747
|
files: [...files, ...previousFiles],
|
|
668
748
|
maxAttachments,
|
|
669
749
|
maxAttachmentSize,
|
|
670
750
|
});
|
|
671
|
-
if (hasMaxAttachmentsError ||
|
|
751
|
+
if (hasMaxAttachmentsError ||
|
|
752
|
+
hasMaxAttachmentSizeError ||
|
|
753
|
+
hasUnsupportedFileError) {
|
|
672
754
|
const errors = [];
|
|
673
755
|
if (hasMaxAttachmentsError) {
|
|
674
756
|
errors.push(displayText.getMaxAttachmentErrorText(maxAttachments));
|
|
675
757
|
}
|
|
758
|
+
if (hasUnsupportedFileError) {
|
|
759
|
+
errors.push(displayText.getAttachmentFormatErrorText([...validFileTypes]));
|
|
760
|
+
}
|
|
676
761
|
if (hasMaxAttachmentSizeError) {
|
|
677
762
|
errors.push(displayText.getAttachmentSizeErrorText(
|
|
678
763
|
// base64 size is about 137% that of the file size
|
|
@@ -690,7 +775,7 @@ const FormControl = () => {
|
|
|
690
775
|
}));
|
|
691
776
|
}, [setInput, input, displayText, maxAttachmentSize, maxAttachments, setError]);
|
|
692
777
|
if (controls?.Form) {
|
|
693
|
-
return (React__namespace["default"].createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, onValidate: onValidate, allowAttachments: allowAttachments, isLoading: isLoading, error: error, setError: setError }));
|
|
778
|
+
return (React__namespace["default"].createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, onValidate: onValidate, allowAttachments: allowAttachments, isLoading: isLoading ?? isSubmitting, error: error, setError: setError }));
|
|
694
779
|
}
|
|
695
780
|
return (React__namespace["default"].createElement("form", { className: `${FIELD_BLOCK}__form`, onSubmit: handleSubmit, method: "post", ref: ref },
|
|
696
781
|
allowAttachments ? React__namespace["default"].createElement(AttachFileControl, null) : null,
|
|
@@ -741,6 +826,17 @@ const ToolContent = ({ toolUse, }) => {
|
|
|
741
826
|
}
|
|
742
827
|
}
|
|
743
828
|
};
|
|
829
|
+
const DocumentContent = ({ format, name, source, }) => {
|
|
830
|
+
const icons = internal.useIcons('aiConversation');
|
|
831
|
+
const fileIcon = icons?.document ?? React__namespace["default"].createElement(internal.IconDocument, null);
|
|
832
|
+
return (React__namespace["default"].createElement(View$1, { className: ui.ComponentClassName.AIConversationAttachment },
|
|
833
|
+
fileIcon,
|
|
834
|
+
React__namespace["default"].createElement(View$1, { className: ui.ComponentClassName.AIConversationAttachmentName },
|
|
835
|
+
name,
|
|
836
|
+
".",
|
|
837
|
+
format),
|
|
838
|
+
React__namespace["default"].createElement(View$1, { className: ui.ComponentClassName.AIConversationAttachmentSize }, ui.humanFileSize(source.bytes.length, true))));
|
|
839
|
+
};
|
|
744
840
|
const MessageControl = ({ message }) => {
|
|
745
841
|
const messageRenderer = React__namespace["default"].useContext(MessageRendererContext);
|
|
746
842
|
return (React__namespace["default"].createElement(React__namespace["default"].Fragment, null, message.content.map((content, index) => {
|
|
@@ -750,6 +846,9 @@ const MessageControl = ({ message }) => {
|
|
|
750
846
|
else if (content.image) {
|
|
751
847
|
return messageRenderer?.image ? (React__namespace["default"].createElement(React__namespace["default"].Fragment, { key: index }, messageRenderer?.image({ image: content.image }))) : (React__namespace["default"].createElement(MediaContent, { "data-testid": 'image-content', key: index, alt: "", src: convertBufferToBase64(content.image?.source.bytes, content.image?.format) }));
|
|
752
848
|
}
|
|
849
|
+
else if (content.document) {
|
|
850
|
+
return React__namespace["default"].createElement(DocumentContent, { key: index, ...content.document });
|
|
851
|
+
}
|
|
753
852
|
else if (content.toolUse) {
|
|
754
853
|
return React__namespace["default"].createElement(ToolContent, { toolUse: content.toolUse, key: index });
|
|
755
854
|
}
|
|
@@ -988,6 +1087,7 @@ const MessageList = ({ messages, }) => {
|
|
|
988
1087
|
const isLoading = React__namespace.useContext(LoadingContext);
|
|
989
1088
|
const messagesWithRenderableContent = messages?.filter((message) => message.content.some((content) => content.image ??
|
|
990
1089
|
content.text ??
|
|
1090
|
+
content.document ??
|
|
991
1091
|
content.toolUse?.name.startsWith(RESPONSE_COMPONENT_PREFIX))) ?? [];
|
|
992
1092
|
return (React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageList },
|
|
993
1093
|
isLoading ? (React__namespace.createElement(React__namespace.Fragment, null,
|
|
@@ -999,8 +1099,11 @@ const MessageList = ({ messages, }) => {
|
|
|
999
1099
|
const Attachment = ({ file, handleRemove, }) => {
|
|
1000
1100
|
const icons = internal.useIcons('aiConversation');
|
|
1001
1101
|
const removeIcon = icons?.remove ?? React__namespace.createElement(internal.IconClose, null);
|
|
1102
|
+
const fileIcon = icons?.document ?? React__namespace.createElement(internal.IconDocument, null);
|
|
1103
|
+
const format = getAttachmentFormat(file);
|
|
1104
|
+
const isDocument = documentFileTypes.has(format);
|
|
1002
1105
|
return (React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationAttachment },
|
|
1003
|
-
React__namespace.createElement(uiReact.Image, { className: ui.ComponentClassName.AIConversationAttachmentImage, src: URL.createObjectURL(file), alt: file.name }),
|
|
1106
|
+
isDocument ? (fileIcon) : (React__namespace.createElement(uiReact.Image, { className: ui.ComponentClassName.AIConversationAttachmentImage, src: URL.createObjectURL(file), alt: file.name })),
|
|
1004
1107
|
React__namespace.createElement(uiReact.Text, { as: "span", className: ui.ComponentClassName.AIConversationAttachmentName }, file.name),
|
|
1005
1108
|
React__namespace.createElement(uiReact.Text, { as: "span", className: ui.ComponentClassName.AIConversationAttachmentSize }, ui.humanFileSize(file.size, true)),
|
|
1006
1109
|
React__namespace.createElement(uiReact.Button, { size: "small", variation: "link", colorTheme: "error", className: ui.ComponentClassName.AIConversationAttachmentRemove, onClick: handleRemove }, removeIcon)));
|
|
@@ -1041,8 +1144,9 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
|
|
|
1041
1144
|
const sendIcon = icons?.send ?? React__namespace.createElement(internal.IconSend, null);
|
|
1042
1145
|
const attachIcon = icons?.attach ?? React__namespace.createElement(internal.IconAttach, null);
|
|
1043
1146
|
const hiddenInput = React__namespace.useRef(null);
|
|
1147
|
+
// Bedrock does not accept message that are empty or are only whitespace
|
|
1148
|
+
const isInputEmpty = !input?.text?.length || !!input.text.match(/^\s+$/);
|
|
1044
1149
|
const [composing, setComposing] = React__namespace.useState(false);
|
|
1045
|
-
const isInputEmpty = !input?.text?.length && !input?.files?.length;
|
|
1046
1150
|
return (React__namespace.createElement(FormWrapper, { onValidate: onValidate, allowAttachments: allowAttachments },
|
|
1047
1151
|
React__namespace.createElement(uiReact.View, { as: "form", className: ui.ComponentClassName.AIConversationForm, onSubmit: handleSubmit },
|
|
1048
1152
|
allowAttachments ? (React__namespace.createElement(uiReact.Button, { className: ui.ComponentClassName.AIConversationFormAttach, onClick: () => {
|
|
@@ -1058,7 +1162,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
|
|
|
1058
1162
|
return;
|
|
1059
1163
|
}
|
|
1060
1164
|
onValidate(Array.from(e.target.files));
|
|
1061
|
-
}, multiple: true, accept:
|
|
1165
|
+
}, multiple: true, accept: [...validFileTypes].map((type) => `.${type}`).join(','), "data-testid": "hidden-file-input" })))) : null,
|
|
1062
1166
|
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) => {
|
|
1063
1167
|
// Submit on enter key if shift is not pressed also
|
|
1064
1168
|
const shouldSubmit = !e.shiftKey && e.key === 'Enter' && !composing;
|
|
@@ -1072,10 +1176,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
|
|
|
1072
1176
|
text: e.target.value,
|
|
1073
1177
|
}));
|
|
1074
1178
|
} }),
|
|
1075
|
-
React__namespace.createElement(uiReact.Button, { type: "submit", variation: "primary", className: ui.ComponentClassName.AIConversationFormSend,
|
|
1076
|
-
// we intentionally || in the case where isLoading is false we should use the value of isInputEmpty
|
|
1077
|
-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
1078
|
-
isDisabled: isLoading || isInputEmpty },
|
|
1179
|
+
React__namespace.createElement(uiReact.Button, { type: "submit", variation: "primary", className: ui.ComponentClassName.AIConversationFormSend, isDisabled: isLoading ?? isInputEmpty },
|
|
1079
1180
|
React__namespace.createElement("span", null, sendIcon))),
|
|
1080
1181
|
error ? (React__namespace.createElement(uiReact.Message, { className: ui.ComponentClassName.AIConversationFormError, variation: "plain", colorTheme: "warning" }, error)) : null,
|
|
1081
1182
|
React__namespace.createElement(Attachments, { setInput: setInput, files: input?.files })));
|
|
@@ -1092,7 +1193,7 @@ const PromptList = ({ setInput, suggestedPrompts = [], }) => {
|
|
|
1092
1193
|
})));
|
|
1093
1194
|
};
|
|
1094
1195
|
|
|
1095
|
-
const VERSION = '1.
|
|
1196
|
+
const VERSION = '1.4.0';
|
|
1096
1197
|
|
|
1097
1198
|
function AIConversationBase({ avatars, controls, ...rest }) {
|
|
1098
1199
|
uiReactCore.useSetUserAgent({
|
|
@@ -1294,16 +1395,17 @@ function createUseAIConversation(client) {
|
|
|
1294
1395
|
// between the gen2 schema definition and
|
|
1295
1396
|
// whats in amplify_outputs
|
|
1296
1397
|
if (!clientRoute) {
|
|
1398
|
+
const error = {
|
|
1399
|
+
message: 'Conversation route does not exist',
|
|
1400
|
+
errorInfo: null,
|
|
1401
|
+
errorType: '',
|
|
1402
|
+
};
|
|
1297
1403
|
setDataState({
|
|
1298
1404
|
...ERROR_STATE,
|
|
1299
1405
|
data: { messages: [] },
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
errorInfo: null,
|
|
1304
|
-
errorType: '',
|
|
1305
|
-
},
|
|
1306
|
-
],
|
|
1406
|
+
// TODO in MV bump: remove `messages`
|
|
1407
|
+
messages: [error],
|
|
1408
|
+
errors: [error],
|
|
1307
1409
|
});
|
|
1308
1410
|
return;
|
|
1309
1411
|
}
|
|
@@ -1417,7 +1519,9 @@ function createUseAIConversation(client) {
|
|
|
1417
1519
|
return {
|
|
1418
1520
|
...prev,
|
|
1419
1521
|
...ERROR_STATE,
|
|
1522
|
+
// TODO in MV bump: remove `messages`
|
|
1420
1523
|
messages: error.errors,
|
|
1524
|
+
errors: error.errors,
|
|
1421
1525
|
};
|
|
1422
1526
|
});
|
|
1423
1527
|
},
|
|
@@ -1461,16 +1565,17 @@ function createUseAIConversation(client) {
|
|
|
1461
1565
|
conversation.sendMessage(input);
|
|
1462
1566
|
}
|
|
1463
1567
|
else {
|
|
1568
|
+
const error = {
|
|
1569
|
+
message: 'No conversation found',
|
|
1570
|
+
errorInfo: null,
|
|
1571
|
+
errorType: '',
|
|
1572
|
+
};
|
|
1464
1573
|
setDataState((prev) => ({
|
|
1465
1574
|
...prev,
|
|
1466
1575
|
...ERROR_STATE,
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
errorInfo: null,
|
|
1471
|
-
errorType: '',
|
|
1472
|
-
},
|
|
1473
|
-
],
|
|
1576
|
+
// TODO in MV bump: remove `messages`
|
|
1577
|
+
messages: [error],
|
|
1578
|
+
errors: [error],
|
|
1474
1579
|
}));
|
|
1475
1580
|
}
|
|
1476
1581
|
}, [conversation]);
|
|
@@ -3,6 +3,7 @@ export type ConversationDisplayText = {
|
|
|
3
3
|
getMessageTimestampText?: (date: Date) => string;
|
|
4
4
|
getMaxAttachmentErrorText?: (count: number) => string;
|
|
5
5
|
getAttachmentSizeErrorText?: (sizeText: string) => string;
|
|
6
|
+
getAttachmentFormatErrorText?: (formats: string[]) => string;
|
|
6
7
|
};
|
|
7
8
|
export declare const defaultAIConversationDisplayTextEn: Required<AIConversationDisplayText>;
|
|
8
9
|
export type AIConversationDisplayText = DisplayTextTemplate<ConversationDisplayText>;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { ImageContentBlock } from '../../types';
|
|
1
|
+
import { ImageContentBlock, DocumentContentBlock } from '../../types';
|
|
2
2
|
export declare function formatDate(date: Date): string;
|
|
3
3
|
export declare function convertBufferToBase64(buffer: ArrayBuffer, format: ImageContentBlock['format']): string;
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function getAttachmentFormat(file: File): string;
|
|
5
|
+
export declare function getValidDocumentName(file: File): string;
|
|
6
|
+
export declare const documentFileTypes: Set<string>;
|
|
7
|
+
export declare const imageFileTypes: Set<string>;
|
|
8
|
+
export declare const validFileTypes: Set<string>;
|
|
9
|
+
export declare function isDocumentFormat(format: string): format is DocumentContentBlock['format'];
|
|
10
|
+
export declare function isImageFormat(format: string): format is ImageContentBlock['format'];
|
|
5
11
|
export declare function attachmentsValidator({ files, maxAttachments, maxAttachmentSize, }: {
|
|
6
12
|
files: File[];
|
|
7
13
|
maxAttachments: number;
|
|
@@ -11,4 +17,5 @@ export declare function attachmentsValidator({ files, maxAttachments, maxAttachm
|
|
|
11
17
|
rejectedFiles: File[];
|
|
12
18
|
hasMaxAttachmentSizeError: boolean;
|
|
13
19
|
hasMaxAttachmentsError: boolean;
|
|
20
|
+
hasUnsupportedFileError: boolean;
|
|
14
21
|
}>;
|
|
@@ -3,6 +3,7 @@ import { AIConversationElements } from '../../context/elements';
|
|
|
3
3
|
import { ActionsBarControl } from './ActionsBarControl';
|
|
4
4
|
import { AvatarControl } from './AvatarControl';
|
|
5
5
|
import { ConversationMessage } from '../../../../types';
|
|
6
|
+
export declare const DocumentContent: ({ format, name, source, }: NonNullable<ConversationMessage['content'][number]['document']>) => React.JSX.Element;
|
|
6
7
|
export declare const MessageControl: MessageControl;
|
|
7
8
|
interface MessageControl {
|
|
8
9
|
(props: {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { DataState } from '@aws-amplify/ui-react-core';
|
|
2
2
|
import { GraphQLFormattedError } from '../types';
|
|
3
3
|
export type DataClientState<T> = Omit<DataState<T>, 'message'> & {
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated will be removed in a future major version. Superseded by `errors`
|
|
6
|
+
* @description errors returned from the websocket connection
|
|
7
|
+
*/
|
|
4
8
|
messages?: GraphQLFormattedError[];
|
|
9
|
+
/**
|
|
10
|
+
* @description errors returned from the websocket connection
|
|
11
|
+
*/
|
|
12
|
+
errors?: GraphQLFormattedError[];
|
|
5
13
|
};
|
|
6
14
|
export type DataClientResponse<T> = {
|
|
7
15
|
data: T | null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ClientExtensions } from '@aws-amplify/data-schema/runtime';
|
|
2
2
|
import { getSchema } from '../types';
|
|
3
3
|
import { DataClientState } from './shared';
|
|
4
4
|
export interface UseAIGenerationHookWrapper<Key extends keyof AIGenerationClient<Schema>['generations'], Schema extends Record<any, any>> {
|
|
@@ -11,6 +11,6 @@ export type UseAIGenerationHook<Key extends keyof AIGenerationClient<Schema>['ge
|
|
|
11
11
|
Awaited<DataClientState<Schema[Key]['returnType']>>,
|
|
12
12
|
(input: Schema[Key]['args']) => void
|
|
13
13
|
];
|
|
14
|
-
type AIGenerationClient<T extends Record<any, any>> = Pick<
|
|
14
|
+
type AIGenerationClient<T extends Record<any, any>> = Pick<ClientExtensions<T>, 'generations'>;
|
|
15
15
|
export declare function createUseAIGeneration<Client extends Record<'generations' | 'conversations', Record<string, any>>, Schema extends getSchema<Client>>(client: Client): UseAIGenerationHook<keyof Client['generations'], Client>;
|
|
16
16
|
export {};
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export type ConversationRoute =
|
|
1
|
+
import type { ClientExtensions } from '@aws-amplify/data-schema/runtime';
|
|
2
|
+
export type ConversationRoute = ClientExtensions<any>['conversations'][string];
|
|
3
3
|
export type Conversation = NonNullable<Awaited<ReturnType<ConversationRoute['create']>>['data']>;
|
|
4
4
|
export type ConversationStreamEvent = Parameters<Parameters<Conversation['onStreamEvent']>[0]['next']>[0];
|
|
5
5
|
export type ConversationMessage = NonNullable<Awaited<ReturnType<Conversation['sendMessage']>>['data']> & {
|
|
@@ -8,6 +8,7 @@ export type ConversationMessage = NonNullable<Awaited<ReturnType<Conversation['s
|
|
|
8
8
|
export type ConversationMessageContent = ConversationMessage['content'][number];
|
|
9
9
|
export type TextContentBlock = NonNullable<ConversationMessageContent['text']>;
|
|
10
10
|
export type ImageContentBlock = NonNullable<ConversationMessageContent['image']>;
|
|
11
|
+
export type DocumentContentBlock = NonNullable<ConversationMessageContent['document']>;
|
|
11
12
|
export type ToolUseContent = NonNullable<ConversationMessageContent['toolUse']>;
|
|
12
13
|
export type ToolResultContent = NonNullable<ConversationMessageContent['toolResult']>;
|
|
13
14
|
export type InputContent = Exclude<Parameters<Conversation['sendMessage']>[0], string>['content'][number];
|
|
@@ -20,7 +21,7 @@ export interface SendMesageParameters {
|
|
|
20
21
|
toolConfiguration?: ToolConfiguration;
|
|
21
22
|
}
|
|
22
23
|
export type SendMessage = (input: SendMesageParameters) => void;
|
|
23
|
-
type AIClient<T extends Record<any, any>> = Pick<
|
|
24
|
+
type AIClient<T extends Record<any, any>> = Pick<ClientExtensions<T>, 'generations' | 'conversations'>;
|
|
24
25
|
export type getSchema<T> = T extends AIClient<infer Schema> ? Schema : never;
|
|
25
26
|
export interface GraphQLFormattedError {
|
|
26
27
|
readonly message: string;
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.
|
|
1
|
+
export declare const VERSION = "1.4.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aws-amplify/ui-react-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/esm/index.mjs",
|
|
6
6
|
"exports": {
|
|
@@ -42,32 +42,28 @@
|
|
|
42
42
|
"typecheck": "tsc --noEmit"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@aws-amplify/
|
|
45
|
+
"@aws-amplify/data-schema": "^1.19.0",
|
|
46
46
|
"aws-amplify": "^6.9.0",
|
|
47
47
|
"react": "^16.14 || ^17 || ^18 || ^19",
|
|
48
48
|
"react-dom": "^16.14 || ^17 || ^18 || ^19"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@aws-amplify/ui": "^6.
|
|
52
|
-
"@aws-amplify/ui-react": "^6.
|
|
53
|
-
"@aws-amplify/ui-react-core": "^3.
|
|
54
|
-
},
|
|
55
|
-
"devDependencies": {
|
|
56
|
-
"@types/jest-when": "^3.5.0",
|
|
57
|
-
"jest-when": "^3.5.1"
|
|
51
|
+
"@aws-amplify/ui": "^6.10.1",
|
|
52
|
+
"@aws-amplify/ui-react": "^6.11.0",
|
|
53
|
+
"@aws-amplify/ui-react-core": "^3.4.1"
|
|
58
54
|
},
|
|
59
55
|
"size-limit": [
|
|
60
56
|
{
|
|
61
57
|
"name": "AIConversation",
|
|
62
58
|
"path": "dist/esm/index.mjs",
|
|
63
59
|
"import": "{ AIConversation }",
|
|
64
|
-
"limit": "
|
|
60
|
+
"limit": "27 kB"
|
|
65
61
|
},
|
|
66
62
|
{
|
|
67
63
|
"name": "createAIConversation",
|
|
68
64
|
"path": "dist/esm/index.mjs",
|
|
69
65
|
"import": "{ createAIConversation }",
|
|
70
|
-
"limit": "
|
|
66
|
+
"limit": "20 kB"
|
|
71
67
|
}
|
|
72
68
|
]
|
|
73
69
|
}
|