@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/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 getImageTypeFromMimeType(mimeType) {
106
- return mimeType.split('/')[1];
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 hasMaxSizeError = false;
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
- hasMaxSizeError = true;
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
- const HeadingElement = elements.defineBaseElementWithRef({
345
+ elements.defineBaseElementWithRef({
292
346
  type: 'h2',
293
347
  displayName: 'Title',
294
348
  });
295
- const ImageElement = elements.defineBaseElementWithRef({
349
+ elements.defineBaseElementWithRef({
296
350
  type: 'img',
297
351
  displayName: 'Image',
298
352
  });
299
- const InputElement = elements.defineBaseElementWithRef({
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
- ref.current?.reset();
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 (input?.text) {
684
+ if (isInputText) {
623
685
  const textContent = {
624
686
  text: input.text,
625
687
  };
626
688
  submittedContent.push(textContent);
627
689
  }
628
- if (input?.files) {
690
+ if (isInputFiles) {
629
691
  for (const file of input.files) {
630
692
  const buffer = await file.arrayBuffer();
631
- const fileContent = {
632
- image: {
633
- format: getImageTypeFromMimeType(file.type),
634
- source: { bytes: new Uint8Array(buffer) },
635
- },
636
- };
637
- submittedContent.push(fileContent);
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
- const hasInput = !!input?.text || (input?.files?.length && input?.files?.length > 0);
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 || hasMaxAttachmentSizeError) {
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: ".jpeg,.png,.webp,.gif", "data-testid": "hidden-file-input" })))) : null,
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.3.0';
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 [dataState, setDataState] = React__namespace.useState(() => ({
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
- setDataState(({ data }) => ({ ...LOADING_STATE, data }));
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
- setDataState({
1166
- ...ERROR_STATE,
1167
- data,
1168
- messages: errors,
1169
- });
1260
+ setClientState({ ...ERROR_STATE, data, messages: errors });
1170
1261
  }
1171
1262
  else {
1172
- setDataState({ ...INITIAL_STATE, data });
1263
+ setClientState({ ...INITIAL_STATE, data });
1173
1264
  }
1174
1265
  }, [routeName]);
1175
- return [dataState, handleGeneration];
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 [dataState, setDataState] = React__namespace["default"].useState(() => ({
1333
+ const [clientState, setClientState] = React__namespace["default"].useState(() => ({
1243
1334
  ...INITIAL_STATE,
1244
1335
  data: { messages: [], conversation: undefined },
1245
1336
  }));
1246
- const { conversation } = dataState.data;
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
- setDataState({
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
- setDataState({
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
- setDataState({
1369
+ setClientState({
1279
1370
  ...INITIAL_STATE,
1280
1371
  data: { messages, conversation },
1281
1372
  });
1282
1373
  }
1283
1374
  else {
1284
- setDataState({
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
- setDataState({
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
- messages: [
1301
- {
1302
- message: 'Conversation route does not exist',
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
- setDataState({
1407
+ setClientState({
1316
1408
  ...INITIAL_STATE,
1317
1409
  data: { messages: [], conversation: undefined },
1318
1410
  });
1319
1411
  };
1320
- }, [clientRoute, id, setDataState]);
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
- setDataState((prev) => {
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
- setDataState((prev) => {
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
- setDataState((prev) => {
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, setDataState]);
1526
+ }, [conversation, onInitialize, onMessage, setClientState]);
1433
1527
  const handleSendMessage = React__namespace["default"].useCallback((input) => {
1434
1528
  const { content } = input;
1435
1529
  if (conversation) {
1436
- setDataState((prevState) => ({
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
- setDataState((prev) => ({
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
- messages: [
1468
- {
1469
- message: 'No conversation found',
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 [dataState, handleSendMessage];
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 getImageTypeFromMimeType(mimeType: string): 'png' | 'jpeg' | 'gif' | 'webp';
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 type DataClientState<T> = Omit<DataState<T>, 'message'> & {
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
- export type DataClientResponse<T> = {
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 { DataClientState } from './shared';
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) => [DataClientState<AIConversationState>, SendMessage];
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 { V6Client } from '@aws-amplify/api-graphql';
1
+ import type { ClientExtensions } from '@aws-amplify/data-schema/runtime';
2
2
  import { getSchema } from '../types';
3
- import { DataClientState } from './shared';
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<DataClientState<Schema[U]['returnType']>>,
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<DataClientState<Schema[Key]['returnType']>>,
11
+ Awaited<AiClientState<Schema[Key]['returnType']>>,
12
12
  (input: Schema[Key]['args']) => void
13
13
  ];
14
- type AIGenerationClient<T extends Record<any, any>> = Pick<V6Client<T>, 'generations'>;
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 {};