@aws-amplify/ui-react-ai 1.2.1 → 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 +20 -3
- 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 +19 -15
- package/dist/esm/version.mjs +1 -1
- package/dist/index.js +154 -47
- package/dist/types/components/AIConversation/context/AIContextContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/ActionsContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/AttachmentContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/AvatarsContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/ControlsContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/ConversationInputContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/DisplayTextContext.d.ts +0 -1
- package/dist/types/components/AIConversation/context/FallbackComponentContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/LoadingContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/MessageRenderContext.d.ts +0 -1
- package/dist/types/components/AIConversation/context/MessageVariantContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/MessagesContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/ResponseComponentsContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/SendMessageContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/SuggestedPromptsContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/WelcomeMessageContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/elements/definitions.d.ts +0 -1
- package/dist/types/components/AIConversation/displayText.d.ts +1 -0
- package/dist/types/components/AIConversation/types.d.ts +4 -4
- package/dist/types/components/AIConversation/utils.d.ts +9 -2
- package/dist/types/components/AIConversation/views/Controls/AttachmentListControl.d.ts +5 -5
- package/dist/types/components/AIConversation/views/Controls/DefaultMessageControl.d.ts +2 -2
- package/dist/types/components/AIConversation/views/Controls/MessagesControl.d.ts +4 -3
- package/dist/types/components/AIConversation/views/default/Attachments.d.ts +2 -2
- 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 -4
- package/dist/types/version.d.ts +1 -1
- package/package.json +9 -13
|
@@ -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
|
}
|
|
@@ -126,7 +141,9 @@ const MessagesControl = () => {
|
|
|
126
141
|
content.toolUse?.name.startsWith(RESPONSE_COMPONENT_PREFIX))) ?? [];
|
|
127
142
|
return (React__default.createElement(Layout, null, messagesWithRenderableContent?.map((message, index) => {
|
|
128
143
|
return (React__default.createElement(RoleContext.Provider, { value: message.role, key: `message-${index}` },
|
|
129
|
-
React__default.createElement(MessageContainer, { "data-testid": `message`, key: `message-${index}`, tabIndex: focusedItemIndex === index ? 0 : -1, onFocus: () => handleFocus(index), onKeyDown: (event) => onKeyDown(index, event), ref: (el) =>
|
|
144
|
+
React__default.createElement(MessageContainer, { "data-testid": `message`, key: `message-${index}`, tabIndex: focusedItemIndex === index ? 0 : -1, onFocus: () => handleFocus(index), onKeyDown: (event) => onKeyDown(index, event), ref: (el) => {
|
|
145
|
+
messagesRef.current[index] = el;
|
|
146
|
+
} },
|
|
130
147
|
React__default.createElement(HeaderContainer, null,
|
|
131
148
|
React__default.createElement(AvatarControl, null),
|
|
132
149
|
React__default.createElement(Timestamp, null, getMessageTimestampText(new Date(message.createdAt)))),
|
|
@@ -141,4 +158,4 @@ MessagesControl.HeaderContainer = HeaderContainer;
|
|
|
141
158
|
MessagesControl.Layout = Layout;
|
|
142
159
|
MessagesControl.Message = MessageControl;
|
|
143
160
|
|
|
144
|
-
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,
|
|
@@ -22,7 +22,7 @@ function createUseAIConversation(client) {
|
|
|
22
22
|
const clientRoute = client.conversations[routeName];
|
|
23
23
|
// We need to keep track of the stream events as the come in
|
|
24
24
|
// for an assistant message, but don't need to keep them in state
|
|
25
|
-
const contentBlocksRef = React__default.useRef();
|
|
25
|
+
const contentBlocksRef = React__default.useRef(undefined);
|
|
26
26
|
// Using this hook without an existing conversation id means
|
|
27
27
|
// it will create a new conversation when it is executed
|
|
28
28
|
// we don't want to create 2 conversations
|
|
@@ -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
|
}
|
|
@@ -812,7 +911,9 @@ const MessagesControl = () => {
|
|
|
812
911
|
content.toolUse?.name.startsWith(RESPONSE_COMPONENT_PREFIX))) ?? [];
|
|
813
912
|
return (React__namespace["default"].createElement(Layout, null, messagesWithRenderableContent?.map((message, index) => {
|
|
814
913
|
return (React__namespace["default"].createElement(RoleContext.Provider, { value: message.role, key: `message-${index}` },
|
|
815
|
-
React__namespace["default"].createElement(MessageContainer, { "data-testid": `message`, key: `message-${index}`, tabIndex: focusedItemIndex === index ? 0 : -1, onFocus: () => handleFocus(index), onKeyDown: (event) => onKeyDown(index, event), ref: (el) =>
|
|
914
|
+
React__namespace["default"].createElement(MessageContainer, { "data-testid": `message`, key: `message-${index}`, tabIndex: focusedItemIndex === index ? 0 : -1, onFocus: () => handleFocus(index), onKeyDown: (event) => onKeyDown(index, event), ref: (el) => {
|
|
915
|
+
messagesRef.current[index] = el;
|
|
916
|
+
} },
|
|
816
917
|
React__namespace["default"].createElement(HeaderContainer, null,
|
|
817
918
|
React__namespace["default"].createElement(AvatarControl, null),
|
|
818
919
|
React__namespace["default"].createElement(Timestamp, null, getMessageTimestampText(new Date(message.createdAt)))),
|
|
@@ -986,6 +1087,7 @@ const MessageList = ({ messages, }) => {
|
|
|
986
1087
|
const isLoading = React__namespace.useContext(LoadingContext);
|
|
987
1088
|
const messagesWithRenderableContent = messages?.filter((message) => message.content.some((content) => content.image ??
|
|
988
1089
|
content.text ??
|
|
1090
|
+
content.document ??
|
|
989
1091
|
content.toolUse?.name.startsWith(RESPONSE_COMPONENT_PREFIX))) ?? [];
|
|
990
1092
|
return (React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageList },
|
|
991
1093
|
isLoading ? (React__namespace.createElement(React__namespace.Fragment, null,
|
|
@@ -997,8 +1099,11 @@ const MessageList = ({ messages, }) => {
|
|
|
997
1099
|
const Attachment = ({ file, handleRemove, }) => {
|
|
998
1100
|
const icons = internal.useIcons('aiConversation');
|
|
999
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);
|
|
1000
1105
|
return (React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationAttachment },
|
|
1001
|
-
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 })),
|
|
1002
1107
|
React__namespace.createElement(uiReact.Text, { as: "span", className: ui.ComponentClassName.AIConversationAttachmentName }, file.name),
|
|
1003
1108
|
React__namespace.createElement(uiReact.Text, { as: "span", className: ui.ComponentClassName.AIConversationAttachmentSize }, ui.humanFileSize(file.size, true)),
|
|
1004
1109
|
React__namespace.createElement(uiReact.Button, { size: "small", variation: "link", colorTheme: "error", className: ui.ComponentClassName.AIConversationAttachmentRemove, onClick: handleRemove }, removeIcon)));
|
|
@@ -1039,8 +1144,9 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
|
|
|
1039
1144
|
const sendIcon = icons?.send ?? React__namespace.createElement(internal.IconSend, null);
|
|
1040
1145
|
const attachIcon = icons?.attach ?? React__namespace.createElement(internal.IconAttach, null);
|
|
1041
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+$/);
|
|
1042
1149
|
const [composing, setComposing] = React__namespace.useState(false);
|
|
1043
|
-
const isInputEmpty = !input?.text?.length && !input?.files?.length;
|
|
1044
1150
|
return (React__namespace.createElement(FormWrapper, { onValidate: onValidate, allowAttachments: allowAttachments },
|
|
1045
1151
|
React__namespace.createElement(uiReact.View, { as: "form", className: ui.ComponentClassName.AIConversationForm, onSubmit: handleSubmit },
|
|
1046
1152
|
allowAttachments ? (React__namespace.createElement(uiReact.Button, { className: ui.ComponentClassName.AIConversationFormAttach, onClick: () => {
|
|
@@ -1056,7 +1162,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
|
|
|
1056
1162
|
return;
|
|
1057
1163
|
}
|
|
1058
1164
|
onValidate(Array.from(e.target.files));
|
|
1059
|
-
}, multiple: true, accept:
|
|
1165
|
+
}, multiple: true, accept: [...validFileTypes].map((type) => `.${type}`).join(','), "data-testid": "hidden-file-input" })))) : null,
|
|
1060
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) => {
|
|
1061
1167
|
// Submit on enter key if shift is not pressed also
|
|
1062
1168
|
const shouldSubmit = !e.shiftKey && e.key === 'Enter' && !composing;
|
|
@@ -1070,10 +1176,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
|
|
|
1070
1176
|
text: e.target.value,
|
|
1071
1177
|
}));
|
|
1072
1178
|
} }),
|
|
1073
|
-
React__namespace.createElement(uiReact.Button, { type: "submit", variation: "primary", className: ui.ComponentClassName.AIConversationFormSend,
|
|
1074
|
-
// we intentionally || in the case where isLoading is false we should use the value of isInputEmpty
|
|
1075
|
-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
1076
|
-
isDisabled: isLoading || isInputEmpty },
|
|
1179
|
+
React__namespace.createElement(uiReact.Button, { type: "submit", variation: "primary", className: ui.ComponentClassName.AIConversationFormSend, isDisabled: isLoading ?? isInputEmpty },
|
|
1077
1180
|
React__namespace.createElement("span", null, sendIcon))),
|
|
1078
1181
|
error ? (React__namespace.createElement(uiReact.Message, { className: ui.ComponentClassName.AIConversationFormError, variation: "plain", colorTheme: "warning" }, error)) : null,
|
|
1079
1182
|
React__namespace.createElement(Attachments, { setInput: setInput, files: input?.files })));
|
|
@@ -1090,7 +1193,7 @@ const PromptList = ({ setInput, suggestedPrompts = [], }) => {
|
|
|
1090
1193
|
})));
|
|
1091
1194
|
};
|
|
1092
1195
|
|
|
1093
|
-
const VERSION = '1.
|
|
1196
|
+
const VERSION = '1.4.0';
|
|
1094
1197
|
|
|
1095
1198
|
function AIConversationBase({ avatars, controls, ...rest }) {
|
|
1096
1199
|
uiReactCore.useSetUserAgent({
|
|
@@ -1232,7 +1335,7 @@ function createUseAIConversation(client) {
|
|
|
1232
1335
|
const clientRoute = client.conversations[routeName];
|
|
1233
1336
|
// We need to keep track of the stream events as the come in
|
|
1234
1337
|
// for an assistant message, but don't need to keep them in state
|
|
1235
|
-
const contentBlocksRef = React__namespace["default"].useRef();
|
|
1338
|
+
const contentBlocksRef = React__namespace["default"].useRef(undefined);
|
|
1236
1339
|
// Using this hook without an existing conversation id means
|
|
1237
1340
|
// it will create a new conversation when it is executed
|
|
1238
1341
|
// we don't want to create 2 conversations
|
|
@@ -1292,16 +1395,17 @@ function createUseAIConversation(client) {
|
|
|
1292
1395
|
// between the gen2 schema definition and
|
|
1293
1396
|
// whats in amplify_outputs
|
|
1294
1397
|
if (!clientRoute) {
|
|
1398
|
+
const error = {
|
|
1399
|
+
message: 'Conversation route does not exist',
|
|
1400
|
+
errorInfo: null,
|
|
1401
|
+
errorType: '',
|
|
1402
|
+
};
|
|
1295
1403
|
setDataState({
|
|
1296
1404
|
...ERROR_STATE,
|
|
1297
1405
|
data: { messages: [] },
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
errorInfo: null,
|
|
1302
|
-
errorType: '',
|
|
1303
|
-
},
|
|
1304
|
-
],
|
|
1406
|
+
// TODO in MV bump: remove `messages`
|
|
1407
|
+
messages: [error],
|
|
1408
|
+
errors: [error],
|
|
1305
1409
|
});
|
|
1306
1410
|
return;
|
|
1307
1411
|
}
|
|
@@ -1415,7 +1519,9 @@ function createUseAIConversation(client) {
|
|
|
1415
1519
|
return {
|
|
1416
1520
|
...prev,
|
|
1417
1521
|
...ERROR_STATE,
|
|
1522
|
+
// TODO in MV bump: remove `messages`
|
|
1418
1523
|
messages: error.errors,
|
|
1524
|
+
errors: error.errors,
|
|
1419
1525
|
};
|
|
1420
1526
|
});
|
|
1421
1527
|
},
|
|
@@ -1459,16 +1565,17 @@ function createUseAIConversation(client) {
|
|
|
1459
1565
|
conversation.sendMessage(input);
|
|
1460
1566
|
}
|
|
1461
1567
|
else {
|
|
1568
|
+
const error = {
|
|
1569
|
+
message: 'No conversation found',
|
|
1570
|
+
errorInfo: null,
|
|
1571
|
+
errorType: '',
|
|
1572
|
+
};
|
|
1462
1573
|
setDataState((prev) => ({
|
|
1463
1574
|
...prev,
|
|
1464
1575
|
...ERROR_STATE,
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
errorInfo: null,
|
|
1469
|
-
errorType: '',
|
|
1470
|
-
},
|
|
1471
|
-
],
|
|
1576
|
+
// TODO in MV bump: remove `messages`
|
|
1577
|
+
messages: [error],
|
|
1578
|
+
errors: [error],
|
|
1472
1579
|
}));
|
|
1473
1580
|
}
|
|
1474
1581
|
}, [conversation]);
|
|
@@ -3,4 +3,4 @@ import { AIConversationInput } from '../types';
|
|
|
3
3
|
export interface AttachmentContextProps extends Pick<AIConversationInput, 'allowAttachments' | 'maxAttachments' | 'maxAttachmentSize'> {
|
|
4
4
|
}
|
|
5
5
|
export declare const AttachmentContext: React.Context<Required<AttachmentContextProps>>;
|
|
6
|
-
export declare const AttachmentProvider: ({ children, allowAttachments, maxAttachmentSize, maxAttachments, }: React.PropsWithChildren<AttachmentContextProps>) => JSX.Element;
|
|
6
|
+
export declare const AttachmentProvider: ({ children, allowAttachments, maxAttachmentSize, maxAttachments, }: React.PropsWithChildren<AttachmentContextProps>) => React.JSX.Element;
|
|
@@ -21,4 +21,4 @@ export declare const ControlsContext: React.Context<ControlsContextProps | undef
|
|
|
21
21
|
export declare const ControlsProvider: ({ children, controls, }: {
|
|
22
22
|
children?: React.ReactNode;
|
|
23
23
|
controls?: ControlsContextProps | undefined;
|
|
24
|
-
}) => JSX.Element;
|
|
24
|
+
}) => React.JSX.Element;
|
|
@@ -12,4 +12,4 @@ export interface ConversationInputContextProps {
|
|
|
12
12
|
export declare const ConversationInputContext: React.Context<ConversationInputContextProps>;
|
|
13
13
|
export declare const ConversationInputContextProvider: ({ children, }: {
|
|
14
14
|
children?: React.ReactNode;
|
|
15
|
-
}) => JSX.Element;
|
|
15
|
+
}) => React.JSX.Element;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/// <reference types="react" />
|
|
2
1
|
import { ConversationDisplayText } from '../displayText';
|
|
3
2
|
export declare const ConversationDisplayTextContext: import("react").Context<Required<ConversationDisplayText>>, ConversationDisplayTextProvider: import("react").ComponentType<import("react").PropsWithChildren<Required<ConversationDisplayText>>>, useConversationDisplayText: (params?: {
|
|
4
3
|
errorMessage?: string | undefined;
|
|
@@ -4,4 +4,4 @@ export declare const FallbackComponentContext: React.Context<React.ComponentType
|
|
|
4
4
|
export declare const FallbackComponentProvider: ({ children, FallbackComponent, }: {
|
|
5
5
|
children?: React.ReactNode;
|
|
6
6
|
FallbackComponent?: AIConversationInput['FallbackResponseComponent'];
|
|
7
|
-
}) => JSX.Element;
|
|
7
|
+
}) => React.JSX.Element;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/// <reference types="react" />
|
|
2
1
|
import { MessageRenderer } from '../types';
|
|
3
2
|
export declare const MessageRendererContext: import("react").Context<MessageRenderer>, MessageRendererProvider: import("react").ComponentType<import("react").PropsWithChildren<MessageRenderer>>, useMessageRenderer: (params?: {
|
|
4
3
|
errorMessage?: string | undefined;
|
|
@@ -6,5 +6,5 @@ export declare const RoleContext: React.Context<"user" | "assistant" | undefined
|
|
|
6
6
|
export declare const MessagesProvider: ({ children, messages, }: {
|
|
7
7
|
children?: React.ReactNode;
|
|
8
8
|
messages: ConversationMessage[];
|
|
9
|
-
}) => JSX.Element;
|
|
9
|
+
}) => React.JSX.Element;
|
|
10
10
|
export {};
|
|
@@ -7,6 +7,6 @@ export declare const prependResponseComponents: (responseComponents?: ResponseCo
|
|
|
7
7
|
export declare const ResponseComponentsProvider: ({ children, responseComponents, }: {
|
|
8
8
|
children?: React.ReactNode;
|
|
9
9
|
responseComponents?: ResponseComponents | undefined;
|
|
10
|
-
}) => JSX.Element;
|
|
10
|
+
}) => React.JSX.Element;
|
|
11
11
|
export declare const convertResponseComponentsToToolConfiguration: (responseComponents?: ResponseComponents) => ToolConfiguration | undefined;
|
|
12
12
|
export {};
|
|
@@ -4,4 +4,4 @@ export declare const SendMessageContext: React.Context<SendMessage | undefined>;
|
|
|
4
4
|
export declare const SendMessageContextProvider: ({ children, handleSendMessage, }: {
|
|
5
5
|
children?: React.ReactNode;
|
|
6
6
|
handleSendMessage: SendMessage;
|
|
7
|
-
}) => JSX.Element;
|
|
7
|
+
}) => React.JSX.Element;
|
|
@@ -5,5 +5,5 @@ export declare const SuggestedPromptsContext: React.Context<SuggestedPromptsCont
|
|
|
5
5
|
export declare const SuggestedPromptProvider: ({ children, suggestedPrompts, }: {
|
|
6
6
|
children?: React.ReactNode;
|
|
7
7
|
suggestedPrompts?: SuggestedPrompt[] | undefined;
|
|
8
|
-
}) => JSX.Element;
|
|
8
|
+
}) => React.JSX.Element;
|
|
9
9
|
export {};
|
|
@@ -4,5 +4,5 @@ export declare const WelcomeMessageContext: React.Context<WelcomeMessageContextP
|
|
|
4
4
|
export declare const WelcomeMessageProvider: ({ children, welcomeMessage, }: {
|
|
5
5
|
children?: React.ReactNode;
|
|
6
6
|
welcomeMessage?: React.ReactNode;
|
|
7
|
-
}) => JSX.Element;
|
|
7
|
+
}) => React.JSX.Element;
|
|
8
8
|
export {};
|
|
@@ -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>;
|
|
@@ -34,10 +34,10 @@ export interface AIConversationProps {
|
|
|
34
34
|
aiContext?: () => object;
|
|
35
35
|
}
|
|
36
36
|
export interface AIConversation<PropsType extends AIConversationProps = AIConversationProps> {
|
|
37
|
-
(props: PropsType): JSX.Element;
|
|
38
|
-
DefaultMessage: () => JSX.Element | undefined;
|
|
39
|
-
Messages: () => JSX.Element;
|
|
40
|
-
Form: () => JSX.Element;
|
|
37
|
+
(props: PropsType): React.JSX.Element;
|
|
38
|
+
DefaultMessage: () => React.JSX.Element | undefined;
|
|
39
|
+
Messages: () => React.JSX.Element;
|
|
40
|
+
Form: () => React.JSX.Element;
|
|
41
41
|
Provider: (props: AIConversationProviderProps) => React.JSX.Element;
|
|
42
42
|
}
|
|
43
43
|
export type MessageVariant = 'bubble' | 'default';
|
|
@@ -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
|
}>;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import { AIConversationElements } from '../../context/elements';
|
|
3
3
|
export declare const RemoveButtonControl: RemoveButtonControl;
|
|
4
4
|
interface RemoveButtonControl<T extends Partial<AIConversationElements> = AIConversationElements> {
|
|
5
5
|
(props: {
|
|
6
6
|
onRemove: () => void;
|
|
7
|
-
}): JSX.Element;
|
|
7
|
+
}): React.JSX.Element;
|
|
8
8
|
Button: T['Button'];
|
|
9
9
|
Icon: T['Icon'];
|
|
10
10
|
}
|
|
@@ -13,7 +13,7 @@ interface TextControl<T extends Partial<AIConversationElements> = AIConversation
|
|
|
13
13
|
(props: {
|
|
14
14
|
fileName: string;
|
|
15
15
|
fileSize: number;
|
|
16
|
-
}): JSX.Element;
|
|
16
|
+
}): React.JSX.Element;
|
|
17
17
|
Container: T['View'];
|
|
18
18
|
FileName: T['Text'];
|
|
19
19
|
FileSize: T['Text'];
|
|
@@ -24,7 +24,7 @@ interface AttachmentControl<T extends Partial<AIConversationElements> = AIConver
|
|
|
24
24
|
(props: {
|
|
25
25
|
image: File;
|
|
26
26
|
onRemove: () => void;
|
|
27
|
-
}): JSX.Element;
|
|
27
|
+
}): React.JSX.Element;
|
|
28
28
|
Container: T['ListItem'];
|
|
29
29
|
ImageIcon: T['Icon'];
|
|
30
30
|
RemoveButton: RemoveButtonControl<T>;
|
|
@@ -32,7 +32,7 @@ interface AttachmentControl<T extends Partial<AIConversationElements> = AIConver
|
|
|
32
32
|
}
|
|
33
33
|
export declare const AttachmentListControl: AttachmentListControl;
|
|
34
34
|
export interface AttachmentListControl<T extends Partial<AIConversationElements> = AIConversationElements> {
|
|
35
|
-
(): JSX.Element;
|
|
35
|
+
(): React.JSX.Element;
|
|
36
36
|
List: T['UnorderedList'];
|
|
37
37
|
Item: AttachmentControl<T>;
|
|
38
38
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const DefaultMessageControl: () => JSX.Element | undefined;
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export declare const DefaultMessageControl: () => React.JSX.Element | undefined;
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import React from 'react';
|
|
2
2
|
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: {
|
|
9
10
|
message: ConversationMessage;
|
|
10
|
-
}): JSX.Element;
|
|
11
|
+
}): React.JSX.Element;
|
|
11
12
|
}
|
|
12
13
|
export declare const MessagesControl: MessagesControl;
|
|
13
14
|
export interface MessagesControl {
|
|
14
|
-
(): JSX.Element;
|
|
15
|
+
(): React.JSX.Element;
|
|
15
16
|
ActionsBar: ActionsBarControl;
|
|
16
17
|
Avatar: AvatarControl;
|
|
17
18
|
Container: AIConversationElements['View'];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import * as React from 'react';
|
|
2
2
|
import { ConversationInputContextProps } from '../../context';
|
|
3
3
|
export declare const Attachments: ({ files, setInput, }: {
|
|
4
4
|
files?: File[] | undefined;
|
|
5
5
|
setInput: ConversationInputContextProps['setInput'];
|
|
6
|
-
}) => JSX.Element | null;
|
|
6
|
+
}) => React.JSX.Element | null;
|
|
@@ -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,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export type ConversationRoute = V6Client<any>['conversations'][string];
|
|
1
|
+
import type { ClientExtensions } from '@aws-amplify/data-schema/runtime';
|
|
2
|
+
export type ConversationRoute = ClientExtensions<any>['conversations'][string];
|
|
4
3
|
export type Conversation = NonNullable<Awaited<ReturnType<ConversationRoute['create']>>['data']>;
|
|
5
4
|
export type ConversationStreamEvent = Parameters<Parameters<Conversation['onStreamEvent']>[0]['next']>[0];
|
|
6
5
|
export type ConversationMessage = NonNullable<Awaited<ReturnType<Conversation['sendMessage']>>['data']> & {
|
|
@@ -9,6 +8,7 @@ export type ConversationMessage = NonNullable<Awaited<ReturnType<Conversation['s
|
|
|
9
8
|
export type ConversationMessageContent = ConversationMessage['content'][number];
|
|
10
9
|
export type TextContentBlock = NonNullable<ConversationMessageContent['text']>;
|
|
11
10
|
export type ImageContentBlock = NonNullable<ConversationMessageContent['image']>;
|
|
11
|
+
export type DocumentContentBlock = NonNullable<ConversationMessageContent['document']>;
|
|
12
12
|
export type ToolUseContent = NonNullable<ConversationMessageContent['toolUse']>;
|
|
13
13
|
export type ToolResultContent = NonNullable<ConversationMessageContent['toolResult']>;
|
|
14
14
|
export type InputContent = Exclude<Parameters<Conversation['sendMessage']>[0], string>['content'][number];
|
|
@@ -21,7 +21,7 @@ export interface SendMesageParameters {
|
|
|
21
21
|
toolConfiguration?: ToolConfiguration;
|
|
22
22
|
}
|
|
23
23
|
export type SendMessage = (input: SendMesageParameters) => void;
|
|
24
|
-
type AIClient<T extends Record<any, any>> = Pick<
|
|
24
|
+
type AIClient<T extends Record<any, any>> = Pick<ClientExtensions<T>, 'generations' | 'conversations'>;
|
|
25
25
|
export type getSchema<T> = T extends AIClient<infer Schema> ? Schema : never;
|
|
26
26
|
export interface GraphQLFormattedError {
|
|
27
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
|
-
"react": "^16.14
|
|
48
|
-
"react-dom": "^16.14
|
|
47
|
+
"react": "^16.14 || ^17 || ^18 || ^19",
|
|
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
|
}
|