@aws-amplify/ui-react-ai 1.3.0 → 1.4.1
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/AIConversation.mjs +1 -1
- package/dist/esm/components/AIConversation/context/elements/definitions.mjs +4 -7
- 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 +34 -30
- package/dist/esm/hooks/useAIGeneration.mjs +5 -12
- package/dist/esm/version.mjs +1 -1
- package/dist/index.js +174 -79
- 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 +15 -5
- package/dist/types/hooks/useAIConversation.d.ts +2 -2
- package/dist/types/hooks/useAIGeneration.d.ts +5 -5
- package/dist/types/types.d.ts +4 -3
- package/dist/types/version.d.ts +1 -1
- package/package.json +8 -12
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({
|
|
@@ -288,15 +342,15 @@ const ListItemElement = elements.defineBaseElementWithRef({
|
|
|
288
342
|
type: 'li',
|
|
289
343
|
displayName: 'ListItem',
|
|
290
344
|
});
|
|
291
|
-
|
|
345
|
+
elements.defineBaseElementWithRef({
|
|
292
346
|
type: 'h2',
|
|
293
347
|
displayName: 'Title',
|
|
294
348
|
});
|
|
295
|
-
|
|
349
|
+
elements.defineBaseElementWithRef({
|
|
296
350
|
type: 'img',
|
|
297
351
|
displayName: 'Image',
|
|
298
352
|
});
|
|
299
|
-
|
|
353
|
+
elements.defineBaseElementWithRef({
|
|
300
354
|
type: 'input',
|
|
301
355
|
displayName: 'Input',
|
|
302
356
|
});
|
|
@@ -315,10 +369,7 @@ const TextAreaElement = elements.defineBaseElementWithRef({
|
|
|
315
369
|
});
|
|
316
370
|
const AIConversationElements = {
|
|
317
371
|
Button: ButtonElement,
|
|
318
|
-
Heading: HeadingElement,
|
|
319
372
|
Icon: IconElement,
|
|
320
|
-
Input: InputElement,
|
|
321
|
-
Image: ImageElement,
|
|
322
373
|
Label: LabelElement$1,
|
|
323
374
|
ListItem: ListItemElement,
|
|
324
375
|
Span: SpanElement,
|
|
@@ -605,6 +656,8 @@ const TextInput = React__namespace["default"].forwardRef(function TextInput(prop
|
|
|
605
656
|
const InputContainer = elements.withBaseElementProps(View$2, {
|
|
606
657
|
className: `${FIELD_BLOCK}__input-container`,
|
|
607
658
|
});
|
|
659
|
+
const isConversationInputWithText = (input) => !!input?.text;
|
|
660
|
+
const isConversationInputWithFiles = (input) => !!input?.files?.length;
|
|
608
661
|
const FormControl = () => {
|
|
609
662
|
const { input, setInput, error, setError } = React__namespace["default"].useContext(ConversationInputContext);
|
|
610
663
|
const handleSendMessage = React__namespace["default"].useContext(SendMessageContext);
|
|
@@ -616,25 +669,46 @@ const FormControl = () => {
|
|
|
616
669
|
const ref = React__namespace["default"].useRef(null);
|
|
617
670
|
const controls = React__namespace["default"].useContext(ControlsContext);
|
|
618
671
|
const [composing, setComposing] = React__namespace["default"].useState(false);
|
|
672
|
+
const [isSubmitting, setIsSubmitting] = React__namespace["default"].useState(false);
|
|
673
|
+
const isInputText = isConversationInputWithText(input);
|
|
674
|
+
// an empty array will resolve false when evaluating the length
|
|
675
|
+
const isInputFiles = isConversationInputWithFiles(input);
|
|
619
676
|
const submitMessage = async () => {
|
|
620
|
-
|
|
677
|
+
const hasInput = isInputFiles || isInputText;
|
|
678
|
+
// Prevent double submission and empty submission
|
|
679
|
+
if (isSubmitting || !hasInput) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
setIsSubmitting(true);
|
|
621
683
|
const submittedContent = [];
|
|
622
|
-
if (
|
|
684
|
+
if (isInputText) {
|
|
623
685
|
const textContent = {
|
|
624
686
|
text: input.text,
|
|
625
687
|
};
|
|
626
688
|
submittedContent.push(textContent);
|
|
627
689
|
}
|
|
628
|
-
if (
|
|
690
|
+
if (isInputFiles) {
|
|
629
691
|
for (const file of input.files) {
|
|
630
692
|
const buffer = await file.arrayBuffer();
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
693
|
+
const format = getAttachmentFormat(file);
|
|
694
|
+
const source = { bytes: new Uint8Array(buffer) };
|
|
695
|
+
if (isDocumentFormat(format)) {
|
|
696
|
+
submittedContent.push({
|
|
697
|
+
document: {
|
|
698
|
+
name: getValidDocumentName(file),
|
|
699
|
+
format,
|
|
700
|
+
source,
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
else if (isImageFormat(format)) {
|
|
705
|
+
submittedContent.push({
|
|
706
|
+
image: {
|
|
707
|
+
format,
|
|
708
|
+
source,
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
}
|
|
638
712
|
}
|
|
639
713
|
}
|
|
640
714
|
if (handleSendMessage) {
|
|
@@ -644,6 +718,12 @@ const FormControl = () => {
|
|
|
644
718
|
toolConfiguration: convertResponseComponentsToToolConfiguration(responseComponents),
|
|
645
719
|
});
|
|
646
720
|
}
|
|
721
|
+
// Clear the attachment errors when submitting
|
|
722
|
+
// because the errors are not actually preventing the submission
|
|
723
|
+
// but rather notifying the user that certain files were not attached and why they weren't
|
|
724
|
+
setError?.(undefined);
|
|
725
|
+
setIsSubmitting(false);
|
|
726
|
+
ref.current?.reset();
|
|
647
727
|
if (setInput)
|
|
648
728
|
setInput({ text: '', files: [] });
|
|
649
729
|
};
|
|
@@ -655,24 +735,26 @@ const FormControl = () => {
|
|
|
655
735
|
const { key, shiftKey } = event;
|
|
656
736
|
if (key === 'Enter' && !shiftKey && !composing) {
|
|
657
737
|
event.preventDefault();
|
|
658
|
-
|
|
659
|
-
if (hasInput) {
|
|
660
|
-
submitMessage();
|
|
661
|
-
}
|
|
738
|
+
submitMessage();
|
|
662
739
|
}
|
|
663
740
|
};
|
|
664
741
|
const onValidate = React__namespace["default"].useCallback(async (files) => {
|
|
665
742
|
const previousFiles = input?.files ?? [];
|
|
666
|
-
const { acceptedFiles, hasMaxAttachmentsError, hasMaxAttachmentSizeError, } = await attachmentsValidator({
|
|
743
|
+
const { acceptedFiles, hasMaxAttachmentsError, hasMaxAttachmentSizeError, hasUnsupportedFileError, } = await attachmentsValidator({
|
|
667
744
|
files: [...files, ...previousFiles],
|
|
668
745
|
maxAttachments,
|
|
669
746
|
maxAttachmentSize,
|
|
670
747
|
});
|
|
671
|
-
if (hasMaxAttachmentsError ||
|
|
748
|
+
if (hasMaxAttachmentsError ||
|
|
749
|
+
hasMaxAttachmentSizeError ||
|
|
750
|
+
hasUnsupportedFileError) {
|
|
672
751
|
const errors = [];
|
|
673
752
|
if (hasMaxAttachmentsError) {
|
|
674
753
|
errors.push(displayText.getMaxAttachmentErrorText(maxAttachments));
|
|
675
754
|
}
|
|
755
|
+
if (hasUnsupportedFileError) {
|
|
756
|
+
errors.push(displayText.getAttachmentFormatErrorText([...validFileTypes]));
|
|
757
|
+
}
|
|
676
758
|
if (hasMaxAttachmentSizeError) {
|
|
677
759
|
errors.push(displayText.getAttachmentSizeErrorText(
|
|
678
760
|
// base64 size is about 137% that of the file size
|
|
@@ -690,7 +772,7 @@ const FormControl = () => {
|
|
|
690
772
|
}));
|
|
691
773
|
}, [setInput, input, displayText, maxAttachmentSize, maxAttachments, setError]);
|
|
692
774
|
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 }));
|
|
775
|
+
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
776
|
}
|
|
695
777
|
return (React__namespace["default"].createElement("form", { className: `${FIELD_BLOCK}__form`, onSubmit: handleSubmit, method: "post", ref: ref },
|
|
696
778
|
allowAttachments ? React__namespace["default"].createElement(AttachFileControl, null) : null,
|
|
@@ -741,6 +823,17 @@ const ToolContent = ({ toolUse, }) => {
|
|
|
741
823
|
}
|
|
742
824
|
}
|
|
743
825
|
};
|
|
826
|
+
const DocumentContent = ({ format, name, source, }) => {
|
|
827
|
+
const icons = internal.useIcons('aiConversation');
|
|
828
|
+
const fileIcon = icons?.document ?? React__namespace["default"].createElement(internal.IconDocument, null);
|
|
829
|
+
return (React__namespace["default"].createElement(View$1, { className: ui.ComponentClassName.AIConversationAttachment },
|
|
830
|
+
fileIcon,
|
|
831
|
+
React__namespace["default"].createElement(View$1, { className: ui.ComponentClassName.AIConversationAttachmentName },
|
|
832
|
+
name,
|
|
833
|
+
".",
|
|
834
|
+
format),
|
|
835
|
+
React__namespace["default"].createElement(View$1, { className: ui.ComponentClassName.AIConversationAttachmentSize }, ui.humanFileSize(source.bytes.length, true))));
|
|
836
|
+
};
|
|
744
837
|
const MessageControl = ({ message }) => {
|
|
745
838
|
const messageRenderer = React__namespace["default"].useContext(MessageRendererContext);
|
|
746
839
|
return (React__namespace["default"].createElement(React__namespace["default"].Fragment, null, message.content.map((content, index) => {
|
|
@@ -750,6 +843,9 @@ const MessageControl = ({ message }) => {
|
|
|
750
843
|
else if (content.image) {
|
|
751
844
|
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
845
|
}
|
|
846
|
+
else if (content.document) {
|
|
847
|
+
return React__namespace["default"].createElement(DocumentContent, { key: index, ...content.document });
|
|
848
|
+
}
|
|
753
849
|
else if (content.toolUse) {
|
|
754
850
|
return React__namespace["default"].createElement(ToolContent, { toolUse: content.toolUse, key: index });
|
|
755
851
|
}
|
|
@@ -988,6 +1084,7 @@ const MessageList = ({ messages, }) => {
|
|
|
988
1084
|
const isLoading = React__namespace.useContext(LoadingContext);
|
|
989
1085
|
const messagesWithRenderableContent = messages?.filter((message) => message.content.some((content) => content.image ??
|
|
990
1086
|
content.text ??
|
|
1087
|
+
content.document ??
|
|
991
1088
|
content.toolUse?.name.startsWith(RESPONSE_COMPONENT_PREFIX))) ?? [];
|
|
992
1089
|
return (React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageList },
|
|
993
1090
|
isLoading ? (React__namespace.createElement(React__namespace.Fragment, null,
|
|
@@ -999,8 +1096,11 @@ const MessageList = ({ messages, }) => {
|
|
|
999
1096
|
const Attachment = ({ file, handleRemove, }) => {
|
|
1000
1097
|
const icons = internal.useIcons('aiConversation');
|
|
1001
1098
|
const removeIcon = icons?.remove ?? React__namespace.createElement(internal.IconClose, null);
|
|
1099
|
+
const fileIcon = icons?.document ?? React__namespace.createElement(internal.IconDocument, null);
|
|
1100
|
+
const format = getAttachmentFormat(file);
|
|
1101
|
+
const isDocument = documentFileTypes.has(format);
|
|
1002
1102
|
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 }),
|
|
1103
|
+
isDocument ? (fileIcon) : (React__namespace.createElement(uiReact.Image, { className: ui.ComponentClassName.AIConversationAttachmentImage, src: URL.createObjectURL(file), alt: file.name })),
|
|
1004
1104
|
React__namespace.createElement(uiReact.Text, { as: "span", className: ui.ComponentClassName.AIConversationAttachmentName }, file.name),
|
|
1005
1105
|
React__namespace.createElement(uiReact.Text, { as: "span", className: ui.ComponentClassName.AIConversationAttachmentSize }, ui.humanFileSize(file.size, true)),
|
|
1006
1106
|
React__namespace.createElement(uiReact.Button, { size: "small", variation: "link", colorTheme: "error", className: ui.ComponentClassName.AIConversationAttachmentRemove, onClick: handleRemove }, removeIcon)));
|
|
@@ -1041,8 +1141,9 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
|
|
|
1041
1141
|
const sendIcon = icons?.send ?? React__namespace.createElement(internal.IconSend, null);
|
|
1042
1142
|
const attachIcon = icons?.attach ?? React__namespace.createElement(internal.IconAttach, null);
|
|
1043
1143
|
const hiddenInput = React__namespace.useRef(null);
|
|
1144
|
+
// Bedrock does not accept message that are empty or are only whitespace
|
|
1145
|
+
const isInputEmpty = !input?.text?.length || !!input.text.match(/^\s+$/);
|
|
1044
1146
|
const [composing, setComposing] = React__namespace.useState(false);
|
|
1045
|
-
const isInputEmpty = !input?.text?.length && !input?.files?.length;
|
|
1046
1147
|
return (React__namespace.createElement(FormWrapper, { onValidate: onValidate, allowAttachments: allowAttachments },
|
|
1047
1148
|
React__namespace.createElement(uiReact.View, { as: "form", className: ui.ComponentClassName.AIConversationForm, onSubmit: handleSubmit },
|
|
1048
1149
|
allowAttachments ? (React__namespace.createElement(uiReact.Button, { className: ui.ComponentClassName.AIConversationFormAttach, onClick: () => {
|
|
@@ -1058,7 +1159,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
|
|
|
1058
1159
|
return;
|
|
1059
1160
|
}
|
|
1060
1161
|
onValidate(Array.from(e.target.files));
|
|
1061
|
-
}, multiple: true, accept:
|
|
1162
|
+
}, multiple: true, accept: [...validFileTypes].map((type) => `.${type}`).join(','), "data-testid": "hidden-file-input" })))) : null,
|
|
1062
1163
|
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
1164
|
// Submit on enter key if shift is not pressed also
|
|
1064
1165
|
const shouldSubmit = !e.shiftKey && e.key === 'Enter' && !composing;
|
|
@@ -1072,10 +1173,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
|
|
|
1072
1173
|
text: e.target.value,
|
|
1073
1174
|
}));
|
|
1074
1175
|
} }),
|
|
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 },
|
|
1176
|
+
React__namespace.createElement(uiReact.Button, { type: "submit", variation: "primary", className: ui.ComponentClassName.AIConversationFormSend, isDisabled: isLoading ?? isInputEmpty },
|
|
1079
1177
|
React__namespace.createElement("span", null, sendIcon))),
|
|
1080
1178
|
error ? (React__namespace.createElement(uiReact.Message, { className: ui.ComponentClassName.AIConversationFormError, variation: "plain", colorTheme: "warning" }, error)) : null,
|
|
1081
1179
|
React__namespace.createElement(Attachments, { setInput: setInput, files: input?.files })));
|
|
@@ -1092,7 +1190,7 @@ const PromptList = ({ setInput, suggestedPrompts = [], }) => {
|
|
|
1092
1190
|
})));
|
|
1093
1191
|
};
|
|
1094
1192
|
|
|
1095
|
-
const VERSION = '1.
|
|
1193
|
+
const VERSION = '1.4.1';
|
|
1096
1194
|
|
|
1097
1195
|
function AIConversationBase({ avatars, controls, ...rest }) {
|
|
1098
1196
|
uiReactCore.useSetUserAgent({
|
|
@@ -1153,26 +1251,19 @@ const ERROR_STATE = { hasError: true, isLoading: false };
|
|
|
1153
1251
|
|
|
1154
1252
|
function createUseAIGeneration(client) {
|
|
1155
1253
|
const useAIGeneration = (routeName) => {
|
|
1156
|
-
const [
|
|
1157
|
-
...INITIAL_STATE,
|
|
1158
|
-
data: undefined,
|
|
1159
|
-
}));
|
|
1254
|
+
const [clientState, setClientState] = React__namespace.useState(() => ({ ...INITIAL_STATE, data: undefined }));
|
|
1160
1255
|
const handleGeneration = React__namespace.useCallback(async (input) => {
|
|
1161
|
-
|
|
1256
|
+
setClientState(({ data }) => ({ ...LOADING_STATE, data }));
|
|
1162
1257
|
const result = await client.generations[routeName](input);
|
|
1163
1258
|
const { data, errors } = result;
|
|
1164
1259
|
if (errors) {
|
|
1165
|
-
|
|
1166
|
-
...ERROR_STATE,
|
|
1167
|
-
data,
|
|
1168
|
-
messages: errors,
|
|
1169
|
-
});
|
|
1260
|
+
setClientState({ ...ERROR_STATE, data, messages: errors });
|
|
1170
1261
|
}
|
|
1171
1262
|
else {
|
|
1172
|
-
|
|
1263
|
+
setClientState({ ...INITIAL_STATE, data });
|
|
1173
1264
|
}
|
|
1174
1265
|
}, [routeName]);
|
|
1175
|
-
return [
|
|
1266
|
+
return [clientState, handleGeneration];
|
|
1176
1267
|
};
|
|
1177
1268
|
return useAIGeneration;
|
|
1178
1269
|
}
|
|
@@ -1239,11 +1330,11 @@ function createUseAIConversation(client) {
|
|
|
1239
1330
|
// it will create a new conversation when it is executed
|
|
1240
1331
|
// we don't want to create 2 conversations
|
|
1241
1332
|
const initRef = React__namespace["default"].useRef('initial');
|
|
1242
|
-
const [
|
|
1333
|
+
const [clientState, setClientState] = React__namespace["default"].useState(() => ({
|
|
1243
1334
|
...INITIAL_STATE,
|
|
1244
1335
|
data: { messages: [], conversation: undefined },
|
|
1245
1336
|
}));
|
|
1246
|
-
const { conversation } =
|
|
1337
|
+
const { conversation } = clientState.data;
|
|
1247
1338
|
const { id, onInitialize, onMessage } = input;
|
|
1248
1339
|
React__namespace["default"].useEffect(() => {
|
|
1249
1340
|
async function initialize() {
|
|
@@ -1255,7 +1346,7 @@ function createUseAIConversation(client) {
|
|
|
1255
1346
|
// Only show component loading state if we are
|
|
1256
1347
|
// actually loading messages
|
|
1257
1348
|
if (id) {
|
|
1258
|
-
|
|
1349
|
+
setClientState({
|
|
1259
1350
|
...LOADING_STATE,
|
|
1260
1351
|
data: { messages: [], conversation: undefined },
|
|
1261
1352
|
});
|
|
@@ -1264,7 +1355,7 @@ function createUseAIConversation(client) {
|
|
|
1264
1355
|
? await clientRoute.get({ id })
|
|
1265
1356
|
: await clientRoute.create();
|
|
1266
1357
|
if (errors ?? !conversation) {
|
|
1267
|
-
|
|
1358
|
+
setClientState({
|
|
1268
1359
|
...ERROR_STATE,
|
|
1269
1360
|
data: { messages: [] },
|
|
1270
1361
|
messages: errors,
|
|
@@ -1275,13 +1366,13 @@ function createUseAIConversation(client) {
|
|
|
1275
1366
|
const { data: messages } = await exhaustivelyListMessages({
|
|
1276
1367
|
conversation,
|
|
1277
1368
|
});
|
|
1278
|
-
|
|
1369
|
+
setClientState({
|
|
1279
1370
|
...INITIAL_STATE,
|
|
1280
1371
|
data: { messages, conversation },
|
|
1281
1372
|
});
|
|
1282
1373
|
}
|
|
1283
1374
|
else {
|
|
1284
|
-
|
|
1375
|
+
setClientState({
|
|
1285
1376
|
...INITIAL_STATE,
|
|
1286
1377
|
data: { conversation, messages: [] },
|
|
1287
1378
|
});
|
|
@@ -1294,16 +1385,17 @@ function createUseAIConversation(client) {
|
|
|
1294
1385
|
// between the gen2 schema definition and
|
|
1295
1386
|
// whats in amplify_outputs
|
|
1296
1387
|
if (!clientRoute) {
|
|
1297
|
-
|
|
1388
|
+
const error = {
|
|
1389
|
+
message: 'Conversation route does not exist',
|
|
1390
|
+
errorInfo: null,
|
|
1391
|
+
errorType: '',
|
|
1392
|
+
};
|
|
1393
|
+
setClientState({
|
|
1298
1394
|
...ERROR_STATE,
|
|
1299
1395
|
data: { messages: [] },
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
errorInfo: null,
|
|
1304
|
-
errorType: '',
|
|
1305
|
-
},
|
|
1306
|
-
],
|
|
1396
|
+
// TODO in MV bump: remove `messages`
|
|
1397
|
+
messages: [error],
|
|
1398
|
+
errors: [error],
|
|
1307
1399
|
});
|
|
1308
1400
|
return;
|
|
1309
1401
|
}
|
|
@@ -1312,12 +1404,12 @@ function createUseAIConversation(client) {
|
|
|
1312
1404
|
contentBlocksRef.current = undefined;
|
|
1313
1405
|
if (hasStarted(initRef.current))
|
|
1314
1406
|
return;
|
|
1315
|
-
|
|
1407
|
+
setClientState({
|
|
1316
1408
|
...INITIAL_STATE,
|
|
1317
1409
|
data: { messages: [], conversation: undefined },
|
|
1318
1410
|
});
|
|
1319
1411
|
};
|
|
1320
|
-
}, [clientRoute, id,
|
|
1412
|
+
}, [clientRoute, id, setClientState]);
|
|
1321
1413
|
// Run a separate effect that is triggered by the conversation state
|
|
1322
1414
|
// so that we know we have a conversation object to set up the subscription
|
|
1323
1415
|
// and also unsubscribe on cleanup
|
|
@@ -1345,7 +1437,7 @@ function createUseAIConversation(client) {
|
|
|
1345
1437
|
// stop reason will signify end of conversation turn
|
|
1346
1438
|
if (stopReason) {
|
|
1347
1439
|
// remove loading state from streamed message
|
|
1348
|
-
|
|
1440
|
+
setClientState((prev) => {
|
|
1349
1441
|
return {
|
|
1350
1442
|
...prev,
|
|
1351
1443
|
data: {
|
|
@@ -1392,7 +1484,7 @@ function createUseAIConversation(client) {
|
|
|
1392
1484
|
];
|
|
1393
1485
|
}
|
|
1394
1486
|
}
|
|
1395
|
-
|
|
1487
|
+
setClientState((prev) => {
|
|
1396
1488
|
const message = {
|
|
1397
1489
|
id,
|
|
1398
1490
|
conversationId,
|
|
@@ -1413,11 +1505,13 @@ function createUseAIConversation(client) {
|
|
|
1413
1505
|
});
|
|
1414
1506
|
},
|
|
1415
1507
|
error: (error) => {
|
|
1416
|
-
|
|
1508
|
+
setClientState((prev) => {
|
|
1417
1509
|
return {
|
|
1418
1510
|
...prev,
|
|
1419
1511
|
...ERROR_STATE,
|
|
1512
|
+
// TODO in MV bump: remove `messages`
|
|
1420
1513
|
messages: error.errors,
|
|
1514
|
+
errors: error.errors,
|
|
1421
1515
|
};
|
|
1422
1516
|
});
|
|
1423
1517
|
},
|
|
@@ -1429,11 +1523,11 @@ function createUseAIConversation(client) {
|
|
|
1429
1523
|
contentBlocksRef.current = undefined;
|
|
1430
1524
|
subscription.unsubscribe();
|
|
1431
1525
|
};
|
|
1432
|
-
}, [conversation, onInitialize, onMessage,
|
|
1526
|
+
}, [conversation, onInitialize, onMessage, setClientState]);
|
|
1433
1527
|
const handleSendMessage = React__namespace["default"].useCallback((input) => {
|
|
1434
1528
|
const { content } = input;
|
|
1435
1529
|
if (conversation) {
|
|
1436
|
-
|
|
1530
|
+
setClientState((prevState) => ({
|
|
1437
1531
|
...prevState,
|
|
1438
1532
|
data: {
|
|
1439
1533
|
...prevState.data,
|
|
@@ -1461,20 +1555,21 @@ function createUseAIConversation(client) {
|
|
|
1461
1555
|
conversation.sendMessage(input);
|
|
1462
1556
|
}
|
|
1463
1557
|
else {
|
|
1464
|
-
|
|
1558
|
+
const error = {
|
|
1559
|
+
message: 'No conversation found',
|
|
1560
|
+
errorInfo: null,
|
|
1561
|
+
errorType: '',
|
|
1562
|
+
};
|
|
1563
|
+
setClientState((prev) => ({
|
|
1465
1564
|
...prev,
|
|
1466
1565
|
...ERROR_STATE,
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
errorInfo: null,
|
|
1471
|
-
errorType: '',
|
|
1472
|
-
},
|
|
1473
|
-
],
|
|
1566
|
+
// TODO in MV bump: remove `messages`
|
|
1567
|
+
messages: [error],
|
|
1568
|
+
errors: [error],
|
|
1474
1569
|
}));
|
|
1475
1570
|
}
|
|
1476
1571
|
}, [conversation]);
|
|
1477
|
-
return [
|
|
1572
|
+
return [clientState, handleSendMessage];
|
|
1478
1573
|
};
|
|
1479
1574
|
return useAIConversation;
|
|
1480
1575
|
}
|
|
@@ -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,12 +1,22 @@
|
|
|
1
|
-
import { DataState } from '@aws-amplify/ui-react-core';
|
|
2
1
|
import { GraphQLFormattedError } from '../types';
|
|
3
|
-
export
|
|
2
|
+
export interface AiClientState<T> {
|
|
3
|
+
data: T;
|
|
4
|
+
hasError: boolean;
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated will be removed in a future major version. Superseded by `errors`
|
|
8
|
+
* @description errors returned from the websocket connection
|
|
9
|
+
*/
|
|
4
10
|
messages?: GraphQLFormattedError[];
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
/**
|
|
12
|
+
* @description errors returned from the websocket connection
|
|
13
|
+
*/
|
|
14
|
+
errors?: GraphQLFormattedError[];
|
|
15
|
+
}
|
|
16
|
+
export interface AiClientResponse<T> {
|
|
7
17
|
data: T | null;
|
|
8
18
|
errors?: GraphQLFormattedError[];
|
|
9
|
-
}
|
|
19
|
+
}
|
|
10
20
|
export declare const INITIAL_STATE: {
|
|
11
21
|
hasError: boolean;
|
|
12
22
|
isLoading: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Conversation, ConversationMessage, ConversationRoute, SendMessage } from '../types';
|
|
2
|
-
import {
|
|
2
|
+
import { AiClientState } from './shared';
|
|
3
3
|
interface UseAIConversationInput {
|
|
4
4
|
id?: string;
|
|
5
5
|
onMessage?: (message: ConversationMessage) => void;
|
|
@@ -9,6 +9,6 @@ interface AIConversationState {
|
|
|
9
9
|
messages: ConversationMessage[];
|
|
10
10
|
conversation?: Conversation;
|
|
11
11
|
}
|
|
12
|
-
export type UseAIConversationHook<T extends string> = (routeName: T, input?: UseAIConversationInput) => [
|
|
12
|
+
export type UseAIConversationHook<T extends string> = (routeName: T, input?: UseAIConversationInput) => [AiClientState<AIConversationState>, SendMessage];
|
|
13
13
|
export declare function createUseAIConversation<T extends Record<'conversations', Record<string, ConversationRoute>>>(client: T): UseAIConversationHook<Extract<keyof T['conversations'], string>>;
|
|
14
14
|
export {};
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ClientExtensions } from '@aws-amplify/data-schema/runtime';
|
|
2
2
|
import { getSchema } from '../types';
|
|
3
|
-
import {
|
|
3
|
+
import { AiClientState } from './shared';
|
|
4
4
|
export interface UseAIGenerationHookWrapper<Key extends keyof AIGenerationClient<Schema>['generations'], Schema extends Record<any, any>> {
|
|
5
5
|
useAIGeneration: <U extends Key>(routeName: U) => [
|
|
6
|
-
Awaited<
|
|
6
|
+
Awaited<AiClientState<Schema[U]['returnType']>>,
|
|
7
7
|
(input: Schema[U]['args']) => void
|
|
8
8
|
];
|
|
9
9
|
}
|
|
10
10
|
export type UseAIGenerationHook<Key extends keyof AIGenerationClient<Schema>['generations'], Schema extends Record<any, any>> = (routeName: Key) => [
|
|
11
|
-
Awaited<
|
|
11
|
+
Awaited<AiClientState<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 {};
|