@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.
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { Flex, ScrollView } from '@aws-amplify/ui-react';
3
- import { useIcons, IconAssistant, IconUser } from '@aws-amplify/ui-react/internal';
3
+ import { useIcons, IconUser, IconAssistant } from '@aws-amplify/ui-react/internal';
4
4
  import { MessagesControl } from './views/Controls/MessagesControl.mjs';
5
5
  import { FormControl } from './views/Controls/FormControl.mjs';
6
6
  import { MessageList } from './views/default/MessageList.mjs';
@@ -17,15 +17,15 @@ const ListItemElement = defineBaseElementWithRef({
17
17
  type: 'li',
18
18
  displayName: 'ListItem',
19
19
  });
20
- const HeadingElement = defineBaseElementWithRef({
20
+ defineBaseElementWithRef({
21
21
  type: 'h2',
22
22
  displayName: 'Title',
23
23
  });
24
- const ImageElement = defineBaseElementWithRef({
24
+ defineBaseElementWithRef({
25
25
  type: 'img',
26
26
  displayName: 'Image',
27
27
  });
28
- const InputElement = defineBaseElementWithRef({
28
+ defineBaseElementWithRef({
29
29
  type: 'input',
30
30
  displayName: 'Input',
31
31
  });
@@ -44,10 +44,7 @@ const TextAreaElement = defineBaseElementWithRef({
44
44
  });
45
45
  const AIConversationElements = {
46
46
  Button: ButtonElement,
47
- Heading: HeadingElement,
48
47
  Icon: IconElement,
49
- Input: InputElement,
50
- Image: ImageElement,
51
48
  Label: LabelElement,
52
49
  ListItem: ListItemElement,
53
50
  Span: SpanElement,
@@ -57,4 +54,4 @@ const AIConversationElements = {
57
54
  View: ViewElement,
58
55
  };
59
56
 
60
- export { AIConversationElements, ButtonElement, HeadingElement, ImageElement, InputElement, LabelElement, ListItemElement, SpanElement, TextAreaElement, TextElement, UnorderedListElement, ViewElement };
57
+ export { AIConversationElements, ButtonElement, LabelElement, ListItemElement, SpanElement, TextAreaElement, TextElement, UnorderedListElement, ViewElement };
@@ -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 getImageTypeFromMimeType(mimeType) {
35
- return mimeType.split('/')[1];
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 hasMaxSizeError = false;
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
- hasMaxSizeError = true;
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, getImageTypeFromMimeType };
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, getImageTypeFromMimeType } from '../../utils.mjs';
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
- ref.current?.reset();
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 (input?.text) {
130
+ if (isInputText) {
120
131
  const textContent = {
121
132
  text: input.text,
122
133
  };
123
134
  submittedContent.push(textContent);
124
135
  }
125
- if (input?.files) {
136
+ if (isInputFiles) {
126
137
  for (const file of input.files) {
127
138
  const buffer = await file.arrayBuffer();
128
- const fileContent = {
129
- image: {
130
- format: getImageTypeFromMimeType(file.type),
131
- source: { bytes: new Uint8Array(buffer) },
132
- },
133
- };
134
- submittedContent.push(fileContent);
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
- const hasInput = !!input?.text || (input?.files?.length && input?.files?.length > 0);
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 || hasMaxAttachmentSizeError) {
194
+ if (hasMaxAttachmentsError ||
195
+ hasMaxAttachmentSizeError ||
196
+ hasUnsupportedFileError) {
169
197
  const errors = [];
170
198
  if (hasMaxAttachmentsError) {
171
199
  errors.push(displayText.getMaxAttachmentErrorText(maxAttachments));
172
200
  }
201
+ if (hasUnsupportedFileError) {
202
+ errors.push(displayText.getAttachmentFormatErrorText([...validFileTypes]));
203
+ }
173
204
  if (hasMaxAttachmentSizeError) {
174
205
  errors.push(displayText.getAttachmentSizeErrorText(
175
206
  // base64 size is about 137% that of the file size
@@ -187,7 +218,7 @@ const FormControl = () => {
187
218
  }));
188
219
  }, [setInput, input, displayText, maxAttachmentSize, maxAttachments, setError]);
189
220
  if (controls?.Form) {
190
- return (React__default.createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, onValidate: onValidate, allowAttachments: allowAttachments, isLoading: isLoading, error: error, setError: setError }));
221
+ return (React__default.createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, onValidate: onValidate, allowAttachments: allowAttachments, isLoading: isLoading ?? isSubmitting, error: error, setError: setError }));
191
222
  }
192
223
  return (React__default.createElement("form", { className: `${FIELD_BLOCK}__form`, onSubmit: handleSubmit, method: "post", ref: ref },
193
224
  allowAttachments ? React__default.createElement(AttachFileControl, null) : null,
@@ -1,5 +1,7 @@
1
1
  import React__default from 'react';
2
+ import { classNames, ComponentClassName, humanFileSize } from '@aws-amplify/ui';
2
3
  import { Image } from '@aws-amplify/ui-react';
4
+ import { useIcons, IconDocument } from '@aws-amplify/ui-react/internal';
3
5
  import { withBaseElementProps } from '@aws-amplify/ui-react-core/elements';
4
6
  import '../../context/AIContextContext.mjs';
5
7
  import '../../context/ActionsContext.mjs';
@@ -21,7 +23,6 @@ import { AIConversationElements } from '../../context/elements/definitions.mjs';
21
23
  import { convertBufferToBase64 } from '../../utils.mjs';
22
24
  import { ActionsBarControl } from './ActionsBarControl.mjs';
23
25
  import { AvatarControl } from './AvatarControl.mjs';
24
- import { classNames } from '@aws-amplify/ui';
25
26
 
26
27
  const { Text, View } = AIConversationElements;
27
28
  const MESSAGES_BLOCK = 'amplify-ai-conversation__message__list';
@@ -55,6 +56,17 @@ const ToolContent = ({ toolUse, }) => {
55
56
  }
56
57
  }
57
58
  };
59
+ const DocumentContent = ({ format, name, source, }) => {
60
+ const icons = useIcons('aiConversation');
61
+ const fileIcon = icons?.document ?? React__default.createElement(IconDocument, null);
62
+ return (React__default.createElement(View, { className: ComponentClassName.AIConversationAttachment },
63
+ fileIcon,
64
+ React__default.createElement(View, { className: ComponentClassName.AIConversationAttachmentName },
65
+ name,
66
+ ".",
67
+ format),
68
+ React__default.createElement(View, { className: ComponentClassName.AIConversationAttachmentSize }, humanFileSize(source.bytes.length, true))));
69
+ };
58
70
  const MessageControl = ({ message }) => {
59
71
  const messageRenderer = React__default.useContext(MessageRendererContext);
60
72
  return (React__default.createElement(React__default.Fragment, null, message.content.map((content, index) => {
@@ -64,6 +76,9 @@ const MessageControl = ({ message }) => {
64
76
  else if (content.image) {
65
77
  return messageRenderer?.image ? (React__default.createElement(React__default.Fragment, { key: index }, messageRenderer?.image({ image: content.image }))) : (React__default.createElement(MediaContent, { "data-testid": 'image-content', key: index, alt: "", src: convertBufferToBase64(content.image?.source.bytes, content.image?.format) }));
66
78
  }
79
+ else if (content.document) {
80
+ return React__default.createElement(DocumentContent, { key: index, ...content.document });
81
+ }
67
82
  else if (content.toolUse) {
68
83
  return React__default.createElement(ToolContent, { toolUse: content.toolUse, key: index });
69
84
  }
@@ -143,4 +158,4 @@ MessagesControl.HeaderContainer = HeaderContainer;
143
158
  MessagesControl.Layout = Layout;
144
159
  MessagesControl.Message = MessageControl;
145
160
 
146
- export { MessageControl, MessagesControl };
161
+ export { DocumentContent, MessageControl, MessagesControl };
@@ -1,13 +1,17 @@
1
1
  import * as React from 'react';
2
2
  import { View, Image, Text, Button } from '@aws-amplify/ui-react';
3
- import { useIcons, IconClose } from '@aws-amplify/ui-react/internal';
3
+ import { useIcons, IconClose, IconDocument } from '@aws-amplify/ui-react/internal';
4
4
  import { ComponentClassName, humanFileSize } from '@aws-amplify/ui';
5
+ import { getAttachmentFormat, documentFileTypes } from '../../utils.mjs';
5
6
 
6
7
  const Attachment = ({ file, handleRemove, }) => {
7
8
  const icons = useIcons('aiConversation');
8
9
  const removeIcon = icons?.remove ?? React.createElement(IconClose, null);
10
+ const fileIcon = icons?.document ?? React.createElement(IconDocument, null);
11
+ const format = getAttachmentFormat(file);
12
+ const isDocument = documentFileTypes.has(format);
9
13
  return (React.createElement(View, { className: ComponentClassName.AIConversationAttachment },
10
- React.createElement(Image, { className: ComponentClassName.AIConversationAttachmentImage, src: URL.createObjectURL(file), alt: file.name }),
14
+ isDocument ? (fileIcon) : (React.createElement(Image, { className: ComponentClassName.AIConversationAttachmentImage, src: URL.createObjectURL(file), alt: file.name })),
11
15
  React.createElement(Text, { as: "span", className: ComponentClassName.AIConversationAttachmentName }, file.name),
12
16
  React.createElement(Text, { as: "span", className: ComponentClassName.AIConversationAttachmentSize }, humanFileSize(file.size, true)),
13
17
  React.createElement(Button, { size: "small", variation: "link", colorTheme: "error", className: ComponentClassName.AIConversationAttachmentRemove, onClick: handleRemove }, removeIcon)));
@@ -3,6 +3,7 @@ import { View, Button, VisuallyHidden, TextAreaField, Message, DropZone } from '
3
3
  import { useIcons, IconSend, IconAttach } from '@aws-amplify/ui-react/internal';
4
4
  import { ComponentClassName } from '@aws-amplify/ui';
5
5
  import { Attachments } from './Attachments.mjs';
6
+ import { validFileTypes } from '../../utils.mjs';
6
7
 
7
8
  function isHTMLFormElement(target) {
8
9
  return 'form' in target;
@@ -26,8 +27,9 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
26
27
  const sendIcon = icons?.send ?? React.createElement(IconSend, null);
27
28
  const attachIcon = icons?.attach ?? React.createElement(IconAttach, null);
28
29
  const hiddenInput = React.useRef(null);
30
+ // Bedrock does not accept message that are empty or are only whitespace
31
+ const isInputEmpty = !input?.text?.length || !!input.text.match(/^\s+$/);
29
32
  const [composing, setComposing] = React.useState(false);
30
- const isInputEmpty = !input?.text?.length && !input?.files?.length;
31
33
  return (React.createElement(FormWrapper, { onValidate: onValidate, allowAttachments: allowAttachments },
32
34
  React.createElement(View, { as: "form", className: ComponentClassName.AIConversationForm, onSubmit: handleSubmit },
33
35
  allowAttachments ? (React.createElement(Button, { className: ComponentClassName.AIConversationFormAttach, onClick: () => {
@@ -43,7 +45,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isL
43
45
  return;
44
46
  }
45
47
  onValidate(Array.from(e.target.files));
46
- }, multiple: true, accept: ".jpeg,.png,.webp,.gif", "data-testid": "hidden-file-input" })))) : null,
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,
@@ -27,11 +27,11 @@ function createUseAIConversation(client) {
27
27
  // it will create a new conversation when it is executed
28
28
  // we don't want to create 2 conversations
29
29
  const initRef = React__default.useRef('initial');
30
- const [dataState, setDataState] = React__default.useState(() => ({
30
+ const [clientState, setClientState] = React__default.useState(() => ({
31
31
  ...INITIAL_STATE,
32
32
  data: { messages: [], conversation: undefined },
33
33
  }));
34
- const { conversation } = dataState.data;
34
+ const { conversation } = clientState.data;
35
35
  const { id, onInitialize, onMessage } = input;
36
36
  React__default.useEffect(() => {
37
37
  async function initialize() {
@@ -43,7 +43,7 @@ function createUseAIConversation(client) {
43
43
  // Only show component loading state if we are
44
44
  // actually loading messages
45
45
  if (id) {
46
- setDataState({
46
+ setClientState({
47
47
  ...LOADING_STATE,
48
48
  data: { messages: [], conversation: undefined },
49
49
  });
@@ -52,7 +52,7 @@ function createUseAIConversation(client) {
52
52
  ? await clientRoute.get({ id })
53
53
  : await clientRoute.create();
54
54
  if (errors ?? !conversation) {
55
- setDataState({
55
+ setClientState({
56
56
  ...ERROR_STATE,
57
57
  data: { messages: [] },
58
58
  messages: errors,
@@ -63,13 +63,13 @@ function createUseAIConversation(client) {
63
63
  const { data: messages } = await exhaustivelyListMessages({
64
64
  conversation,
65
65
  });
66
- setDataState({
66
+ setClientState({
67
67
  ...INITIAL_STATE,
68
68
  data: { messages, conversation },
69
69
  });
70
70
  }
71
71
  else {
72
- setDataState({
72
+ setClientState({
73
73
  ...INITIAL_STATE,
74
74
  data: { conversation, messages: [] },
75
75
  });
@@ -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
- setDataState({
85
+ const error = {
86
+ message: 'Conversation route does not exist',
87
+ errorInfo: null,
88
+ errorType: '',
89
+ };
90
+ setClientState({
86
91
  ...ERROR_STATE,
87
92
  data: { messages: [] },
88
- messages: [
89
- {
90
- message: 'Conversation route does not exist',
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
  }
@@ -100,12 +101,12 @@ function createUseAIConversation(client) {
100
101
  contentBlocksRef.current = undefined;
101
102
  if (hasStarted(initRef.current))
102
103
  return;
103
- setDataState({
104
+ setClientState({
104
105
  ...INITIAL_STATE,
105
106
  data: { messages: [], conversation: undefined },
106
107
  });
107
108
  };
108
- }, [clientRoute, id, setDataState]);
109
+ }, [clientRoute, id, setClientState]);
109
110
  // Run a separate effect that is triggered by the conversation state
110
111
  // so that we know we have a conversation object to set up the subscription
111
112
  // and also unsubscribe on cleanup
@@ -133,7 +134,7 @@ function createUseAIConversation(client) {
133
134
  // stop reason will signify end of conversation turn
134
135
  if (stopReason) {
135
136
  // remove loading state from streamed message
136
- setDataState((prev) => {
137
+ setClientState((prev) => {
137
138
  return {
138
139
  ...prev,
139
140
  data: {
@@ -180,7 +181,7 @@ function createUseAIConversation(client) {
180
181
  ];
181
182
  }
182
183
  }
183
- setDataState((prev) => {
184
+ setClientState((prev) => {
184
185
  const message = {
185
186
  id,
186
187
  conversationId,
@@ -201,11 +202,13 @@ function createUseAIConversation(client) {
201
202
  });
202
203
  },
203
204
  error: (error) => {
204
- setDataState((prev) => {
205
+ setClientState((prev) => {
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
  },
@@ -217,11 +220,11 @@ function createUseAIConversation(client) {
217
220
  contentBlocksRef.current = undefined;
218
221
  subscription.unsubscribe();
219
222
  };
220
- }, [conversation, onInitialize, onMessage, setDataState]);
223
+ }, [conversation, onInitialize, onMessage, setClientState]);
221
224
  const handleSendMessage = React__default.useCallback((input) => {
222
225
  const { content } = input;
223
226
  if (conversation) {
224
- setDataState((prevState) => ({
227
+ setClientState((prevState) => ({
225
228
  ...prevState,
226
229
  data: {
227
230
  ...prevState.data,
@@ -249,20 +252,21 @@ function createUseAIConversation(client) {
249
252
  conversation.sendMessage(input);
250
253
  }
251
254
  else {
252
- setDataState((prev) => ({
255
+ const error = {
256
+ message: 'No conversation found',
257
+ errorInfo: null,
258
+ errorType: '',
259
+ };
260
+ setClientState((prev) => ({
253
261
  ...prev,
254
262
  ...ERROR_STATE,
255
- messages: [
256
- {
257
- message: 'No conversation found',
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]);
265
- return [dataState, handleSendMessage];
269
+ return [clientState, handleSendMessage];
266
270
  };
267
271
  return useAIConversation;
268
272
  }
@@ -3,26 +3,19 @@ import { INITIAL_STATE, LOADING_STATE, ERROR_STATE } from './shared.mjs';
3
3
 
4
4
  function createUseAIGeneration(client) {
5
5
  const useAIGeneration = (routeName) => {
6
- const [dataState, setDataState] = React.useState(() => ({
7
- ...INITIAL_STATE,
8
- data: undefined,
9
- }));
6
+ const [clientState, setClientState] = React.useState(() => ({ ...INITIAL_STATE, data: undefined }));
10
7
  const handleGeneration = React.useCallback(async (input) => {
11
- setDataState(({ data }) => ({ ...LOADING_STATE, data }));
8
+ setClientState(({ data }) => ({ ...LOADING_STATE, data }));
12
9
  const result = await client.generations[routeName](input);
13
10
  const { data, errors } = result;
14
11
  if (errors) {
15
- setDataState({
16
- ...ERROR_STATE,
17
- data,
18
- messages: errors,
19
- });
12
+ setClientState({ ...ERROR_STATE, data, messages: errors });
20
13
  }
21
14
  else {
22
- setDataState({ ...INITIAL_STATE, data });
15
+ setClientState({ ...INITIAL_STATE, data });
23
16
  }
24
17
  }, [routeName]);
25
- return [dataState, handleGeneration];
18
+ return [clientState, handleGeneration];
26
19
  };
27
20
  return useAIGeneration;
28
21
  }
@@ -1,3 +1,3 @@
1
- const VERSION = '1.3.0';
1
+ const VERSION = '1.4.1';
2
2
 
3
3
  export { VERSION };