@aws-amplify/ui-react-ai 0.4.0 → 1.0.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 (56) 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/FallbackComponentContext.mjs +8 -0
  5. package/dist/esm/components/AIConversation/context/ResponseComponentsContext.mjs +6 -2
  6. package/dist/esm/components/AIConversation/createAIConversation.mjs +2 -5
  7. package/dist/esm/components/AIConversation/views/Controls/ActionsBarControl.mjs +3 -2
  8. package/dist/esm/components/AIConversation/views/Controls/AttachFileControl.mjs +2 -0
  9. package/dist/esm/components/AIConversation/views/Controls/AttachmentListControl.mjs +2 -0
  10. package/dist/esm/components/AIConversation/views/Controls/AvatarControl.mjs +2 -0
  11. package/dist/esm/components/AIConversation/views/Controls/DefaultMessageControl.mjs +2 -0
  12. package/dist/esm/components/AIConversation/views/Controls/FormControl.mjs +11 -4
  13. package/dist/esm/components/AIConversation/views/Controls/MessagesControl.mjs +24 -31
  14. package/dist/esm/components/AIConversation/views/Controls/PromptControl.mjs +2 -0
  15. package/dist/esm/components/AIConversation/views/default/Form.mjs +4 -5
  16. package/dist/esm/components/AIConversation/views/default/MessageList.mjs +31 -16
  17. package/dist/esm/hooks/contentFromEvents.mjs +22 -0
  18. package/dist/esm/hooks/createAIHooks.mjs +0 -3
  19. package/dist/esm/hooks/exhaustivelyListMessages.mjs +19 -0
  20. package/dist/esm/hooks/shared.mjs +14 -0
  21. package/dist/esm/hooks/useAIConversation.mjs +246 -106
  22. package/dist/esm/hooks/useAIGeneration.mjs +1 -8
  23. package/dist/esm/index.mjs +0 -1
  24. package/dist/esm/version.mjs +1 -1
  25. package/dist/index.js +396 -236
  26. package/dist/types/components/AIConversation/AIConversation.d.ts +0 -3
  27. package/dist/types/components/AIConversation/AIConversationProvider.d.ts +1 -1
  28. package/dist/types/components/AIConversation/context/AIContextContext.d.ts +6 -0
  29. package/dist/types/components/AIConversation/context/ControlsContext.d.ts +1 -0
  30. package/dist/types/components/AIConversation/context/FallbackComponentContext.d.ts +7 -0
  31. package/dist/types/components/AIConversation/context/ResponseComponentsContext.d.ts +2 -2
  32. package/dist/types/components/AIConversation/context/elements/definitions.d.ts +1 -1
  33. package/dist/types/components/AIConversation/context/index.d.ts +2 -0
  34. package/dist/types/components/AIConversation/createAIConversation.d.ts +0 -3
  35. package/dist/types/components/AIConversation/index.d.ts +2 -1
  36. package/dist/types/components/AIConversation/types.d.ts +4 -24
  37. package/dist/types/components/AIConversation/views/Controls/MessagesControl.d.ts +1 -5
  38. package/dist/types/components/AIConversation/views/default/Form.d.ts +1 -1
  39. package/dist/types/components/AIConversation/views/default/MessageList.d.ts +1 -1
  40. package/dist/types/components/AIConversation/views/default/PromptList.d.ts +1 -1
  41. package/dist/types/hooks/contentFromEvents.d.ts +2 -0
  42. package/dist/types/hooks/createAIHooks.d.ts +0 -3
  43. package/dist/types/hooks/exhaustivelyListMessages.d.ts +8 -0
  44. package/dist/types/hooks/index.d.ts +1 -2
  45. package/dist/types/hooks/shared.d.ts +23 -0
  46. package/dist/types/hooks/useAIConversation.d.ts +6 -4
  47. package/dist/types/hooks/useAIGeneration.d.ts +3 -13
  48. package/dist/types/index.d.ts +1 -1
  49. package/dist/types/types.d.ts +32 -1
  50. package/dist/types/version.d.ts +1 -1
  51. package/package.json +4 -4
  52. package/dist/ai-conversation-styles.css +0 -195
  53. package/dist/ai-conversation-styles.js +0 -2
  54. package/dist/esm/hooks/AIContextProvider.mjs +0 -20
  55. package/dist/types/ai-conversation-styles.d.ts +0 -1
  56. 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));
@@ -141,8 +146,12 @@ const convertResponseComponentsToToolConfiguration = (responseComponents) => {
141
146
  const { props } = responseComponents[toolName];
142
147
  const requiredProps = [];
143
148
  Object.keys(props).forEach((propName) => {
144
- if (props[propName].required)
149
+ if (props[propName].required) {
145
150
  requiredProps.push(propName);
151
+ // The inputSchema for a tool needs to not
152
+ // have `required` in the properties
153
+ props[propName].required = undefined;
154
+ }
146
155
  });
147
156
  tools[toolName] = {
148
157
  description: responseComponents[toolName].description,
@@ -181,6 +190,11 @@ const WelcomeMessageProvider = ({ children, welcomeMessage, }) => {
181
190
  return (React__namespace.createElement(WelcomeMessageContext.Provider, { value: welcomeMessage }, children));
182
191
  };
183
192
 
193
+ const FallbackComponentContext = React__namespace["default"].createContext(undefined);
194
+ const FallbackComponentProvider = ({ children, FallbackComponent, }) => {
195
+ return (React__namespace["default"].createElement(FallbackComponentContext.Provider, { value: FallbackComponent }, children));
196
+ };
197
+
184
198
  const DEFAULT_ICON_PATHS = {
185
199
  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
200
  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',
@@ -269,9 +283,9 @@ const AIConversationElements = {
269
283
  View: ViewElement,
270
284
  };
271
285
 
272
- const { Button: Button$4, Span: Span$3, View: View$6 } = AIConversationElements;
286
+ const { Button: Button$4, Span: Span$2, View: View$6 } = AIConversationElements;
273
287
  const ACTIONS_BAR_BLOCK = 'ai-actions-bar';
274
- const ActionIcon = elements.withBaseElementProps(Span$3, {
288
+ const ActionIcon = elements.withBaseElementProps(Span$2, {
275
289
  'aria-hidden': 'true',
276
290
  className: `${ACTIONS_BAR_BLOCK}__icon`,
277
291
  });
@@ -286,14 +300,13 @@ const Container$3 = elements.withBaseElementProps(View$6, {
286
300
  });
287
301
  const ActionsBarControl = ({ message, focusable, }) => {
288
302
  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))))));
303
+ 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
304
  };
292
305
  ActionsBarControl.Button = ActionButton;
293
306
  ActionsBarControl.Container = Container$3;
294
307
  ActionsBarControl.Icon = ActionIcon;
295
308
 
296
- const { Icon: Icon$3, Span: Span$2, Text: Text$2, View: View$5 } = AIConversationElements;
309
+ const { Icon: Icon$3, Span: Span$1, Text: Text$2, View: View$5 } = AIConversationElements;
297
310
  const AVATAR_BLOCK = 'ai-avatar';
298
311
  const DEFAULT_USER_ICON = elements.withBaseElementProps(Icon$3, {
299
312
  variant: 'user-avatar',
@@ -304,7 +317,7 @@ const DEFAULT_AI_ICON = () => (React__namespace["default"].createElement("svg",
304
317
  const AvatarDisplayName = elements.withBaseElementProps(Text$2, {
305
318
  className: `${AVATAR_BLOCK}__display-name`,
306
319
  });
307
- const AvatarIcon = elements.withBaseElementProps(Span$2, {
320
+ const AvatarIcon = elements.withBaseElementProps(Span$1, {
308
321
  'aria-hidden': true,
309
322
  className: `${AVATAR_BLOCK}__icon`,
310
323
  });
@@ -385,7 +398,7 @@ AttachFileControl.Icon = AttachFileIcon;
385
398
  AttachFileControl.Button = AttachFileButton;
386
399
  AttachFileControl.Container = AttachFileContainer;
387
400
 
388
- const { Button: Button$2, Icon: Icon$1, ListItem, UnorderedList: ListElement, Span: Span$1, Text: Text$1, View: View$3, } = AIConversationElements;
401
+ const { Button: Button$2, Icon: Icon$1, ListItem, UnorderedList: ListElement, Span, Text: Text$1, View: View$3, } = AIConversationElements;
389
402
  const IMAGE_LIST_BLOCK = 'ai-attachment-list';
390
403
  const IMAGE_ITEM_BLOCK = 'ai-attachment';
391
404
  const REMOVE_IMAGE_BLOCK = 'ai-remove-attachment';
@@ -416,7 +429,7 @@ const FileNameText = elements.withBaseElementProps(Text$1, {
416
429
  const FileSizeText = elements.withBaseElementProps(Text$1, {
417
430
  className: `${IMAGE_TEXT_BLOCK}__file-size`,
418
431
  });
419
- const Separator$1 = elements.withBaseElementProps(Span$1, {
432
+ const Separator = elements.withBaseElementProps(Span, {
420
433
  'aria-hidden': true,
421
434
  className: `${IMAGE_TEXT_BLOCK}__separator`,
422
435
  children: '|',
@@ -427,13 +440,13 @@ const TextContainer = elements.withBaseElementProps(View$3, {
427
440
  const TextControl = ({ fileName, fileSize }) => {
428
441
  return (React__namespace["default"].createElement(TextContainer, null,
429
442
  React__namespace["default"].createElement(FileNameText, null, fileName),
430
- React__namespace["default"].createElement(Separator$1, null),
443
+ React__namespace["default"].createElement(Separator, null),
431
444
  React__namespace["default"].createElement(FileSizeText, null, fileSize)));
432
445
  };
433
446
  TextControl.Container = TextContainer;
434
447
  TextControl.FileName = FileNameText;
435
448
  TextControl.FileSize = FileSizeText;
436
- TextControl.Separator = Separator$1;
449
+ TextControl.Separator = Separator;
437
450
  const Container$1 = elements.withBaseElementProps(ListItem, {
438
451
  className: `${IMAGE_ITEM_BLOCK}__list-item`,
439
452
  });
@@ -551,9 +564,12 @@ const FormControl = () => {
551
564
  const { input, setInput } = React__namespace["default"].useContext(ConversationInputContext);
552
565
  const handleSendMessage = React__namespace["default"].useContext(SendMessageContext);
553
566
  const allowAttachments = React__namespace["default"].useContext(AttachmentContext);
554
- const ref = React__namespace["default"].useRef(null);
555
567
  const responseComponents = React__namespace["default"].useContext(ResponseComponentsContext);
568
+ const isLoading = React__namespace["default"].useContext(LoadingContext);
569
+ const aiContext = React__namespace["default"].useContext(AIContextContext);
570
+ const ref = React__namespace["default"].useRef(null);
556
571
  const controls = React__namespace["default"].useContext(ControlsContext);
572
+ const [composing, setComposing] = React__namespace["default"].useState(false);
557
573
  const submitMessage = async () => {
558
574
  ref.current?.reset();
559
575
  const submittedContent = [];
@@ -578,6 +594,7 @@ const FormControl = () => {
578
594
  if (handleSendMessage) {
579
595
  handleSendMessage({
580
596
  content: submittedContent,
597
+ aiContext: ui.isFunction(aiContext) ? aiContext() : undefined,
581
598
  toolConfiguration: convertResponseComponentsToToolConfiguration(responseComponents),
582
599
  });
583
600
  }
@@ -590,7 +607,7 @@ const FormControl = () => {
590
607
  };
591
608
  const handleOnKeyDown = (event) => {
592
609
  const { key, shiftKey } = event;
593
- if (key === 'Enter' && !shiftKey) {
610
+ if (key === 'Enter' && !shiftKey && !composing) {
594
611
  event.preventDefault();
595
612
  const hasInput = !!input?.text || (input?.files?.length && input?.files?.length > 0);
596
613
  if (hasInput) {
@@ -599,14 +616,14 @@ const FormControl = () => {
599
616
  }
600
617
  };
601
618
  if (controls?.Form) {
602
- return (React__namespace["default"].createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, allowAttachments: allowAttachments }));
619
+ return (React__namespace["default"].createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, allowAttachments: allowAttachments, isLoading: isLoading }));
603
620
  }
604
621
  return (React__namespace["default"].createElement("form", { className: `${FIELD_BLOCK}__form`, onSubmit: handleSubmit, method: "post", ref: ref },
605
622
  allowAttachments ? React__namespace["default"].createElement(AttachFileControl, null) : null,
606
623
  React__namespace["default"].createElement(InputContainer, null,
607
624
  React__namespace["default"].createElement(VisuallyHidden, null,
608
625
  React__namespace["default"].createElement(Label, null)),
609
- React__namespace["default"].createElement(TextInput, { onKeyDown: handleOnKeyDown }),
626
+ React__namespace["default"].createElement(TextInput, { onKeyDown: handleOnKeyDown, onCompositionStart: () => setComposing(true), onCompositionEnd: () => setComposing(false) }),
610
627
  React__namespace["default"].createElement(AttachmentListControl, null)),
611
628
  React__namespace["default"].createElement(SendButton, null,
612
629
  React__namespace["default"].createElement(SendIcon, null))));
@@ -618,61 +635,52 @@ FormControl.TextInput = TextInput;
618
635
  FormControl.SendButton = SendButton;
619
636
  FormControl.SendIcon = SendIcon;
620
637
 
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) {
638
+ const { Text, View: View$1 } = AIConversationElements;
639
+ const MESSAGES_BLOCK = 'amplify-ai-conversation__message__list';
640
+ const MESSAGE_BLOCK = 'amplify-ai-conversation__message';
641
+ const MediaContent = (props) => {
628
642
  const variant = React__namespace["default"].useContext(MessageVariantContext);
629
643
  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
- });
644
+ return (React__namespace["default"].createElement(uiReact.Image, { className: ui.classNames(`${MESSAGE_BLOCK}__image`, variant && `${MESSAGE_BLOCK}__image--${variant}`, `${MESSAGE_BLOCK}__image--${role}`), ...props }));
645
+ };
632
646
  const TextContent = React__namespace["default"].forwardRef(function TextContent(props, ref) {
633
647
  return React__namespace["default"].createElement(Text, { ref: ref, className: `${MESSAGE_BLOCK}__text`, ...props });
634
648
  });
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
649
  const ToolContent = ({ toolUse, }) => {
640
- const responseComponents = React__namespace["default"].useContext(ResponseComponentsContext);
650
+ const responseComponents = React__namespace["default"].useContext(ResponseComponentsContext) ?? {};
651
+ const FallbackComponent = React__namespace["default"].useContext(FallbackComponentContext);
641
652
  // For now tool use is limited to custom response components
642
653
  const { name, input } = toolUse;
643
- if (!responseComponents ||
644
- !name ||
645
- !name.startsWith(RESPONSE_COMPONENT_PREFIX)) {
654
+ if (!name || !name.startsWith(RESPONSE_COMPONENT_PREFIX)) {
646
655
  return;
647
656
  }
648
657
  else {
649
658
  const response = responseComponents[name];
650
- const CustomComponent = response.component;
651
- return React__namespace["default"].createElement(CustomComponent, { ...input });
659
+ if (response) {
660
+ const CustomComponent = response.component;
661
+ return React__namespace["default"].createElement(CustomComponent, { ...input });
662
+ }
663
+ // fallback if there is a UI component message but we don't have
664
+ // a React component that matches
665
+ if (FallbackComponent) {
666
+ return React__namespace["default"].createElement(FallbackComponent, { ...input });
667
+ }
652
668
  }
653
669
  };
654
670
  const MessageControl = ({ message }) => {
655
671
  const messageRenderer = React__namespace["default"].useContext(MessageRendererContext);
656
- return (React__namespace["default"].createElement(ContentContainer, null, message.content.map((content, index) => {
672
+ return (React__namespace["default"].createElement(React__namespace["default"].Fragment, null, message.content.map((content, index) => {
657
673
  if (content.text) {
658
674
  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
675
  }
660
676
  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) }));
677
+ 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
678
  }
663
679
  else if (content.toolUse) {
664
680
  return React__namespace["default"].createElement(ToolContent, { toolUse: content.toolUse, key: index });
665
681
  }
666
682
  })));
667
683
  };
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
684
  const Timestamp = elements.withBaseElementProps(Text, {
677
685
  className: `${MESSAGE_BLOCK}__timestamp`,
678
686
  });
@@ -733,7 +741,6 @@ const MessagesControl = () => {
733
741
  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
742
  React__namespace["default"].createElement(HeaderContainer, null,
735
743
  React__namespace["default"].createElement(AvatarControl, null),
736
- React__namespace["default"].createElement(Separator, null),
737
744
  React__namespace["default"].createElement(Timestamp, null, getMessageTimestampText(new Date(message.createdAt)))),
738
745
  React__namespace["default"].createElement(MessageControl, { message: message }),
739
746
  message.role === 'assistant' ? (React__namespace["default"].createElement(ActionsBarControl, { message: message, focusable: focusedItemIndex === index })) : null)));
@@ -745,7 +752,6 @@ MessagesControl.Container = MessageContainer;
745
752
  MessagesControl.HeaderContainer = HeaderContainer;
746
753
  MessagesControl.Layout = Layout;
747
754
  MessagesControl.Message = MessageControl;
748
- MessagesControl.Separator = Separator;
749
755
 
750
756
  const { View, Button } = AIConversationElements;
751
757
  const PROMPT_BLOCK = 'ai-prompts';
@@ -789,25 +795,27 @@ PromptControl.Container = Container;
789
795
  PromptControl.PromptGroup = PromptGroup;
790
796
  PromptControl.PromptCard = PromptCard;
791
797
 
792
- const AIConversationProvider = ({ actions, allowAttachments, avatars, children, controls, displayText, elements: elements$1, handleSendMessage, isLoading, messages, responseComponents, suggestedPrompts, variant, welcomeMessage, }) => {
798
+ const AIConversationProvider = ({ aiContext, actions, allowAttachments, avatars, children, controls, displayText, handleSendMessage, isLoading, messages, messageRenderer, responseComponents, suggestedPrompts, variant, welcomeMessage, FallbackResponseComponent, }) => {
793
799
  const _displayText = {
794
800
  ...defaultAIConversationDisplayTextEn,
795
801
  ...displayText,
796
802
  };
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)))))))))))))));
803
+ return (React__namespace["default"].createElement(ControlsProvider, { controls: controls },
804
+ React__namespace["default"].createElement(SuggestedPromptProvider, { suggestedPrompts: suggestedPrompts },
805
+ React__namespace["default"].createElement(WelcomeMessageProvider, { welcomeMessage: welcomeMessage },
806
+ React__namespace["default"].createElement(FallbackComponentProvider, { FallbackComponent: FallbackResponseComponent },
807
+ React__namespace["default"].createElement(MessageRendererProvider, { ...messageRenderer },
808
+ React__namespace["default"].createElement(ResponseComponentsProvider, { responseComponents: responseComponents },
809
+ React__namespace["default"].createElement(AttachmentProvider, { allowAttachments: allowAttachments },
810
+ React__namespace["default"].createElement(ConversationDisplayTextProvider, { ..._displayText },
811
+ React__namespace["default"].createElement(ConversationInputContextProvider, null,
812
+ React__namespace["default"].createElement(SendMessageContextProvider, { handleSendMessage: handleSendMessage },
813
+ React__namespace["default"].createElement(AvatarsProvider, { avatars: avatars },
814
+ React__namespace["default"].createElement(ActionsProvider, { actions: actions },
815
+ React__namespace["default"].createElement(MessageVariantProvider, { variant: variant },
816
+ React__namespace["default"].createElement(MessagesProvider, { messages: messages },
817
+ React__namespace["default"].createElement(AIContextProvider, { aiContext: aiContext },
818
+ React__namespace["default"].createElement(LoadingContextProvider, { isLoading: isLoading }, children)))))))))))))))));
811
819
  };
812
820
 
813
821
  const DefaultMessageControl = () => {
@@ -820,15 +828,11 @@ const DefaultMessageControl = () => {
820
828
  }
821
829
  };
822
830
 
823
- /**
824
- * @experimental
825
- */
826
831
  function createAIConversation(input = {}) {
827
- const { elements, suggestedPrompts, actions, responseComponents, variant, controls, displayText, allowAttachments, messageRenderer, } = input;
832
+ const { suggestedPrompts, actions, responseComponents, variant, controls, displayText, allowAttachments, messageRenderer, FallbackResponseComponent, } = input;
828
833
  function AIConversation(props) {
829
834
  const { messages, avatars, handleSendMessage, isLoading } = props;
830
835
  const providerProps = {
831
- elements,
832
836
  actions,
833
837
  suggestedPrompts,
834
838
  responseComponents,
@@ -841,6 +845,7 @@ function createAIConversation(input = {}) {
841
845
  handleSendMessage,
842
846
  isLoading,
843
847
  messageRenderer,
848
+ FallbackResponseComponent,
844
849
  };
845
850
  return (React__namespace["default"].createElement(AIConversationProvider, { ...providerProps },
846
851
  React__namespace["default"].createElement(ViewElement, null,
@@ -857,6 +862,16 @@ function createAIConversation(input = {}) {
857
862
  return { AIConversation };
858
863
  }
859
864
 
865
+ const PlaceholderMessage = ({ role }) => {
866
+ const variant = React__namespace.useContext(MessageVariantContext);
867
+ return (React__namespace.createElement(uiReact.View, { className: ui.classNames(ui.ComponentClassName.AIConversationMessage, ui.classNameModifier(ui.ComponentClassName.AIConversationMessage, variant), ui.classNameModifier(ui.ComponentClassName.AIConversationMessage, role)) },
868
+ React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageAvatar },
869
+ React__namespace.createElement(uiReact.Avatar, null)),
870
+ React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageBody },
871
+ React__namespace.createElement(uiReact.Placeholder, { width: "25%" }),
872
+ React__namespace.createElement(uiReact.Placeholder, { width: "50%" }),
873
+ React__namespace.createElement(uiReact.Placeholder, { width: "25%" }))));
874
+ };
860
875
  const MessageMeta = ({ message }) => {
861
876
  // need to pass this in as props in order for it to be overridable
862
877
  const avatars = React__namespace.useContext(AvatarsContext);
@@ -868,29 +883,30 @@ const MessageMeta = ({ message }) => {
868
883
  React__namespace.createElement(uiReact.Text, { className: ui.ComponentClassName.AIConversationMessageSenderUsername }, avatar?.username),
869
884
  React__namespace.createElement(uiReact.Text, { className: ui.ComponentClassName.AIConversationMessageSenderTimestamp }, getMessageTimestampText(new Date(message.createdAt)))));
870
885
  };
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)))));
886
+ const MessageActions = ({ message }) => {
887
+ const actions = React__namespace.useContext(ActionsContext);
888
+ if (!actions)
889
+ return null;
890
+ return (React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageActions }, actions.map((action, i) => {
891
+ return (React__namespace.createElement(uiReact.Button, { key: i, size: "small", onClick: () => {
892
+ action.handler(message);
893
+ } }, action.component));
894
+ })));
881
895
  };
882
896
  const Message = ({ message }) => {
883
897
  const avatars = React__namespace.useContext(AvatarsContext);
884
898
  const variant = React__namespace.useContext(MessageVariantContext);
899
+ const { isLoading } = message;
885
900
  const avatar = message.role === 'assistant' ? avatars?.ai : avatars?.user;
886
901
  return (React__namespace.createElement(RoleContext.Provider, { value: message.role },
887
902
  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
903
  React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageAvatar },
889
- React__namespace.createElement(uiReact.Avatar, null, avatar?.avatar)),
904
+ React__namespace.createElement(uiReact.Avatar, { isLoading: isLoading }, avatar?.avatar)),
890
905
  React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageBody },
891
906
  React__namespace.createElement(MessageMeta, { message: message }),
892
907
  React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageContent },
893
- React__namespace.createElement(MessageControl, { message: message }))))));
908
+ React__namespace.createElement(MessageControl, { message: message })),
909
+ message.role === 'assistant' ? (React__namespace.createElement(MessageActions, { message: message })) : null))));
894
910
  };
895
911
  const MessageList = ({ messages, }) => {
896
912
  const isLoading = React__namespace.useContext(LoadingContext);
@@ -898,8 +914,10 @@ const MessageList = ({ messages, }) => {
898
914
  content.text ??
899
915
  content.toolUse?.name.startsWith(RESPONSE_COMPONENT_PREFIX))) ?? [];
900
916
  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));
917
+ isLoading ? (React__namespace.createElement(React__namespace.Fragment, null,
918
+ React__namespace.createElement(PlaceholderMessage, { role: "user" }),
919
+ React__namespace.createElement(PlaceholderMessage, { role: "assistant" }))) : null,
920
+ messagesWithRenderableContent.map((message, i) => (React__namespace.createElement(Message, { key: `message-${i}`, message: message })))));
903
921
  };
904
922
 
905
923
  const Attachment = ({ file, handleRemove, }) => {
@@ -945,12 +963,12 @@ const FormWrapper = ({ children, allowAttachments, setInput, }) => {
945
963
  return children;
946
964
  }
947
965
  };
948
- const Form = ({ setInput, input, handleSubmit, allowAttachments, }) => {
966
+ const Form = ({ setInput, input, handleSubmit, allowAttachments, isLoading, }) => {
949
967
  const icons = internal.useIcons('aiConversation');
950
968
  const sendIcon = icons?.send ?? React__namespace.createElement(internal.IconSend, null);
951
969
  const attachIcon = icons?.attach ?? React__namespace.createElement(internal.IconAttach, null);
952
970
  const hiddenInput = React__namespace.useRef(null);
953
- const isLoading = React__namespace.useContext(LoadingContext);
971
+ const [composing, setComposing] = React__namespace.useState(false);
954
972
  const isInputEmpty = !input?.text?.length && !input?.files?.length;
955
973
  return (React__namespace.createElement(FormWrapper, { allowAttachments: allowAttachments, setInput: setInput },
956
974
  React__namespace.createElement(uiReact.View, { as: "form", className: ui.ComponentClassName.AIConversationForm, onSubmit: handleSubmit },
@@ -972,9 +990,9 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, }) => {
972
990
  files: [...(prevValue?.files ?? []), ...Array.from(files)],
973
991
  }));
974
992
  }, 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) => {
993
+ 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
994
  // Submit on enter key if shift is not pressed also
977
- const shouldSubmit = !e.shiftKey && e.key === 'Enter';
995
+ const shouldSubmit = !e.shiftKey && e.key === 'Enter' && !composing;
978
996
  if (shouldSubmit && isHTMLFormElement(e.target)) {
979
997
  e.target.form.requestSubmit();
980
998
  e.preventDefault();
@@ -1004,9 +1022,9 @@ const PromptList = ({ setInput, suggestedPrompts = [], }) => {
1004
1022
  })));
1005
1023
  };
1006
1024
 
1007
- const VERSION = '0.4.0';
1025
+ const VERSION = '1.0.0';
1008
1026
 
1009
- function Provider({ actions, avatars, controls, handleSendMessage, messages, responseComponents, suggestedPrompts, variant, isLoading, displayText, allowAttachments, messageRenderer, children, }) {
1027
+ function AIConversationBase({ avatars, controls, ...rest }) {
1010
1028
  uiReactCore.useSetUserAgent({
1011
1029
  componentName: 'AIConversation',
1012
1030
  packageName: 'react-ai',
@@ -1016,83 +1034,53 @@ function Provider({ actions, avatars, controls, handleSendMessage, messages, res
1016
1034
  const defaultAvatars = {
1017
1035
  ai: {
1018
1036
  username: 'Assistant',
1019
- avatar: icons?.assistant ?? React__namespace.createElement(internal.IconAssistant, null),
1037
+ avatar: icons?.assistant ?? React__namespace.createElement(internal.IconAssistant, { testId: "icon-assistant" }),
1020
1038
  },
1021
1039
  user: {
1022
1040
  username: 'User',
1023
- avatar: icons?.user ?? React__namespace.createElement(internal.IconUser, null),
1041
+ avatar: icons?.user ?? React__namespace.createElement(internal.IconUser, { testId: "icon-user" }),
1024
1042
  },
1025
1043
  };
1026
1044
  const providerProps = {
1027
- messages,
1028
- handleSendMessage,
1045
+ ...rest,
1029
1046
  avatars: {
1030
1047
  ...defaultAvatars,
1031
1048
  ...avatars,
1032
1049
  },
1033
- isLoading,
1034
- elements: {
1035
- Text: uiReact.Text,
1036
- },
1037
- actions,
1038
- suggestedPrompts,
1039
- responseComponents,
1040
- variant,
1041
1050
  controls: {
1042
1051
  MessageList,
1043
1052
  PromptList,
1044
1053
  Form,
1045
1054
  ...controls,
1046
1055
  },
1047
- displayText,
1048
- allowAttachments,
1049
- messageRenderer,
1050
1056
  };
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 },
1057
+ return (React__namespace.createElement(AIConversationProvider, { ...providerProps },
1058
+ React__namespace.createElement(uiReact.Flex, { className: ui.ComponentClassName.AIConversation, testId: "ai-conversation" },
1056
1059
  React__namespace.createElement(uiReact.ScrollView, { autoScroll: "smooth", flex: "1" },
1057
1060
  React__namespace.createElement(DefaultMessageControl, null),
1058
1061
  React__namespace.createElement(MessagesControl, null)),
1059
1062
  React__namespace.createElement(FormControl, null))));
1060
1063
  }
1061
- /**
1062
- * @experimental
1063
- */
1064
1064
  const AIConversation = Object.assign(AIConversationBase, {
1065
- Provider,
1065
+ Provider: AIConversationProvider,
1066
1066
  DefaultMessage: DefaultMessageControl,
1067
1067
  Messages: MessagesControl,
1068
1068
  Form: FormControl,
1069
1069
  });
1070
1070
 
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
1071
  // default state
1089
1072
  const INITIAL_STATE = {
1090
1073
  hasError: false,
1091
1074
  isLoading: false,
1092
1075
  messages: undefined,
1093
1076
  };
1094
- const LOADING_STATE = { hasError: false, isLoading: true, messages: undefined };
1077
+ const LOADING_STATE = {
1078
+ hasError: false,
1079
+ isLoading: true,
1080
+ messages: undefined,
1081
+ };
1095
1082
  const ERROR_STATE = { hasError: true, isLoading: false };
1083
+
1096
1084
  function createUseAIGeneration(client) {
1097
1085
  const useAIGeneration = (routeName) => {
1098
1086
  const [dataState, setDataState] = React__namespace.useState(() => ({
@@ -1119,142 +1107,314 @@ function createUseAIGeneration(client) {
1119
1107
  return useAIGeneration;
1120
1108
  }
1121
1109
 
1122
- function createNewConversationMessageInRoute({ previousValue, routeName, conversationId, messages, }) {
1110
+ const contentFromEvents = (contentBlocks) => {
1111
+ if (!contentBlocks)
1112
+ return [];
1113
+ return contentBlocks.map((contentBlock) => {
1114
+ const isTextBlock = contentBlock.some((event) => event.text);
1115
+ if (isTextBlock) {
1116
+ return {
1117
+ text: contentBlock
1118
+ .map((event) => {
1119
+ return event.text;
1120
+ })
1121
+ .join(''),
1122
+ };
1123
+ }
1124
+ // tool use is never chunked
1125
+ if (contentBlock[0].toolUse) {
1126
+ return { toolUse: contentBlock[0].toolUse };
1127
+ }
1128
+ });
1129
+ };
1130
+
1131
+ async function exhaustivelyListMessages({ conversation, messages = [], nextToken, }) {
1132
+ const result = await conversation.listMessages({ nextToken });
1133
+ if (result.data) {
1134
+ messages?.push(...result.data);
1135
+ }
1136
+ if (result.nextToken) {
1137
+ return exhaustivelyListMessages({
1138
+ conversation,
1139
+ messages,
1140
+ nextToken: result.nextToken,
1141
+ });
1142
+ }
1123
1143
  return {
1124
- ...previousValue,
1125
- [routeName]: {
1126
- ...previousValue[routeName],
1127
- [conversationId]: messages,
1128
- },
1144
+ ...result,
1145
+ data: messages,
1129
1146
  };
1130
1147
  }
1148
+
1149
+ function hasStarted(state) {
1150
+ return ['initialLoading', 'initialized'].includes(state);
1151
+ }
1131
1152
  function createUseAIConversation(client) {
1153
+ // This is a bit complicated so buckle up.
1154
+ // The way the data client works is conversation.get() or conversation.create()
1155
+ // is an async function because it makes a graphql call to appsync
1156
+ // then it returns a conversation object, which is like a normal
1157
+ // data client record, except that it also has functions on it,
1158
+ // like sendMessage and onStreamEvent. onStreamEvent sets up a
1159
+ // subscription using a websocket connection, which ideally we only want to
1160
+ // do once per conversation. Because we can only subscribe AFTER the
1161
+ // async call to get/create the conversation is made, the cleanup
1162
+ // function in the effect will won't actually unsubscribe
1132
1163
  const useAIConversation = (routeName, input = {}) => {
1133
1164
  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
1165
+ // We need to keep track of the stream events as the come in
1166
+ // for an assistant message, but don't need to keep them in state
1167
+ const contentBlocksRef = React__namespace["default"].useRef();
1168
+ // Using this hook without an existing conversation id means
1169
+ // it will create a new conversation when it is executed
1170
+ // we don't want to create 2 conversations
1171
+ const initRef = React__namespace["default"].useRef('initial');
1172
+ const [dataState, setDataState] = React__namespace["default"].useState(() => ({
1173
+ ...INITIAL_STATE,
1174
+ data: { messages: [], conversation: undefined },
1175
+ }));
1176
+ const { conversation } = dataState.data;
1177
+ const { id, onInitialize, onMessage } = input;
1144
1178
  React__namespace["default"].useEffect(() => {
1145
1179
  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);
1180
+ // We don't want to run the effect multiple times
1181
+ // because that could create multiple conversation records
1182
+ if (hasStarted(initRef.current))
1183
+ return;
1184
+ initRef.current = 'initialLoading';
1185
+ // Only show component loading state if we are
1186
+ // actually loading messages
1187
+ if (id) {
1188
+ setDataState({
1189
+ ...LOADING_STATE,
1190
+ data: { messages: [], conversation: undefined },
1191
+ });
1154
1192
  }
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,
1193
+ const { data: conversation, errors } = id
1194
+ ? await clientRoute.get({ id })
1195
+ : await clientRoute.create();
1196
+ if (errors ?? !conversation) {
1197
+ setDataState({
1198
+ ...ERROR_STATE,
1199
+ data: { messages: [] },
1200
+ messages: errors,
1164
1201
  });
1202
+ }
1203
+ else {
1204
+ if (id) {
1205
+ const { data: messages } = await exhaustivelyListMessages({
1206
+ conversation,
1207
+ });
1208
+ setDataState({
1209
+ ...INITIAL_STATE,
1210
+ data: { messages, conversation },
1211
+ });
1212
+ }
1213
+ else {
1214
+ setDataState({
1215
+ ...INITIAL_STATE,
1216
+ data: { conversation, messages: [] },
1217
+ });
1218
+ }
1219
+ initRef.current = 'initialized';
1220
+ }
1221
+ }
1222
+ // this is a runtime guard to make catch an error if
1223
+ // the route name wrong, or there is a mismatch
1224
+ // between the gen2 schema definition and
1225
+ // whats in amplify_outputs
1226
+ if (!clientRoute) {
1227
+ setDataState({
1228
+ ...ERROR_STATE,
1229
+ data: { messages: [] },
1230
+ messages: [
1231
+ {
1232
+ message: 'Conversation route does not exist',
1233
+ errorInfo: null,
1234
+ errorType: '',
1235
+ },
1236
+ ],
1165
1237
  });
1238
+ return;
1166
1239
  }
1167
1240
  initialize();
1168
- }, [clientRoute, input.id, routeName, setRouteToConversationsMap]);
1169
- // Update messages to match what is in AIContext if they aren't equal
1241
+ return () => {
1242
+ contentBlocksRef.current = undefined;
1243
+ if (hasStarted(initRef.current))
1244
+ return;
1245
+ setDataState({
1246
+ ...INITIAL_STATE,
1247
+ data: { messages: [], conversation: undefined },
1248
+ });
1249
+ };
1250
+ }, [clientRoute, id, setDataState]);
1251
+ // Run a separate effect that is triggered by the conversation state
1252
+ // so that we know we have a conversation object to set up the subscription
1253
+ // and also unsubscribe on cleanup
1170
1254
  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
- ],
1255
+ if (!conversation)
1256
+ return;
1257
+ const subscription = conversation.onStreamEvent({
1258
+ next: (event) => {
1259
+ const {
1260
+ // messages have a content block array,
1261
+ // this is the index of the content block that was updated
1262
+ contentBlockIndex,
1263
+ // this is the index of the content chunk, ensure these are in order!
1264
+ contentBlockDeltaIndex,
1265
+ // this is sent after the last content chunk, verify this matches the
1266
+ // previous contentBlockDeltaIndex
1267
+ contentBlockDoneAtIndex,
1268
+ // this is the final event of the conversation turn
1269
+ stopReason, conversationId, id, } = event;
1270
+ // return early for content blocks being done
1271
+ // or conversation turn being over
1272
+ if (contentBlockDoneAtIndex) {
1273
+ return;
1274
+ }
1275
+ // stop reason will signify end of conversation turn
1276
+ if (stopReason) {
1277
+ // remove loading state from streamed message
1278
+ setDataState((prev) => {
1279
+ return {
1280
+ ...prev,
1281
+ data: {
1282
+ ...prev.data,
1283
+ messages: prev.data.messages.map((message) => ({
1284
+ ...message,
1285
+ isLoading: false,
1286
+ })),
1287
+ },
1288
+ };
1195
1289
  });
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
- ],
1290
+ onMessage?.({
1291
+ id,
1292
+ conversationId,
1293
+ content: contentFromEvents(contentBlocksRef.current),
1294
+ createdAt: new Date().toISOString(),
1295
+ role: 'assistant',
1296
+ isLoading: true,
1223
1297
  });
1298
+ // clear out the stream cache
1299
+ contentBlocksRef.current = undefined;
1300
+ return;
1301
+ }
1302
+ // no ref means its the first event for the message stream
1303
+ // so lets create the contentBlocks ref or else we will
1304
+ // add the incoming event to the right content content block
1305
+ if (!contentBlocksRef.current) {
1306
+ contentBlocksRef.current = [[event]];
1307
+ }
1308
+ else {
1309
+ // place the incoming event in the right content block
1310
+ // and order. message content is an array so a single message
1311
+ // can have multiple content blocks, and each content block
1312
+ // can have multiple events/chunks
1313
+ const currentBlock = contentBlocksRef.current[contentBlockIndex];
1314
+ if (!currentBlock) {
1315
+ contentBlocksRef.current[contentBlockIndex] = [event];
1316
+ }
1317
+ else {
1318
+ contentBlocksRef.current[contentBlockIndex] = [
1319
+ ...currentBlock.slice(0, contentBlockDeltaIndex),
1320
+ event,
1321
+ ...currentBlock.slice(contentBlockDeltaIndex),
1322
+ ];
1323
+ }
1324
+ }
1325
+ setDataState((prev) => {
1326
+ const message = {
1327
+ id,
1328
+ conversationId,
1329
+ content: contentFromEvents(contentBlocksRef.current),
1330
+ createdAt: new Date().toISOString(),
1331
+ role: 'assistant',
1332
+ isLoading: true,
1333
+ };
1334
+ return {
1335
+ ...prev,
1336
+ data: {
1337
+ ...prev.data,
1338
+ // TODO: we are assuming we only update the last
1339
+ // message, but maybe we should match it by message ID?
1340
+ messages: [...prev.data.messages.slice(0, -1), message],
1341
+ },
1342
+ };
1224
1343
  });
1225
- handleStoreChange(); // should cause a re-render
1226
- });
1344
+ },
1345
+ error: (error) => {
1346
+ setDataState((prev) => {
1347
+ return {
1348
+ ...prev,
1349
+ ...ERROR_STATE,
1350
+ messages: error.errors,
1351
+ };
1352
+ });
1353
+ },
1354
+ });
1355
+ if (ui.isFunction(onInitialize)) {
1356
+ onInitialize(conversation);
1357
+ }
1227
1358
  return () => {
1228
- subscription?.unsubscribe();
1359
+ contentBlocksRef.current = undefined;
1360
+ subscription.unsubscribe();
1229
1361
  };
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
- ];
1362
+ }, [conversation, onInitialize, onMessage, setDataState]);
1363
+ const handleSendMessage = React__namespace["default"].useCallback((input) => {
1364
+ const { content } = input;
1365
+ if (conversation) {
1366
+ setDataState((prevState) => ({
1367
+ ...prevState,
1368
+ data: {
1369
+ ...prevState.data,
1370
+ // optimistically add user and assistant messages
1371
+ messages: [
1372
+ ...prevState.data.messages,
1373
+ {
1374
+ content,
1375
+ role: 'user',
1376
+ createdAt: new Date().toISOString(),
1377
+ id: 'temp-id',
1378
+ conversationId: conversation.id ?? '',
1379
+ },
1380
+ {
1381
+ content: [{ text: ' ' }],
1382
+ role: 'assistant',
1383
+ createdAt: new Date().toISOString(),
1384
+ id: 'temp-id-2',
1385
+ conversationId: conversation.id ?? '',
1386
+ isLoading: true,
1387
+ },
1388
+ ],
1389
+ },
1390
+ }));
1391
+ conversation.sendMessage(input);
1392
+ }
1393
+ else {
1394
+ setDataState((prev) => ({
1395
+ ...prev,
1396
+ ...ERROR_STATE,
1397
+ messages: [
1398
+ {
1399
+ message: 'No conversation found',
1400
+ errorInfo: null,
1401
+ errorType: '',
1402
+ },
1403
+ ],
1404
+ }));
1405
+ }
1406
+ }, [conversation]);
1407
+ return [dataState, handleSendMessage];
1244
1408
  };
1245
1409
  return useAIConversation;
1246
1410
  }
1247
1411
 
1248
- /**
1249
- * @experimental
1250
- */
1251
1412
  function createAIHooks(_client) {
1252
1413
  const useAIConversation = createUseAIConversation(_client);
1253
1414
  const useAIGeneration = createUseAIGeneration(_client);
1254
1415
  return { useAIConversation, useAIGeneration };
1255
1416
  }
1256
1417
 
1257
- exports.AIContextProvider = AIContextProvider;
1258
1418
  exports.AIConversation = AIConversation;
1259
1419
  exports.createAIConversation = createAIConversation;
1260
1420
  exports.createAIHooks = createAIHooks;