@aws-amplify/ui-react-ai 0.4.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/esm/components/AIConversation/AIConversation.mjs +8 -26
  2. package/dist/esm/components/AIConversation/AIConversationProvider.mjs +20 -17
  3. package/dist/esm/components/AIConversation/context/AIContextContext.mjs +8 -0
  4. package/dist/esm/components/AIConversation/context/AttachmentContext.mjs +12 -3
  5. package/dist/esm/components/AIConversation/context/ConversationInputContext.mjs +2 -1
  6. package/dist/esm/components/AIConversation/context/FallbackComponentContext.mjs +8 -0
  7. package/dist/esm/components/AIConversation/context/ResponseComponentsContext.mjs +6 -2
  8. package/dist/esm/components/AIConversation/context/elements/IconElement.mjs +2 -2
  9. package/dist/esm/components/AIConversation/context/elements/definitions.mjs +12 -12
  10. package/dist/esm/components/AIConversation/createAIConversation.mjs +2 -5
  11. package/dist/esm/components/AIConversation/displayText.mjs +6 -0
  12. package/dist/esm/components/AIConversation/utils.mjs +42 -13
  13. package/dist/esm/components/AIConversation/views/Controls/ActionsBarControl.mjs +3 -2
  14. package/dist/esm/components/AIConversation/views/Controls/AttachFileControl.mjs +2 -0
  15. package/dist/esm/components/AIConversation/views/Controls/AttachmentListControl.mjs +2 -0
  16. package/dist/esm/components/AIConversation/views/Controls/AvatarControl.mjs +2 -0
  17. package/dist/esm/components/AIConversation/views/Controls/DefaultMessageControl.mjs +2 -0
  18. package/dist/esm/components/AIConversation/views/Controls/FormControl.mjs +44 -8
  19. package/dist/esm/components/AIConversation/views/Controls/MessagesControl.mjs +24 -31
  20. package/dist/esm/components/AIConversation/views/Controls/PromptControl.mjs +2 -0
  21. package/dist/esm/components/AIConversation/views/default/Form.mjs +13 -20
  22. package/dist/esm/components/AIConversation/views/default/MessageList.mjs +31 -16
  23. package/dist/esm/hooks/contentFromEvents.mjs +22 -0
  24. package/dist/esm/hooks/createAIHooks.mjs +0 -3
  25. package/dist/esm/hooks/exhaustivelyListMessages.mjs +19 -0
  26. package/dist/esm/hooks/shared.mjs +14 -0
  27. package/dist/esm/hooks/useAIConversation.mjs +246 -106
  28. package/dist/esm/hooks/useAIGeneration.mjs +1 -8
  29. package/dist/esm/index.mjs +0 -1
  30. package/dist/esm/version.mjs +1 -1
  31. package/dist/index.js +508 -280
  32. package/dist/types/components/AIConversation/AIConversation.d.ts +0 -3
  33. package/dist/types/components/AIConversation/AIConversationProvider.d.ts +1 -1
  34. package/dist/types/components/AIConversation/context/AIContextContext.d.ts +6 -0
  35. package/dist/types/components/AIConversation/context/AttachmentContext.d.ts +5 -5
  36. package/dist/types/components/AIConversation/context/ControlsContext.d.ts +5 -3
  37. package/dist/types/components/AIConversation/context/ConversationInputContext.d.ts +4 -2
  38. package/dist/types/components/AIConversation/context/DisplayTextContext.d.ts +1 -1
  39. package/dist/types/components/AIConversation/context/FallbackComponentContext.d.ts +7 -0
  40. package/dist/types/components/AIConversation/context/MessageRenderContext.d.ts +1 -1
  41. package/dist/types/components/AIConversation/context/ResponseComponentsContext.d.ts +2 -2
  42. package/dist/types/components/AIConversation/context/elements/IconElement.d.ts +2 -2
  43. package/dist/types/components/AIConversation/context/elements/definitions.d.ts +12 -12
  44. package/dist/types/components/AIConversation/context/index.d.ts +4 -2
  45. package/dist/types/components/AIConversation/createAIConversation.d.ts +0 -3
  46. package/dist/types/components/AIConversation/displayText.d.ts +2 -0
  47. package/dist/types/components/AIConversation/index.d.ts +2 -1
  48. package/dist/types/components/AIConversation/types.d.ts +6 -24
  49. package/dist/types/components/AIConversation/utils.d.ts +10 -0
  50. package/dist/types/components/AIConversation/views/Controls/MessagesControl.d.ts +1 -5
  51. package/dist/types/components/AIConversation/views/default/Attachments.d.ts +2 -2
  52. package/dist/types/components/AIConversation/views/default/Form.d.ts +1 -1
  53. package/dist/types/components/AIConversation/views/default/MessageList.d.ts +1 -1
  54. package/dist/types/components/AIConversation/views/default/PromptList.d.ts +1 -1
  55. package/dist/types/hooks/contentFromEvents.d.ts +2 -0
  56. package/dist/types/hooks/createAIHooks.d.ts +0 -3
  57. package/dist/types/hooks/exhaustivelyListMessages.d.ts +8 -0
  58. package/dist/types/hooks/index.d.ts +1 -2
  59. package/dist/types/hooks/shared.d.ts +23 -0
  60. package/dist/types/hooks/useAIConversation.d.ts +6 -4
  61. package/dist/types/hooks/useAIGeneration.d.ts +3 -13
  62. package/dist/types/index.d.ts +1 -1
  63. package/dist/types/types.d.ts +32 -1
  64. package/dist/types/version.d.ts +1 -1
  65. package/package.json +6 -6
  66. package/dist/ai-conversation-styles.css +0 -195
  67. package/dist/ai-conversation-styles.js +0 -2
  68. package/dist/esm/hooks/AIContextProvider.mjs +0 -20
  69. package/dist/types/ai-conversation-styles.d.ts +0 -1
  70. package/dist/types/hooks/AIContextProvider.d.ts +0 -17
package/dist/index.js CHANGED
@@ -5,9 +5,9 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var React = require('react');
6
6
  var elements = require('@aws-amplify/ui-react-core/elements');
7
7
  var uiReactCore = require('@aws-amplify/ui-react-core');
8
+ var ui = require('@aws-amplify/ui');
8
9
  var uiReact = require('@aws-amplify/ui-react');
9
10
  var internal = require('@aws-amplify/ui-react/internal');
10
- var ui = require('@aws-amplify/ui');
11
11
 
12
12
  function _interopNamespace(e) {
13
13
  if (e && e.__esModule) return e;
@@ -29,6 +29,11 @@ function _interopNamespace(e) {
29
29
 
30
30
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
31
31
 
32
+ const AIContextContext = React__namespace["default"].createContext(undefined);
33
+ const AIContextProvider = ({ children, aiContext, }) => {
34
+ return (React__namespace["default"].createElement(AIContextContext.Provider, { value: aiContext }, children));
35
+ };
36
+
32
37
  const ActionsContext = React__namespace["default"].createContext(undefined);
33
38
  const ActionsProvider = ({ children, actions, }) => {
34
39
  return (React__namespace["default"].createElement(ActionsContext.Provider, { value: actions }, children));
@@ -42,7 +47,8 @@ const AvatarsProvider = ({ children, avatars, }) => {
42
47
  const ConversationInputContext = React__namespace["default"].createContext({});
43
48
  const ConversationInputContextProvider = ({ children, }) => {
44
49
  const [input, setInput] = React__namespace["default"].useState();
45
- const providerValue = React__namespace["default"].useMemo(() => ({ input, setInput }), [input, setInput]);
50
+ const [error, setError] = React__namespace["default"].useState();
51
+ const providerValue = React__namespace["default"].useMemo(() => ({ input, setInput, error, setError }), [input, setInput, error, setError]);
46
52
  return (React__namespace["default"].createElement(ConversationInputContext.Provider, { value: providerValue }, children));
47
53
  };
48
54
 
@@ -77,32 +83,67 @@ function formatDate(date) {
77
83
  return `${dateString} at ${timeString}`;
78
84
  }
79
85
  function arrayBufferToBase64(buffer) {
80
- let binary = '';
81
- const bytes = new Uint8Array(buffer);
82
- const len = bytes.byteLength;
83
- for (let i = 0; i < len; i++) {
84
- binary += String.fromCharCode(bytes[i]);
85
- }
86
- return window.btoa(binary);
87
- }
88
- function convertBufferToBase64(buffer, format) {
89
- let base64string = '';
90
86
  // Use node-based buffer if available
91
87
  // fall back on browser if not
92
88
  if (typeof Buffer !== 'undefined') {
93
- base64string = Buffer.from(new Uint8Array(buffer)).toString('base64');
89
+ return Buffer.from(new Uint8Array(buffer)).toString('base64');
94
90
  }
95
91
  else {
96
- base64string = arrayBufferToBase64(buffer);
92
+ let binary = '';
93
+ const bytes = new Uint8Array(buffer);
94
+ const len = bytes.byteLength;
95
+ for (let i = 0; i < len; i++) {
96
+ binary += String.fromCharCode(bytes[i]);
97
+ }
98
+ return window.btoa(binary);
97
99
  }
100
+ }
101
+ function convertBufferToBase64(buffer, format) {
102
+ const base64string = arrayBufferToBase64(buffer);
98
103
  return `data:image/${format};base64,${base64string}`;
99
104
  }
100
105
  function getImageTypeFromMimeType(mimeType) {
101
106
  return mimeType.split('/')[1];
102
107
  }
108
+ async function attachmentsValidator({ files, maxAttachments, maxAttachmentSize, }) {
109
+ const acceptedFiles = [];
110
+ const rejectedFiles = [];
111
+ let hasMaxSizeError = false;
112
+ for (const file of files) {
113
+ const arrayBuffer = await file.arrayBuffer();
114
+ const base64 = arrayBufferToBase64(arrayBuffer);
115
+ if (base64.length < maxAttachmentSize) {
116
+ acceptedFiles.push(file);
117
+ }
118
+ else {
119
+ rejectedFiles.push(file);
120
+ hasMaxSizeError = true;
121
+ }
122
+ }
123
+ if (acceptedFiles.length > maxAttachments) {
124
+ return {
125
+ acceptedFiles: acceptedFiles.slice(0, maxAttachments),
126
+ rejectedFiles: [...acceptedFiles.slice(maxAttachments), ...rejectedFiles],
127
+ hasMaxAttachmentsError: true,
128
+ hasMaxAttachmentSizeError: hasMaxSizeError,
129
+ };
130
+ }
131
+ return {
132
+ acceptedFiles,
133
+ rejectedFiles,
134
+ hasMaxAttachmentsError: false,
135
+ hasMaxAttachmentSizeError: hasMaxSizeError,
136
+ };
137
+ }
103
138
 
104
139
  const defaultAIConversationDisplayTextEn = {
105
140
  getMessageTimestampText: (date) => formatDate(date),
141
+ getMaxAttachmentErrorText(count) {
142
+ return `Cannot choose more than ${count} ${count === 1 ? 'file' : 'files'}. `;
143
+ },
144
+ getAttachmentSizeErrorText(sizeText) {
145
+ return `File size must be below ${sizeText}.`;
146
+ },
106
147
  };
107
148
 
108
149
  const { ConversationDisplayTextContext, ConversationDisplayTextProvider, useConversationDisplayText, } = uiReactCore.createContextUtilities({
@@ -141,8 +182,12 @@ const convertResponseComponentsToToolConfiguration = (responseComponents) => {
141
182
  const { props } = responseComponents[toolName];
142
183
  const requiredProps = [];
143
184
  Object.keys(props).forEach((propName) => {
144
- if (props[propName].required)
185
+ if (props[propName].required) {
145
186
  requiredProps.push(propName);
187
+ // The inputSchema for a tool needs to not
188
+ // have `required` in the properties
189
+ props[propName].required = undefined;
190
+ }
146
191
  });
147
192
  tools[toolName] = {
148
193
  description: responseComponents[toolName].description,
@@ -171,9 +216,18 @@ const { MessageRendererContext, MessageRendererProvider, useMessageRenderer, } =
171
216
  errorMessage: '`useMessageRenderer` must be used with an AIConversation component',
172
217
  });
173
218
 
174
- const AttachmentContext = React__namespace.createContext(false);
175
- const AttachmentProvider = ({ children, allowAttachments, }) => {
176
- return (React__namespace.createElement(AttachmentContext.Provider, { value: allowAttachments ?? false }, children));
219
+ const AttachmentContext = React__namespace.createContext({
220
+ allowAttachments: false,
221
+ // We save attachments as base64 strings into dynamodb for conversation history
222
+ // DynamoDB has a max size of 400kb for records
223
+ // This can be overridden so cutsomers could provide a lower number
224
+ // or a higher number if in the future we support larger sizes.
225
+ maxAttachmentSize: 400000,
226
+ maxAttachments: 20,
227
+ });
228
+ const AttachmentProvider = ({ children, allowAttachments = false, maxAttachmentSize = 400000, maxAttachments = 20, }) => {
229
+ const providerValue = React__namespace.useMemo(() => ({ maxAttachmentSize, maxAttachments, allowAttachments }), [maxAttachmentSize, maxAttachments, allowAttachments]);
230
+ return (React__namespace.createElement(AttachmentContext.Provider, { value: providerValue }, children));
177
231
  };
178
232
 
179
233
  const WelcomeMessageContext = React__namespace.createContext(undefined);
@@ -181,6 +235,11 @@ const WelcomeMessageProvider = ({ children, welcomeMessage, }) => {
181
235
  return (React__namespace.createElement(WelcomeMessageContext.Provider, { value: welcomeMessage }, children));
182
236
  };
183
237
 
238
+ const FallbackComponentContext = React__namespace["default"].createContext(undefined);
239
+ const FallbackComponentProvider = ({ children, FallbackComponent, }) => {
240
+ return (React__namespace["default"].createElement(FallbackComponentContext.Provider, { value: FallbackComponent }, children));
241
+ };
242
+
184
243
  const DEFAULT_ICON_PATHS = {
185
244
  attach: 'M720-330q0 104-73 177T470-80q-104 0-177-73t-73-177v-370q0-75 52.5-127.5T400-880q75 0 127.5 52.5T580-700v350q0 46-32 78t-78 32q-46 0-78-32t-32-78v-370h80v370q0 13 8.5 21.5T470-320q13 0 21.5-8.5T500-350v-350q-1-42-29.5-71T400-800q-42 0-71 29t-29 71v370q-1 71 49 120.5T470-160q70 0 119-49.5T640-330v-390h80v390Z',
186
245
  close: 'm256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z',
@@ -197,7 +256,7 @@ const DEFAULT_ICON_ATTRIBUTES = {
197
256
  fill: 'none',
198
257
  xmlns: 'http://www.w3.org/2000/svg',
199
258
  };
200
- const BaseIconElement = elements.defineBaseElement({
259
+ const BaseIconElement = elements.defineBaseElementWithRef({
201
260
  type: 'svg',
202
261
  displayName: 'Icon',
203
262
  });
@@ -213,44 +272,44 @@ const getIconProps = ({ variant, ...props }) => {
213
272
  };
214
273
  const IconElement = elements.withBaseElementProps(BaseIconElement, getIconProps);
215
274
 
216
- const LabelElement$1 = elements.defineBaseElement({
275
+ const LabelElement$1 = elements.defineBaseElementWithRef({
217
276
  type: 'label',
218
277
  displayName: 'Label',
219
278
  });
220
- const TextElement = elements.defineBaseElement({
279
+ const TextElement = elements.defineBaseElementWithRef({
221
280
  type: 'p',
222
281
  displayName: 'Text',
223
282
  });
224
- const UnorderedListElement = elements.defineBaseElement({
283
+ const UnorderedListElement = elements.defineBaseElementWithRef({
225
284
  type: 'ul',
226
285
  displayName: 'UnorderedList',
227
286
  });
228
- const ListItemElement = elements.defineBaseElement({
287
+ const ListItemElement = elements.defineBaseElementWithRef({
229
288
  type: 'li',
230
289
  displayName: 'ListItem',
231
290
  });
232
- const HeadingElement = elements.defineBaseElement({
291
+ const HeadingElement = elements.defineBaseElementWithRef({
233
292
  type: 'h2',
234
293
  displayName: 'Title',
235
294
  });
236
- const ImageElement = elements.defineBaseElement({
295
+ const ImageElement = elements.defineBaseElementWithRef({
237
296
  type: 'img',
238
297
  displayName: 'Image',
239
298
  });
240
- const InputElement = elements.defineBaseElement({
299
+ const InputElement = elements.defineBaseElementWithRef({
241
300
  type: 'input',
242
301
  displayName: 'Input',
243
302
  });
244
- const ButtonElement = elements.defineBaseElement({ type: 'button', displayName: 'Button' });
245
- const ViewElement = elements.defineBaseElement({
303
+ const ButtonElement = elements.defineBaseElementWithRef({ type: 'button', displayName: 'Button' });
304
+ const ViewElement = elements.defineBaseElementWithRef({
246
305
  type: 'div',
247
306
  displayName: 'View',
248
307
  });
249
- const SpanElement = elements.defineBaseElement({
308
+ const SpanElement = elements.defineBaseElementWithRef({
250
309
  type: 'span',
251
310
  displayName: 'Span',
252
311
  });
253
- const TextAreaElement = elements.defineBaseElement({
312
+ const TextAreaElement = elements.defineBaseElementWithRef({
254
313
  type: 'textarea',
255
314
  displayName: 'TextArea',
256
315
  });
@@ -269,9 +328,9 @@ const AIConversationElements = {
269
328
  View: ViewElement,
270
329
  };
271
330
 
272
- const { Button: Button$4, Span: Span$3, View: View$6 } = AIConversationElements;
331
+ const { Button: Button$4, Span: Span$2, View: View$6 } = AIConversationElements;
273
332
  const ACTIONS_BAR_BLOCK = 'ai-actions-bar';
274
- const ActionIcon = elements.withBaseElementProps(Span$3, {
333
+ const ActionIcon = elements.withBaseElementProps(Span$2, {
275
334
  'aria-hidden': 'true',
276
335
  className: `${ACTIONS_BAR_BLOCK}__icon`,
277
336
  });
@@ -286,14 +345,13 @@ const Container$3 = elements.withBaseElementProps(View$6, {
286
345
  });
287
346
  const ActionsBarControl = ({ message, focusable, }) => {
288
347
  const actions = React__namespace["default"].useContext(ActionsContext);
289
- return (React__namespace["default"].createElement(Container$3, null, actions?.map((action, index) => (React__namespace["default"].createElement(ActionButton, { "aria-label": action.displayName, key: index, onClick: () => action.handler(message), tabIndex: focusable ? 0 : -1 },
290
- React__namespace["default"].createElement(ActionIcon, { "data-testid": `action-icon-${action.displayName}` }, action.icon))))));
348
+ return (React__namespace["default"].createElement(Container$3, null, actions?.map((action, index) => (React__namespace["default"].createElement(ActionButton, { key: index, onClick: () => action.handler(message), tabIndex: focusable ? 0 : -1 }, action.component)))));
291
349
  };
292
350
  ActionsBarControl.Button = ActionButton;
293
351
  ActionsBarControl.Container = Container$3;
294
352
  ActionsBarControl.Icon = ActionIcon;
295
353
 
296
- const { Icon: Icon$3, Span: Span$2, Text: Text$2, View: View$5 } = AIConversationElements;
354
+ const { Icon: Icon$3, Span: Span$1, Text: Text$2, View: View$5 } = AIConversationElements;
297
355
  const AVATAR_BLOCK = 'ai-avatar';
298
356
  const DEFAULT_USER_ICON = elements.withBaseElementProps(Icon$3, {
299
357
  variant: 'user-avatar',
@@ -304,7 +362,7 @@ const DEFAULT_AI_ICON = () => (React__namespace["default"].createElement("svg",
304
362
  const AvatarDisplayName = elements.withBaseElementProps(Text$2, {
305
363
  className: `${AVATAR_BLOCK}__display-name`,
306
364
  });
307
- const AvatarIcon = elements.withBaseElementProps(Span$2, {
365
+ const AvatarIcon = elements.withBaseElementProps(Span$1, {
308
366
  'aria-hidden': true,
309
367
  className: `${AVATAR_BLOCK}__icon`,
310
368
  });
@@ -385,7 +443,7 @@ AttachFileControl.Icon = AttachFileIcon;
385
443
  AttachFileControl.Button = AttachFileButton;
386
444
  AttachFileControl.Container = AttachFileContainer;
387
445
 
388
- const { Button: Button$2, Icon: Icon$1, ListItem, UnorderedList: ListElement, Span: Span$1, Text: Text$1, View: View$3, } = AIConversationElements;
446
+ const { Button: Button$2, Icon: Icon$1, ListItem, UnorderedList: ListElement, Span, Text: Text$1, View: View$3, } = AIConversationElements;
389
447
  const IMAGE_LIST_BLOCK = 'ai-attachment-list';
390
448
  const IMAGE_ITEM_BLOCK = 'ai-attachment';
391
449
  const REMOVE_IMAGE_BLOCK = 'ai-remove-attachment';
@@ -416,7 +474,7 @@ const FileNameText = elements.withBaseElementProps(Text$1, {
416
474
  const FileSizeText = elements.withBaseElementProps(Text$1, {
417
475
  className: `${IMAGE_TEXT_BLOCK}__file-size`,
418
476
  });
419
- const Separator$1 = elements.withBaseElementProps(Span$1, {
477
+ const Separator = elements.withBaseElementProps(Span, {
420
478
  'aria-hidden': true,
421
479
  className: `${IMAGE_TEXT_BLOCK}__separator`,
422
480
  children: '|',
@@ -427,13 +485,13 @@ const TextContainer = elements.withBaseElementProps(View$3, {
427
485
  const TextControl = ({ fileName, fileSize }) => {
428
486
  return (React__namespace["default"].createElement(TextContainer, null,
429
487
  React__namespace["default"].createElement(FileNameText, null, fileName),
430
- React__namespace["default"].createElement(Separator$1, null),
488
+ React__namespace["default"].createElement(Separator, null),
431
489
  React__namespace["default"].createElement(FileSizeText, null, fileSize)));
432
490
  };
433
491
  TextControl.Container = TextContainer;
434
492
  TextControl.FileName = FileNameText;
435
493
  TextControl.FileSize = FileSizeText;
436
- TextControl.Separator = Separator$1;
494
+ TextControl.Separator = Separator;
437
495
  const Container$1 = elements.withBaseElementProps(ListItem, {
438
496
  className: `${IMAGE_ITEM_BLOCK}__list-item`,
439
497
  });
@@ -548,12 +606,16 @@ const InputContainer = elements.withBaseElementProps(View$2, {
548
606
  className: `${FIELD_BLOCK}__input-container`,
549
607
  });
550
608
  const FormControl = () => {
551
- const { input, setInput } = React__namespace["default"].useContext(ConversationInputContext);
609
+ const { input, setInput, error, setError } = React__namespace["default"].useContext(ConversationInputContext);
552
610
  const handleSendMessage = React__namespace["default"].useContext(SendMessageContext);
553
- const allowAttachments = React__namespace["default"].useContext(AttachmentContext);
554
- const ref = React__namespace["default"].useRef(null);
611
+ const { allowAttachments, maxAttachmentSize, maxAttachments } = React__namespace["default"].useContext(AttachmentContext);
612
+ const displayText = useConversationDisplayText();
555
613
  const responseComponents = React__namespace["default"].useContext(ResponseComponentsContext);
614
+ const isLoading = React__namespace["default"].useContext(LoadingContext);
615
+ const aiContext = React__namespace["default"].useContext(AIContextContext);
616
+ const ref = React__namespace["default"].useRef(null);
556
617
  const controls = React__namespace["default"].useContext(ControlsContext);
618
+ const [composing, setComposing] = React__namespace["default"].useState(false);
557
619
  const submitMessage = async () => {
558
620
  ref.current?.reset();
559
621
  const submittedContent = [];
@@ -578,6 +640,7 @@ const FormControl = () => {
578
640
  if (handleSendMessage) {
579
641
  handleSendMessage({
580
642
  content: submittedContent,
643
+ aiContext: ui.isFunction(aiContext) ? aiContext() : undefined,
581
644
  toolConfiguration: convertResponseComponentsToToolConfiguration(responseComponents),
582
645
  });
583
646
  }
@@ -590,7 +653,7 @@ const FormControl = () => {
590
653
  };
591
654
  const handleOnKeyDown = (event) => {
592
655
  const { key, shiftKey } = event;
593
- if (key === 'Enter' && !shiftKey) {
656
+ if (key === 'Enter' && !shiftKey && !composing) {
594
657
  event.preventDefault();
595
658
  const hasInput = !!input?.text || (input?.files?.length && input?.files?.length > 0);
596
659
  if (hasInput) {
@@ -598,15 +661,43 @@ const FormControl = () => {
598
661
  }
599
662
  }
600
663
  };
664
+ const onValidate = React__namespace["default"].useCallback(async (files) => {
665
+ const previousFiles = input?.files ?? [];
666
+ const { acceptedFiles, hasMaxAttachmentsError, hasMaxAttachmentSizeError, } = await attachmentsValidator({
667
+ files: [...files, ...previousFiles],
668
+ maxAttachments,
669
+ maxAttachmentSize,
670
+ });
671
+ if (hasMaxAttachmentsError || hasMaxAttachmentSizeError) {
672
+ const errors = [];
673
+ if (hasMaxAttachmentsError) {
674
+ errors.push(displayText.getMaxAttachmentErrorText(maxAttachments));
675
+ }
676
+ if (hasMaxAttachmentSizeError) {
677
+ errors.push(displayText.getAttachmentSizeErrorText(
678
+ // base64 size is about 137% that of the file size
679
+ // https://en.wikipedia.org/wiki/Base64#MIME
680
+ ui.humanFileSize((maxAttachmentSize - 814) / 1.37, true)));
681
+ }
682
+ setError?.(errors.join(' '));
683
+ }
684
+ else {
685
+ setError?.(undefined);
686
+ }
687
+ setInput?.((prevValue) => ({
688
+ ...prevValue,
689
+ files: acceptedFiles,
690
+ }));
691
+ }, [setInput, input, displayText, maxAttachmentSize, maxAttachments, setError]);
601
692
  if (controls?.Form) {
602
- return (React__namespace["default"].createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, allowAttachments: allowAttachments }));
693
+ return (React__namespace["default"].createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, onValidate: onValidate, allowAttachments: allowAttachments, isLoading: isLoading, error: error, setError: setError }));
603
694
  }
604
695
  return (React__namespace["default"].createElement("form", { className: `${FIELD_BLOCK}__form`, onSubmit: handleSubmit, method: "post", ref: ref },
605
696
  allowAttachments ? React__namespace["default"].createElement(AttachFileControl, null) : null,
606
697
  React__namespace["default"].createElement(InputContainer, null,
607
698
  React__namespace["default"].createElement(VisuallyHidden, null,
608
699
  React__namespace["default"].createElement(Label, null)),
609
- React__namespace["default"].createElement(TextInput, { onKeyDown: handleOnKeyDown }),
700
+ React__namespace["default"].createElement(TextInput, { onKeyDown: handleOnKeyDown, onCompositionStart: () => setComposing(true), onCompositionEnd: () => setComposing(false) }),
610
701
  React__namespace["default"].createElement(AttachmentListControl, null)),
611
702
  React__namespace["default"].createElement(SendButton, null,
612
703
  React__namespace["default"].createElement(SendIcon, null))));
@@ -618,61 +709,52 @@ FormControl.TextInput = TextInput;
618
709
  FormControl.SendButton = SendButton;
619
710
  FormControl.SendIcon = SendIcon;
620
711
 
621
- const { Image, Span, Text, View: View$1 } = AIConversationElements;
622
- const MESSAGES_BLOCK = 'ai-messages';
623
- const MESSAGE_BLOCK = 'ai-message';
624
- const MediaContentBase = elements.withBaseElementProps(Image, {
625
- alt: 'Image attachment',
626
- });
627
- const MediaContent = React__namespace["default"].forwardRef(function MediaContent(props, ref) {
712
+ const { Text, View: View$1 } = AIConversationElements;
713
+ const MESSAGES_BLOCK = 'amplify-ai-conversation__message__list';
714
+ const MESSAGE_BLOCK = 'amplify-ai-conversation__message';
715
+ const MediaContent = (props) => {
628
716
  const variant = React__namespace["default"].useContext(MessageVariantContext);
629
717
  const role = React__namespace["default"].useContext(RoleContext);
630
- return (React__namespace["default"].createElement(MediaContentBase, { ref: ref, className: `${MESSAGE_BLOCK}__image ${MESSAGE_BLOCK}__image--${variant} ${MESSAGE_BLOCK}__image--${role}`, ...props }));
631
- });
718
+ return (React__namespace["default"].createElement(uiReact.Image, { className: ui.classNames(`${MESSAGE_BLOCK}__image`, variant && `${MESSAGE_BLOCK}__image--${variant}`, `${MESSAGE_BLOCK}__image--${role}`), ...props }));
719
+ };
632
720
  const TextContent = React__namespace["default"].forwardRef(function TextContent(props, ref) {
633
721
  return React__namespace["default"].createElement(Text, { ref: ref, className: `${MESSAGE_BLOCK}__text`, ...props });
634
722
  });
635
- const ContentContainer = React__namespace["default"].forwardRef(function ContentContainer(props, ref) {
636
- const variant = React__namespace["default"].useContext(MessageVariantContext);
637
- return (React__namespace["default"].createElement(View$1, { "data-testid": 'content', className: `${MESSAGE_BLOCK}__content ${MESSAGE_BLOCK}__content--${variant}`, ref: ref, ...props }));
638
- });
639
723
  const ToolContent = ({ toolUse, }) => {
640
- const responseComponents = React__namespace["default"].useContext(ResponseComponentsContext);
724
+ const responseComponents = React__namespace["default"].useContext(ResponseComponentsContext) ?? {};
725
+ const FallbackComponent = React__namespace["default"].useContext(FallbackComponentContext);
641
726
  // For now tool use is limited to custom response components
642
727
  const { name, input } = toolUse;
643
- if (!responseComponents ||
644
- !name ||
645
- !name.startsWith(RESPONSE_COMPONENT_PREFIX)) {
728
+ if (!name || !name.startsWith(RESPONSE_COMPONENT_PREFIX)) {
646
729
  return;
647
730
  }
648
731
  else {
649
732
  const response = responseComponents[name];
650
- const CustomComponent = response.component;
651
- return React__namespace["default"].createElement(CustomComponent, { ...input });
733
+ if (response) {
734
+ const CustomComponent = response.component;
735
+ return React__namespace["default"].createElement(CustomComponent, { ...input });
736
+ }
737
+ // fallback if there is a UI component message but we don't have
738
+ // a React component that matches
739
+ if (FallbackComponent) {
740
+ return React__namespace["default"].createElement(FallbackComponent, { ...input });
741
+ }
652
742
  }
653
743
  };
654
744
  const MessageControl = ({ message }) => {
655
745
  const messageRenderer = React__namespace["default"].useContext(MessageRendererContext);
656
- return (React__namespace["default"].createElement(ContentContainer, null, message.content.map((content, index) => {
746
+ return (React__namespace["default"].createElement(React__namespace["default"].Fragment, null, message.content.map((content, index) => {
657
747
  if (content.text) {
658
748
  return messageRenderer?.text ? (React__namespace["default"].createElement(React__namespace["default"].Fragment, { key: index }, messageRenderer.text({ text: content.text }))) : (React__namespace["default"].createElement(TextContent, { "data-testid": 'text-content', key: index }, content.text));
659
749
  }
660
750
  else if (content.image) {
661
- 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, src: convertBufferToBase64(content.image?.source.bytes, content.image?.format) }));
751
+ 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) }));
662
752
  }
663
753
  else if (content.toolUse) {
664
754
  return React__namespace["default"].createElement(ToolContent, { toolUse: content.toolUse, key: index });
665
755
  }
666
756
  })));
667
757
  };
668
- MessageControl.Container = ContentContainer;
669
- MessageControl.MediaContent = MediaContent;
670
- MessageControl.TextContent = TextContent;
671
- const Separator = elements.withBaseElementProps(Span, {
672
- 'aria-hidden': true,
673
- children: '|',
674
- className: `${MESSAGE_BLOCK}__separator`,
675
- });
676
758
  const Timestamp = elements.withBaseElementProps(Text, {
677
759
  className: `${MESSAGE_BLOCK}__timestamp`,
678
760
  });
@@ -733,7 +815,6 @@ const MessagesControl = () => {
733
815
  React__namespace["default"].createElement(MessageContainer, { "data-testid": `message`, key: `message-${index}`, tabIndex: focusedItemIndex === index ? 0 : -1, onFocus: () => handleFocus(index), onKeyDown: (event) => onKeyDown(index, event), ref: (el) => (messagesRef.current[index] = el) },
734
816
  React__namespace["default"].createElement(HeaderContainer, null,
735
817
  React__namespace["default"].createElement(AvatarControl, null),
736
- React__namespace["default"].createElement(Separator, null),
737
818
  React__namespace["default"].createElement(Timestamp, null, getMessageTimestampText(new Date(message.createdAt)))),
738
819
  React__namespace["default"].createElement(MessageControl, { message: message }),
739
820
  message.role === 'assistant' ? (React__namespace["default"].createElement(ActionsBarControl, { message: message, focusable: focusedItemIndex === index })) : null)));
@@ -745,7 +826,6 @@ MessagesControl.Container = MessageContainer;
745
826
  MessagesControl.HeaderContainer = HeaderContainer;
746
827
  MessagesControl.Layout = Layout;
747
828
  MessagesControl.Message = MessageControl;
748
- MessagesControl.Separator = Separator;
749
829
 
750
830
  const { View, Button } = AIConversationElements;
751
831
  const PROMPT_BLOCK = 'ai-prompts';
@@ -789,25 +869,27 @@ PromptControl.Container = Container;
789
869
  PromptControl.PromptGroup = PromptGroup;
790
870
  PromptControl.PromptCard = PromptCard;
791
871
 
792
- const AIConversationProvider = ({ actions, allowAttachments, avatars, children, controls, displayText, elements: elements$1, handleSendMessage, isLoading, messages, responseComponents, suggestedPrompts, variant, welcomeMessage, }) => {
872
+ const AIConversationProvider = ({ aiContext, actions, allowAttachments, avatars, children, controls, displayText, handleSendMessage, isLoading, maxAttachmentSize, maxAttachments, messages, messageRenderer, responseComponents, suggestedPrompts, variant, welcomeMessage, FallbackResponseComponent, }) => {
793
873
  const _displayText = {
794
874
  ...defaultAIConversationDisplayTextEn,
795
875
  ...displayText,
796
876
  };
797
- return (React__namespace["default"].createElement(elements.ElementsProvider, { elements: elements$1 },
798
- React__namespace["default"].createElement(ControlsProvider, { controls: controls },
799
- React__namespace["default"].createElement(SuggestedPromptProvider, { suggestedPrompts: suggestedPrompts },
800
- React__namespace["default"].createElement(WelcomeMessageProvider, { welcomeMessage: welcomeMessage },
801
- React__namespace["default"].createElement(ResponseComponentsProvider, { responseComponents: responseComponents },
802
- React__namespace["default"].createElement(AttachmentProvider, { allowAttachments: allowAttachments },
803
- React__namespace["default"].createElement(ConversationDisplayTextProvider, { ..._displayText },
804
- React__namespace["default"].createElement(ConversationInputContextProvider, null,
805
- React__namespace["default"].createElement(SendMessageContextProvider, { handleSendMessage: handleSendMessage },
806
- React__namespace["default"].createElement(AvatarsProvider, { avatars: avatars },
807
- React__namespace["default"].createElement(ActionsProvider, { actions: actions },
808
- React__namespace["default"].createElement(MessageVariantProvider, { variant: variant },
809
- React__namespace["default"].createElement(MessagesProvider, { messages: messages },
810
- React__namespace["default"].createElement(LoadingContextProvider, { isLoading: isLoading }, children)))))))))))))));
877
+ return (React__namespace["default"].createElement(ControlsProvider, { controls: controls },
878
+ React__namespace["default"].createElement(SuggestedPromptProvider, { suggestedPrompts: suggestedPrompts },
879
+ React__namespace["default"].createElement(WelcomeMessageProvider, { welcomeMessage: welcomeMessage },
880
+ React__namespace["default"].createElement(FallbackComponentProvider, { FallbackComponent: FallbackResponseComponent },
881
+ React__namespace["default"].createElement(MessageRendererProvider, { ...messageRenderer },
882
+ React__namespace["default"].createElement(ResponseComponentsProvider, { responseComponents: responseComponents },
883
+ React__namespace["default"].createElement(AttachmentProvider, { allowAttachments: allowAttachments, maxAttachmentSize: maxAttachmentSize, maxAttachments: maxAttachments },
884
+ React__namespace["default"].createElement(ConversationDisplayTextProvider, { ..._displayText },
885
+ React__namespace["default"].createElement(ConversationInputContextProvider, null,
886
+ React__namespace["default"].createElement(SendMessageContextProvider, { handleSendMessage: handleSendMessage },
887
+ React__namespace["default"].createElement(AvatarsProvider, { avatars: avatars },
888
+ React__namespace["default"].createElement(ActionsProvider, { actions: actions },
889
+ React__namespace["default"].createElement(MessageVariantProvider, { variant: variant },
890
+ React__namespace["default"].createElement(MessagesProvider, { messages: messages },
891
+ React__namespace["default"].createElement(AIContextProvider, { aiContext: aiContext },
892
+ React__namespace["default"].createElement(LoadingContextProvider, { isLoading: isLoading }, children)))))))))))))))));
811
893
  };
812
894
 
813
895
  const DefaultMessageControl = () => {
@@ -820,15 +902,11 @@ const DefaultMessageControl = () => {
820
902
  }
821
903
  };
822
904
 
823
- /**
824
- * @experimental
825
- */
826
905
  function createAIConversation(input = {}) {
827
- const { elements, suggestedPrompts, actions, responseComponents, variant, controls, displayText, allowAttachments, messageRenderer, } = input;
906
+ const { suggestedPrompts, actions, responseComponents, variant, controls, displayText, allowAttachments, messageRenderer, FallbackResponseComponent, } = input;
828
907
  function AIConversation(props) {
829
908
  const { messages, avatars, handleSendMessage, isLoading } = props;
830
909
  const providerProps = {
831
- elements,
832
910
  actions,
833
911
  suggestedPrompts,
834
912
  responseComponents,
@@ -841,6 +919,7 @@ function createAIConversation(input = {}) {
841
919
  handleSendMessage,
842
920
  isLoading,
843
921
  messageRenderer,
922
+ FallbackResponseComponent,
844
923
  };
845
924
  return (React__namespace["default"].createElement(AIConversationProvider, { ...providerProps },
846
925
  React__namespace["default"].createElement(ViewElement, null,
@@ -857,6 +936,16 @@ function createAIConversation(input = {}) {
857
936
  return { AIConversation };
858
937
  }
859
938
 
939
+ const PlaceholderMessage = ({ role }) => {
940
+ const variant = React__namespace.useContext(MessageVariantContext);
941
+ return (React__namespace.createElement(uiReact.View, { className: ui.classNames(ui.ComponentClassName.AIConversationMessage, ui.classNameModifier(ui.ComponentClassName.AIConversationMessage, variant), ui.classNameModifier(ui.ComponentClassName.AIConversationMessage, role)) },
942
+ React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageAvatar },
943
+ React__namespace.createElement(uiReact.Avatar, null)),
944
+ React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageBody },
945
+ React__namespace.createElement(uiReact.Placeholder, { width: "25%" }),
946
+ React__namespace.createElement(uiReact.Placeholder, { width: "50%" }),
947
+ React__namespace.createElement(uiReact.Placeholder, { width: "25%" }))));
948
+ };
860
949
  const MessageMeta = ({ message }) => {
861
950
  // need to pass this in as props in order for it to be overridable
862
951
  const avatars = React__namespace.useContext(AvatarsContext);
@@ -868,29 +957,30 @@ const MessageMeta = ({ message }) => {
868
957
  React__namespace.createElement(uiReact.Text, { className: ui.ComponentClassName.AIConversationMessageSenderUsername }, avatar?.username),
869
958
  React__namespace.createElement(uiReact.Text, { className: ui.ComponentClassName.AIConversationMessageSenderTimestamp }, getMessageTimestampText(new Date(message.createdAt)))));
870
959
  };
871
- const LoadingMessage = () => {
872
- const avatars = React__namespace.useContext(AvatarsContext);
873
- const variant = React__namespace.useContext(MessageVariantContext);
874
- const avatar = avatars?.ai;
875
- return (React__namespace.createElement(uiReact.View, { className: ui.classNames(ui.ComponentClassName.AIConversationMessage, ui.classNameModifier(ui.ComponentClassName.AIConversationMessage, variant), ui.classNameModifier(ui.ComponentClassName.AIConversationMessage, 'assistant')) },
876
- React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageAvatar },
877
- React__namespace.createElement(uiReact.Avatar, { isLoading: true }, avatar?.avatar)),
878
- React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageBody },
879
- React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageSender },
880
- React__namespace.createElement(uiReact.Text, { className: ui.ComponentClassName.AIConversationMessageSenderUsername }, avatar?.username)))));
960
+ const MessageActions = ({ message }) => {
961
+ const actions = React__namespace.useContext(ActionsContext);
962
+ if (!actions)
963
+ return null;
964
+ return (React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageActions }, actions.map((action, i) => {
965
+ return (React__namespace.createElement(uiReact.Button, { key: i, size: "small", onClick: () => {
966
+ action.handler(message);
967
+ } }, action.component));
968
+ })));
881
969
  };
882
970
  const Message = ({ message }) => {
883
971
  const avatars = React__namespace.useContext(AvatarsContext);
884
972
  const variant = React__namespace.useContext(MessageVariantContext);
973
+ const { isLoading } = message;
885
974
  const avatar = message.role === 'assistant' ? avatars?.ai : avatars?.user;
886
975
  return (React__namespace.createElement(RoleContext.Provider, { value: message.role },
887
976
  React__namespace.createElement(uiReact.View, { className: ui.classNames(ui.ComponentClassName.AIConversationMessage, ui.classNameModifier(ui.ComponentClassName.AIConversationMessage, variant), ui.classNameModifier(ui.ComponentClassName.AIConversationMessage, message.role)) },
888
977
  React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageAvatar },
889
- React__namespace.createElement(uiReact.Avatar, null, avatar?.avatar)),
978
+ React__namespace.createElement(uiReact.Avatar, { isLoading: isLoading }, avatar?.avatar)),
890
979
  React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageBody },
891
980
  React__namespace.createElement(MessageMeta, { message: message }),
892
981
  React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageContent },
893
- React__namespace.createElement(MessageControl, { message: message }))))));
982
+ React__namespace.createElement(MessageControl, { message: message })),
983
+ message.role === 'assistant' ? (React__namespace.createElement(MessageActions, { message: message })) : null))));
894
984
  };
895
985
  const MessageList = ({ messages, }) => {
896
986
  const isLoading = React__namespace.useContext(LoadingContext);
@@ -898,8 +988,10 @@ const MessageList = ({ messages, }) => {
898
988
  content.text ??
899
989
  content.toolUse?.name.startsWith(RESPONSE_COMPONENT_PREFIX))) ?? [];
900
990
  return (React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageList },
901
- messagesWithRenderableContent.map((message, i) => (React__namespace.createElement(Message, { key: `message-${i}`, message: message }))),
902
- isLoading ? React__namespace.createElement(LoadingMessage, null) : null));
991
+ isLoading ? (React__namespace.createElement(React__namespace.Fragment, null,
992
+ React__namespace.createElement(PlaceholderMessage, { role: "user" }),
993
+ React__namespace.createElement(PlaceholderMessage, { role: "assistant" }))) : null,
994
+ messagesWithRenderableContent.map((message, i) => (React__namespace.createElement(Message, { key: `message-${i}`, message: message })))));
903
995
  };
904
996
 
905
997
  const Attachment = ({ file, handleRemove, }) => {
@@ -932,27 +1024,24 @@ function isHTMLFormElement(target) {
932
1024
  * Will conditionally render the DropZone if allowAttachments
933
1025
  * is true
934
1026
  */
935
- const FormWrapper = ({ children, allowAttachments, setInput, }) => {
1027
+ const FormWrapper = ({ children, allowAttachments, onValidate, }) => {
936
1028
  if (allowAttachments) {
937
1029
  return (React__namespace.createElement(uiReact.DropZone, { className: ui.ComponentClassName.AIConversationFormDropzone, onDropComplete: ({ acceptedFiles }) => {
938
- setInput?.((prevInput) => ({
939
- ...prevInput,
940
- files: [...(prevInput?.files ?? []), ...acceptedFiles],
941
- }));
1030
+ onValidate(acceptedFiles);
942
1031
  } }, children));
943
1032
  }
944
1033
  else {
945
1034
  return children;
946
1035
  }
947
1036
  };
948
- const Form = ({ setInput, input, handleSubmit, allowAttachments, }) => {
1037
+ const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isLoading, error, }) => {
949
1038
  const icons = internal.useIcons('aiConversation');
950
1039
  const sendIcon = icons?.send ?? React__namespace.createElement(internal.IconSend, null);
951
1040
  const attachIcon = icons?.attach ?? React__namespace.createElement(internal.IconAttach, null);
952
1041
  const hiddenInput = React__namespace.useRef(null);
953
- const isLoading = React__namespace.useContext(LoadingContext);
1042
+ const [composing, setComposing] = React__namespace.useState(false);
954
1043
  const isInputEmpty = !input?.text?.length && !input?.files?.length;
955
- return (React__namespace.createElement(FormWrapper, { allowAttachments: allowAttachments, setInput: setInput },
1044
+ return (React__namespace.createElement(FormWrapper, { onValidate: onValidate, allowAttachments: allowAttachments },
956
1045
  React__namespace.createElement(uiReact.View, { as: "form", className: ui.ComponentClassName.AIConversationForm, onSubmit: handleSubmit },
957
1046
  allowAttachments ? (React__namespace.createElement(uiReact.Button, { className: ui.ComponentClassName.AIConversationFormAttach, onClick: () => {
958
1047
  hiddenInput?.current?.click();
@@ -963,24 +1052,20 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, }) => {
963
1052
  React__namespace.createElement("span", null, attachIcon),
964
1053
  React__namespace.createElement(uiReact.VisuallyHidden, null,
965
1054
  React__namespace.createElement("input", { type: "file", tabIndex: -1, ref: hiddenInput, onChange: (e) => {
966
- const { files } = e.target;
967
- if (!files || files.length === 0) {
1055
+ if (!e.target.files || e.target.files.length === 0) {
968
1056
  return;
969
1057
  }
970
- setInput((prevValue) => ({
971
- ...prevValue,
972
- files: [...(prevValue?.files ?? []), ...Array.from(files)],
973
- }));
974
- }, multiple: true, accept: "*", "data-testid": "hidden-file-input" })))) : null,
975
- 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", onKeyDown: (e) => {
1058
+ onValidate(Array.from(e.target.files));
1059
+ }, multiple: true, accept: ".jpeg,.png,.webp,.gif", "data-testid": "hidden-file-input" })))) : null,
1060
+ 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) => {
976
1061
  // Submit on enter key if shift is not pressed also
977
- const shouldSubmit = !e.shiftKey && e.key === 'Enter';
1062
+ const shouldSubmit = !e.shiftKey && e.key === 'Enter' && !composing;
978
1063
  if (shouldSubmit && isHTMLFormElement(e.target)) {
979
1064
  e.target.form.requestSubmit();
980
1065
  e.preventDefault();
981
1066
  }
982
1067
  }, onChange: (e) => {
983
- setInput((prevValue) => ({
1068
+ setInput?.((prevValue) => ({
984
1069
  ...prevValue,
985
1070
  text: e.target.value,
986
1071
  }));
@@ -990,6 +1075,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, }) => {
990
1075
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
991
1076
  isDisabled: isLoading || isInputEmpty },
992
1077
  React__namespace.createElement("span", null, sendIcon))),
1078
+ error ? (React__namespace.createElement(uiReact.Message, { className: ui.ComponentClassName.AIConversationFormError, variation: "plain", colorTheme: "warning" }, error)) : null,
993
1079
  React__namespace.createElement(Attachments, { setInput: setInput, files: input?.files })));
994
1080
  };
995
1081
 
@@ -1004,9 +1090,9 @@ const PromptList = ({ setInput, suggestedPrompts = [], }) => {
1004
1090
  })));
1005
1091
  };
1006
1092
 
1007
- const VERSION = '0.4.0';
1093
+ const VERSION = '1.1.0';
1008
1094
 
1009
- function Provider({ actions, avatars, controls, handleSendMessage, messages, responseComponents, suggestedPrompts, variant, isLoading, displayText, allowAttachments, messageRenderer, children, }) {
1095
+ function AIConversationBase({ avatars, controls, ...rest }) {
1010
1096
  uiReactCore.useSetUserAgent({
1011
1097
  componentName: 'AIConversation',
1012
1098
  packageName: 'react-ai',
@@ -1016,83 +1102,53 @@ function Provider({ actions, avatars, controls, handleSendMessage, messages, res
1016
1102
  const defaultAvatars = {
1017
1103
  ai: {
1018
1104
  username: 'Assistant',
1019
- avatar: icons?.assistant ?? React__namespace.createElement(internal.IconAssistant, null),
1105
+ avatar: icons?.assistant ?? React__namespace.createElement(internal.IconAssistant, { testId: "icon-assistant" }),
1020
1106
  },
1021
1107
  user: {
1022
1108
  username: 'User',
1023
- avatar: icons?.user ?? React__namespace.createElement(internal.IconUser, null),
1109
+ avatar: icons?.user ?? React__namespace.createElement(internal.IconUser, { testId: "icon-user" }),
1024
1110
  },
1025
1111
  };
1026
1112
  const providerProps = {
1027
- messages,
1028
- handleSendMessage,
1113
+ ...rest,
1029
1114
  avatars: {
1030
1115
  ...defaultAvatars,
1031
1116
  ...avatars,
1032
1117
  },
1033
- isLoading,
1034
- elements: {
1035
- Text: uiReact.Text,
1036
- },
1037
- actions,
1038
- suggestedPrompts,
1039
- responseComponents,
1040
- variant,
1041
1118
  controls: {
1042
1119
  MessageList,
1043
1120
  PromptList,
1044
1121
  Form,
1045
1122
  ...controls,
1046
1123
  },
1047
- displayText,
1048
- allowAttachments,
1049
- messageRenderer,
1050
1124
  };
1051
- return (React__namespace.createElement(AIConversationProvider, { ...providerProps }, children));
1052
- }
1053
- function AIConversationBase(props) {
1054
- return (React__namespace.createElement(Provider, { ...props },
1055
- React__namespace.createElement(uiReact.Flex, { className: ui.ComponentClassName.AIConversation },
1125
+ return (React__namespace.createElement(AIConversationProvider, { ...providerProps },
1126
+ React__namespace.createElement(uiReact.Flex, { className: ui.ComponentClassName.AIConversation, testId: "ai-conversation" },
1056
1127
  React__namespace.createElement(uiReact.ScrollView, { autoScroll: "smooth", flex: "1" },
1057
1128
  React__namespace.createElement(DefaultMessageControl, null),
1058
1129
  React__namespace.createElement(MessagesControl, null)),
1059
1130
  React__namespace.createElement(FormControl, null))));
1060
1131
  }
1061
- /**
1062
- * @experimental
1063
- */
1064
1132
  const AIConversation = Object.assign(AIConversationBase, {
1065
- Provider,
1133
+ Provider: AIConversationProvider,
1066
1134
  DefaultMessage: DefaultMessageControl,
1067
1135
  Messages: MessagesControl,
1068
1136
  Form: FormControl,
1069
1137
  });
1070
1138
 
1071
- const AIContext = React__namespace["default"].createContext(undefined);
1072
- const useAIContext = () => {
1073
- const context = React__namespace["default"].useContext(AIContext);
1074
- const [routeToConversationsMap, setRouteToConversationsMap] = React__namespace["default"].useState({});
1075
- if (context) {
1076
- return context;
1077
- }
1078
- return { routeToConversationsMap, setRouteToConversationsMap };
1079
- };
1080
- /**
1081
- * @experimental
1082
- */
1083
- const AIContextProvider = ({ children, }) => {
1084
- const context = useAIContext();
1085
- return React__namespace["default"].createElement(AIContext.Provider, { value: context }, children);
1086
- };
1087
-
1088
1139
  // default state
1089
1140
  const INITIAL_STATE = {
1090
1141
  hasError: false,
1091
1142
  isLoading: false,
1092
1143
  messages: undefined,
1093
1144
  };
1094
- const LOADING_STATE = { hasError: false, isLoading: true, messages: undefined };
1145
+ const LOADING_STATE = {
1146
+ hasError: false,
1147
+ isLoading: true,
1148
+ messages: undefined,
1149
+ };
1095
1150
  const ERROR_STATE = { hasError: true, isLoading: false };
1151
+
1096
1152
  function createUseAIGeneration(client) {
1097
1153
  const useAIGeneration = (routeName) => {
1098
1154
  const [dataState, setDataState] = React__namespace.useState(() => ({
@@ -1119,142 +1175,314 @@ function createUseAIGeneration(client) {
1119
1175
  return useAIGeneration;
1120
1176
  }
1121
1177
 
1122
- function createNewConversationMessageInRoute({ previousValue, routeName, conversationId, messages, }) {
1178
+ const contentFromEvents = (contentBlocks) => {
1179
+ if (!contentBlocks)
1180
+ return [];
1181
+ return contentBlocks.map((contentBlock) => {
1182
+ const isTextBlock = contentBlock.some((event) => event.text);
1183
+ if (isTextBlock) {
1184
+ return {
1185
+ text: contentBlock
1186
+ .map((event) => {
1187
+ return event.text;
1188
+ })
1189
+ .join(''),
1190
+ };
1191
+ }
1192
+ // tool use is never chunked
1193
+ if (contentBlock[0].toolUse) {
1194
+ return { toolUse: contentBlock[0].toolUse };
1195
+ }
1196
+ });
1197
+ };
1198
+
1199
+ async function exhaustivelyListMessages({ conversation, messages = [], nextToken, }) {
1200
+ const result = await conversation.listMessages({ nextToken });
1201
+ if (result.data) {
1202
+ messages?.push(...result.data);
1203
+ }
1204
+ if (result.nextToken) {
1205
+ return exhaustivelyListMessages({
1206
+ conversation,
1207
+ messages,
1208
+ nextToken: result.nextToken,
1209
+ });
1210
+ }
1123
1211
  return {
1124
- ...previousValue,
1125
- [routeName]: {
1126
- ...previousValue[routeName],
1127
- [conversationId]: messages,
1128
- },
1212
+ ...result,
1213
+ data: messages,
1129
1214
  };
1130
1215
  }
1216
+
1217
+ function hasStarted(state) {
1218
+ return ['initialLoading', 'initialized'].includes(state);
1219
+ }
1131
1220
  function createUseAIConversation(client) {
1221
+ // This is a bit complicated so buckle up.
1222
+ // The way the data client works is conversation.get() or conversation.create()
1223
+ // is an async function because it makes a graphql call to appsync
1224
+ // then it returns a conversation object, which is like a normal
1225
+ // data client record, except that it also has functions on it,
1226
+ // like sendMessage and onStreamEvent. onStreamEvent sets up a
1227
+ // subscription using a websocket connection, which ideally we only want to
1228
+ // do once per conversation. Because we can only subscribe AFTER the
1229
+ // async call to get/create the conversation is made, the cleanup
1230
+ // function in the effect will won't actually unsubscribe
1132
1231
  const useAIConversation = (routeName, input = {}) => {
1133
1232
  const clientRoute = client.conversations[routeName];
1134
- const { routeToConversationsMap, setRouteToConversationsMap } = useAIContext();
1135
- const messagesFromAIContext = input.id
1136
- ? routeToConversationsMap[routeName]?.[input.id]
1137
- : undefined;
1138
- const [localMessages, setLocalMessages] = React__namespace["default"].useState(messagesFromAIContext ?? []);
1139
- const [conversation, setConversation] = React__namespace["default"].useState(undefined);
1140
- const [waitingForAIResponse, setWaitingForAIResponse] = React__namespace["default"].useState(false);
1141
- const [errorMessage, setErrorMessage] = React__namespace["default"].useState();
1142
- const [hasError, setHasError] = React__namespace["default"].useState(false);
1143
- // On hook initialization get conversation and load all messages
1233
+ // We need to keep track of the stream events as the come in
1234
+ // for an assistant message, but don't need to keep them in state
1235
+ const contentBlocksRef = React__namespace["default"].useRef();
1236
+ // Using this hook without an existing conversation id means
1237
+ // it will create a new conversation when it is executed
1238
+ // we don't want to create 2 conversations
1239
+ const initRef = React__namespace["default"].useRef('initial');
1240
+ const [dataState, setDataState] = React__namespace["default"].useState(() => ({
1241
+ ...INITIAL_STATE,
1242
+ data: { messages: [], conversation: undefined },
1243
+ }));
1244
+ const { conversation } = dataState.data;
1245
+ const { id, onInitialize, onMessage } = input;
1144
1246
  React__namespace["default"].useEffect(() => {
1145
1247
  async function initialize() {
1146
- const { data: conversation } = input.id
1147
- ? await clientRoute.get({ id: input.id })
1148
- : await clientRoute.create();
1149
- if (!conversation) {
1150
- const errorString = 'No conversation found';
1151
- setHasError(true);
1152
- setErrorMessage(errorString);
1153
- throw new Error(errorString);
1248
+ // We don't want to run the effect multiple times
1249
+ // because that could create multiple conversation records
1250
+ if (hasStarted(initRef.current))
1251
+ return;
1252
+ initRef.current = 'initialLoading';
1253
+ // Only show component loading state if we are
1254
+ // actually loading messages
1255
+ if (id) {
1256
+ setDataState({
1257
+ ...LOADING_STATE,
1258
+ data: { messages: [], conversation: undefined },
1259
+ });
1154
1260
  }
1155
- const { data: messages } = await conversation.listMessages();
1156
- setLocalMessages(messages);
1157
- setConversation(conversation);
1158
- setRouteToConversationsMap((previousValue) => {
1159
- return createNewConversationMessageInRoute({
1160
- previousValue,
1161
- routeName: routeName,
1162
- conversationId: conversation.id,
1163
- messages,
1261
+ const { data: conversation, errors } = id
1262
+ ? await clientRoute.get({ id })
1263
+ : await clientRoute.create();
1264
+ if (errors ?? !conversation) {
1265
+ setDataState({
1266
+ ...ERROR_STATE,
1267
+ data: { messages: [] },
1268
+ messages: errors,
1164
1269
  });
1270
+ }
1271
+ else {
1272
+ if (id) {
1273
+ const { data: messages } = await exhaustivelyListMessages({
1274
+ conversation,
1275
+ });
1276
+ setDataState({
1277
+ ...INITIAL_STATE,
1278
+ data: { messages, conversation },
1279
+ });
1280
+ }
1281
+ else {
1282
+ setDataState({
1283
+ ...INITIAL_STATE,
1284
+ data: { conversation, messages: [] },
1285
+ });
1286
+ }
1287
+ initRef.current = 'initialized';
1288
+ }
1289
+ }
1290
+ // this is a runtime guard to make catch an error if
1291
+ // the route name wrong, or there is a mismatch
1292
+ // between the gen2 schema definition and
1293
+ // whats in amplify_outputs
1294
+ if (!clientRoute) {
1295
+ setDataState({
1296
+ ...ERROR_STATE,
1297
+ data: { messages: [] },
1298
+ messages: [
1299
+ {
1300
+ message: 'Conversation route does not exist',
1301
+ errorInfo: null,
1302
+ errorType: '',
1303
+ },
1304
+ ],
1165
1305
  });
1306
+ return;
1166
1307
  }
1167
1308
  initialize();
1168
- }, [clientRoute, input.id, routeName, setRouteToConversationsMap]);
1169
- // Update messages to match what is in AIContext if they aren't equal
1309
+ return () => {
1310
+ contentBlocksRef.current = undefined;
1311
+ if (hasStarted(initRef.current))
1312
+ return;
1313
+ setDataState({
1314
+ ...INITIAL_STATE,
1315
+ data: { messages: [], conversation: undefined },
1316
+ });
1317
+ };
1318
+ }, [clientRoute, id, setDataState]);
1319
+ // Run a separate effect that is triggered by the conversation state
1320
+ // so that we know we have a conversation object to set up the subscription
1321
+ // and also unsubscribe on cleanup
1170
1322
  React__namespace["default"].useEffect(() => {
1171
- if (!!messagesFromAIContext && messagesFromAIContext !== localMessages)
1172
- setLocalMessages(messagesFromAIContext);
1173
- }, [messagesFromAIContext, localMessages]);
1174
- const sendMessage = React__namespace["default"].useCallback((input) => {
1175
- const { content, aiContext, toolConfiguration } = input;
1176
- conversation
1177
- ?.sendMessage({ content, aiContext, toolConfiguration })
1178
- .then((value) => {
1179
- const { data: sentMessage } = value;
1180
- if (sentMessage) {
1181
- setWaitingForAIResponse(true);
1182
- setLocalMessages((previousLocalMessages) => [
1183
- ...previousLocalMessages,
1184
- sentMessage,
1185
- ]);
1186
- setRouteToConversationsMap((previousValue) => {
1187
- return createNewConversationMessageInRoute({
1188
- previousValue,
1189
- routeName: routeName,
1190
- conversationId: conversation.id,
1191
- messages: [
1192
- ...previousValue[routeName][conversation.id],
1193
- sentMessage,
1194
- ],
1323
+ if (!conversation)
1324
+ return;
1325
+ const subscription = conversation.onStreamEvent({
1326
+ next: (event) => {
1327
+ const {
1328
+ // messages have a content block array,
1329
+ // this is the index of the content block that was updated
1330
+ contentBlockIndex,
1331
+ // this is the index of the content chunk, ensure these are in order!
1332
+ contentBlockDeltaIndex,
1333
+ // this is sent after the last content chunk, verify this matches the
1334
+ // previous contentBlockDeltaIndex
1335
+ contentBlockDoneAtIndex,
1336
+ // this is the final event of the conversation turn
1337
+ stopReason, conversationId, id, } = event;
1338
+ // return early for content blocks being done
1339
+ // or conversation turn being over
1340
+ if (contentBlockDoneAtIndex) {
1341
+ return;
1342
+ }
1343
+ // stop reason will signify end of conversation turn
1344
+ if (stopReason) {
1345
+ // remove loading state from streamed message
1346
+ setDataState((prev) => {
1347
+ return {
1348
+ ...prev,
1349
+ data: {
1350
+ ...prev.data,
1351
+ messages: prev.data.messages.map((message) => ({
1352
+ ...message,
1353
+ isLoading: false,
1354
+ })),
1355
+ },
1356
+ };
1195
1357
  });
1196
- });
1197
- }
1198
- })
1199
- .catch((reason) => {
1200
- setHasError(true);
1201
- setErrorMessage(`error sending message ${reason}`);
1202
- });
1203
- }, [conversation, routeName, setRouteToConversationsMap]);
1204
- const subscribe = React__namespace["default"].useCallback((handleStoreChange) => {
1205
- const subscription = conversation &&
1206
- conversation.onMessage((message) => {
1207
- if (input.onResponse)
1208
- input.onResponse(message);
1209
- setWaitingForAIResponse(false);
1210
- setLocalMessages((previousLocalMessages) => [
1211
- ...previousLocalMessages,
1212
- message,
1213
- ]);
1214
- setRouteToConversationsMap((previousValue) => {
1215
- return createNewConversationMessageInRoute({
1216
- previousValue,
1217
- routeName: routeName,
1218
- conversationId: conversation.id,
1219
- messages: [
1220
- ...previousValue[routeName][conversation.id],
1221
- message,
1222
- ],
1358
+ onMessage?.({
1359
+ id,
1360
+ conversationId,
1361
+ content: contentFromEvents(contentBlocksRef.current),
1362
+ createdAt: new Date().toISOString(),
1363
+ role: 'assistant',
1364
+ isLoading: true,
1223
1365
  });
1366
+ // clear out the stream cache
1367
+ contentBlocksRef.current = undefined;
1368
+ return;
1369
+ }
1370
+ // no ref means its the first event for the message stream
1371
+ // so lets create the contentBlocks ref or else we will
1372
+ // add the incoming event to the right content content block
1373
+ if (!contentBlocksRef.current) {
1374
+ contentBlocksRef.current = [[event]];
1375
+ }
1376
+ else {
1377
+ // place the incoming event in the right content block
1378
+ // and order. message content is an array so a single message
1379
+ // can have multiple content blocks, and each content block
1380
+ // can have multiple events/chunks
1381
+ const currentBlock = contentBlocksRef.current[contentBlockIndex];
1382
+ if (!currentBlock) {
1383
+ contentBlocksRef.current[contentBlockIndex] = [event];
1384
+ }
1385
+ else {
1386
+ contentBlocksRef.current[contentBlockIndex] = [
1387
+ ...currentBlock.slice(0, contentBlockDeltaIndex),
1388
+ event,
1389
+ ...currentBlock.slice(contentBlockDeltaIndex),
1390
+ ];
1391
+ }
1392
+ }
1393
+ setDataState((prev) => {
1394
+ const message = {
1395
+ id,
1396
+ conversationId,
1397
+ content: contentFromEvents(contentBlocksRef.current),
1398
+ createdAt: new Date().toISOString(),
1399
+ role: 'assistant',
1400
+ isLoading: true,
1401
+ };
1402
+ return {
1403
+ ...prev,
1404
+ data: {
1405
+ ...prev.data,
1406
+ // TODO: we are assuming we only update the last
1407
+ // message, but maybe we should match it by message ID?
1408
+ messages: [...prev.data.messages.slice(0, -1), message],
1409
+ },
1410
+ };
1224
1411
  });
1225
- handleStoreChange(); // should cause a re-render
1226
- });
1412
+ },
1413
+ error: (error) => {
1414
+ setDataState((prev) => {
1415
+ return {
1416
+ ...prev,
1417
+ ...ERROR_STATE,
1418
+ messages: error.errors,
1419
+ };
1420
+ });
1421
+ },
1422
+ });
1423
+ if (ui.isFunction(onInitialize)) {
1424
+ onInitialize(conversation);
1425
+ }
1227
1426
  return () => {
1228
- subscription?.unsubscribe();
1427
+ contentBlocksRef.current = undefined;
1428
+ subscription.unsubscribe();
1229
1429
  };
1230
- }, [conversation, routeName, setRouteToConversationsMap, input]);
1231
- const getSnapshot = React__namespace["default"].useCallback(() => localMessages, [localMessages]);
1232
- // Using useSyncExternalStore to subscribe to external data updates
1233
- // Have to provide third optional argument in next - https://github.com/vercel/next.js/issues/54685
1234
- const messagesFromStore = React__namespace["default"].useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
1235
- return [
1236
- {
1237
- data: { messages: messagesFromStore },
1238
- isLoading: waitingForAIResponse,
1239
- message: errorMessage,
1240
- hasError,
1241
- },
1242
- sendMessage,
1243
- ];
1430
+ }, [conversation, onInitialize, onMessage, setDataState]);
1431
+ const handleSendMessage = React__namespace["default"].useCallback((input) => {
1432
+ const { content } = input;
1433
+ if (conversation) {
1434
+ setDataState((prevState) => ({
1435
+ ...prevState,
1436
+ data: {
1437
+ ...prevState.data,
1438
+ // optimistically add user and assistant messages
1439
+ messages: [
1440
+ ...prevState.data.messages,
1441
+ {
1442
+ content,
1443
+ role: 'user',
1444
+ createdAt: new Date().toISOString(),
1445
+ id: 'temp-id',
1446
+ conversationId: conversation.id ?? '',
1447
+ },
1448
+ {
1449
+ content: [{ text: ' ' }],
1450
+ role: 'assistant',
1451
+ createdAt: new Date().toISOString(),
1452
+ id: 'temp-id-2',
1453
+ conversationId: conversation.id ?? '',
1454
+ isLoading: true,
1455
+ },
1456
+ ],
1457
+ },
1458
+ }));
1459
+ conversation.sendMessage(input);
1460
+ }
1461
+ else {
1462
+ setDataState((prev) => ({
1463
+ ...prev,
1464
+ ...ERROR_STATE,
1465
+ messages: [
1466
+ {
1467
+ message: 'No conversation found',
1468
+ errorInfo: null,
1469
+ errorType: '',
1470
+ },
1471
+ ],
1472
+ }));
1473
+ }
1474
+ }, [conversation]);
1475
+ return [dataState, handleSendMessage];
1244
1476
  };
1245
1477
  return useAIConversation;
1246
1478
  }
1247
1479
 
1248
- /**
1249
- * @experimental
1250
- */
1251
1480
  function createAIHooks(_client) {
1252
1481
  const useAIConversation = createUseAIConversation(_client);
1253
1482
  const useAIGeneration = createUseAIGeneration(_client);
1254
1483
  return { useAIConversation, useAIGeneration };
1255
1484
  }
1256
1485
 
1257
- exports.AIContextProvider = AIContextProvider;
1258
1486
  exports.AIConversation = AIConversation;
1259
1487
  exports.createAIConversation = createAIConversation;
1260
1488
  exports.createAIHooks = createAIHooks;