diff --git a/.changeset/seven-bats-travel.md b/.changeset/seven-bats-travel.md
new file mode 100644
index 00000000000..03cde47f4d6
--- /dev/null
+++ b/.changeset/seven-bats-travel.md
@@ -0,0 +1,15 @@
+---
+"@aws-amplify/ui-react-ai": minor
+---
+
+feat(ai-conversation): add allowAttachments prop
+
+BREAKING - This is a breaking change to an experimental API. Previously, the AIConversation component always allowed attachments. Now you will need to provide the `allowAttachments` prop to get the same behavior. The reason for this change is that attachments can quickly cost a lot based on the token use and we didn't want the default behavior to have that.
+
+```jsx
+
+```
diff --git a/examples/next/pages/ui/components/ai/ai-conversation/index.page.tsx b/examples/next/pages/ui/components/ai/ai-conversation/index.page.tsx
index e043a999150..b71fbecebf9 100644
--- a/examples/next/pages/ui/components/ai/ai-conversation/index.page.tsx
+++ b/examples/next/pages/ui/components/ai/ai-conversation/index.page.tsx
@@ -36,6 +36,7 @@ function Chat() {
messages={messages}
handleSendMessage={sendMessage}
isLoading={isLoading}
+ allowAttachments
suggestedPrompts={[
{
inputText: 'hello',
diff --git a/packages/react-ai/src/components/AIConversation/AIConversation.tsx b/packages/react-ai/src/components/AIConversation/AIConversation.tsx
index b60f3166b07..a5d74c0944a 100644
--- a/packages/react-ai/src/components/AIConversation/AIConversation.tsx
+++ b/packages/react-ai/src/components/AIConversation/AIConversation.tsx
@@ -30,6 +30,7 @@ function AIConversationBase({
variant,
isLoading,
displayText,
+ allowAttachments,
}: AIConversationBaseProps): JSX.Element {
const icons = useIcons('aiConversation');
const defaultAvatars: Avatars = {
@@ -72,6 +73,7 @@ function AIConversationBase({
...avatars,
},
isLoading,
+ allowAttachments,
};
return (
diff --git a/packages/react-ai/src/components/AIConversation/context/AttachmentContext.tsx b/packages/react-ai/src/components/AIConversation/context/AttachmentContext.tsx
new file mode 100644
index 00000000000..8a51e7dd928
--- /dev/null
+++ b/packages/react-ai/src/components/AIConversation/context/AttachmentContext.tsx
@@ -0,0 +1,17 @@
+import * as React from 'react';
+
+export const AttachmentContext = React.createContext(false);
+
+export const AttachmentProvider = ({
+ children,
+ allowAttachments,
+}: {
+ children?: React.ReactNode;
+ allowAttachments?: boolean;
+}): JSX.Element => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/react-ai/src/components/AIConversation/context/ControlsContext.tsx b/packages/react-ai/src/components/AIConversation/context/ControlsContext.tsx
index 1b13c88c0b7..096c7c4aba8 100644
--- a/packages/react-ai/src/components/AIConversation/context/ControlsContext.tsx
+++ b/packages/react-ai/src/components/AIConversation/context/ControlsContext.tsx
@@ -7,6 +7,7 @@ export interface ControlsContextProps {
Form?: React.ComponentType<
{
handleSubmit: (e: React.FormEvent) => void;
+ allowAttachments?: boolean;
} & Required
>;
MessageList?: React.ComponentType<{ messages: ConversationMessage[] }>;
diff --git a/packages/react-ai/src/components/AIConversation/createAIConversation.tsx b/packages/react-ai/src/components/AIConversation/createAIConversation.tsx
index 58789d2bc22..f894133611a 100644
--- a/packages/react-ai/src/components/AIConversation/createAIConversation.tsx
+++ b/packages/react-ai/src/components/AIConversation/createAIConversation.tsx
@@ -30,6 +30,7 @@ export function createAIConversation(input: AIConversationInput = {}): {
variant,
controls,
displayText,
+ allowAttachments,
} = input;
const Provider = createProvider({
@@ -40,6 +41,7 @@ export function createAIConversation(input: AIConversationInput = {}): {
variant,
controls,
displayText,
+ allowAttachments,
});
function AIConversation(props: AIConversationProps): JSX.Element {
diff --git a/packages/react-ai/src/components/AIConversation/createProvider.tsx b/packages/react-ai/src/components/AIConversation/createProvider.tsx
index cede94d9dfc..0d0f841eb87 100644
--- a/packages/react-ai/src/components/AIConversation/createProvider.tsx
+++ b/packages/react-ai/src/components/AIConversation/createProvider.tsx
@@ -17,6 +17,7 @@ import {
ResponseComponentsProvider,
SendMessageContextProvider,
} from './context';
+import { AttachmentProvider } from './context/AttachmentContext';
export default function createProvider({
elements,
@@ -26,6 +27,7 @@ export default function createProvider({
variant,
controls,
displayText,
+ allowAttachments,
}: Pick<
AIConversationInput,
| 'elements'
@@ -35,6 +37,7 @@ export default function createProvider({
| 'variant'
| 'controls'
| 'displayText'
+ | 'allowAttachments'
>) {
return function Provider({
children,
@@ -57,25 +60,27 @@ export default function createProvider({
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
diff --git a/packages/react-ai/src/components/AIConversation/types.ts b/packages/react-ai/src/components/AIConversation/types.ts
index 296810be9f0..ba987b8dce1 100644
--- a/packages/react-ai/src/components/AIConversation/types.ts
+++ b/packages/react-ai/src/components/AIConversation/types.ts
@@ -31,6 +31,7 @@ export interface AIConversationInput {
responseComponents?: ResponseComponents;
variant?: MessageVariant;
controls?: ControlsContextProps;
+ allowAttachments?: boolean;
}
export interface AIConversationProps {
diff --git a/packages/react-ai/src/components/AIConversation/views/Controls/AttachFileControl.tsx b/packages/react-ai/src/components/AIConversation/views/Controls/AttachFileControl.tsx
index 40338cfc17f..2d6558176a5 100644
--- a/packages/react-ai/src/components/AIConversation/views/Controls/AttachFileControl.tsx
+++ b/packages/react-ai/src/components/AIConversation/views/Controls/AttachFileControl.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import { withBaseElementProps } from '@aws-amplify/ui-react-core/elements';
import { ConversationInputContext } from '../../context';
import { AIConversationElements } from '../../context/elements';
+import { useDropZone } from '@aws-amplify/ui-react-core';
const { Button, Icon, View } = AIConversationElements;
@@ -32,6 +33,17 @@ const AttachFileButton = withBaseElementProps(Button, {
export const AttachFileControl: AttachFileControl = () => {
const hiddenInput = React.useRef(null);
const { setInput } = React.useContext(ConversationInputContext);
+ const { dragState, ...dropHandlers } = useDropZone({
+ acceptedFileTypes: ['.jpeg'],
+ onDropComplete: ({ acceptedFiles }) => {
+ if (acceptedFiles && acceptedFiles?.length > 0 && setInput) {
+ setInput((prevInput) => ({
+ ...prevInput,
+ files: [...(prevInput?.files ?? []), ...acceptedFiles],
+ }));
+ }
+ },
+ });
function handleButtonClick() {
if (hiddenInput.current) {
@@ -53,7 +65,7 @@ export const AttachFileControl: AttachFileControl = () => {
}
return (
-
+
diff --git a/packages/react-ai/src/components/AIConversation/views/Controls/FieldControl.tsx b/packages/react-ai/src/components/AIConversation/views/Controls/FieldControl.tsx
index d44b1e0606c..cc9e012362c 100644
--- a/packages/react-ai/src/components/AIConversation/views/Controls/FieldControl.tsx
+++ b/packages/react-ai/src/components/AIConversation/views/Controls/FieldControl.tsx
@@ -15,6 +15,7 @@ import {
import { ControlsContext } from '../../context/ControlsContext';
import { getImageTypeFromMimeType } from '../../utils';
import { LoadingContext } from '../../context/LoadingContext';
+import { AttachmentContext } from '../../context/AttachmentContext';
const {
Button,
@@ -148,6 +149,7 @@ const InputContainer = withBaseElementProps(View, {
export const FieldControl: FieldControl = () => {
const { input, setInput } = React.useContext(ConversationInputContext);
const handleSendMessage = React.useContext(SendMessageContext);
+ const allowAttachments = React.useContext(AttachmentContext);
const ref = React.useRef(null);
const responseComponents = React.useContext(ResponseComponentsContext);
const controls = React.useContext(ControlsContext);
@@ -212,6 +214,7 @@ export const FieldControl: FieldControl = () => {
handleSubmit={handleSubmit}
input={input!}
setInput={setInput!}
+ allowAttachments={allowAttachments}
/>
);
}
@@ -223,7 +226,7 @@ export const FieldControl: FieldControl = () => {
method="post"
ref={ref}
>
-
+ {allowAttachments ? : null}
diff --git a/packages/react-ai/src/components/AIConversation/views/Controls/__tests__/FieldControl.spec.tsx b/packages/react-ai/src/components/AIConversation/views/Controls/__tests__/FieldControl.spec.tsx
index c149f4090db..c153227af3a 100644
--- a/packages/react-ai/src/components/AIConversation/views/Controls/__tests__/FieldControl.spec.tsx
+++ b/packages/react-ai/src/components/AIConversation/views/Controls/__tests__/FieldControl.spec.tsx
@@ -5,10 +5,15 @@ import { FieldControl } from '../FieldControl';
import { ConversationInputContextProvider } from '../../../context/ConversationInputContext';
import userEvent from '@testing-library/user-event';
import { SendMessageContextProvider } from '../../../context/SendMessageContext';
+import { AttachmentProvider } from '../../../context/AttachmentContext';
describe('FieldControl', () => {
it('renders a FieldControl component with the correct elements', () => {
- const result = render();
+ const result = render(
+
+
+
+ );
expect(result.container).toBeDefined();
const form = screen.findByRole('form');
@@ -23,7 +28,11 @@ describe('FieldControl', () => {
});
it('renders FieldControl with the correct accessibility roles', () => {
- render();
+ render(
+
+
+
+ );
const actionButtons = screen.getAllByRole('button');
const sendButton = actionButtons[1];
diff --git a/packages/react-ai/src/components/AIConversation/views/default/Form.tsx b/packages/react-ai/src/components/AIConversation/views/default/Form.tsx
index f2103328b44..4671041b562 100644
--- a/packages/react-ai/src/components/AIConversation/views/default/Form.tsx
+++ b/packages/react-ai/src/components/AIConversation/views/default/Form.tsx
@@ -11,15 +11,49 @@ import { ComponentClassName } from '@aws-amplify/ui';
import { ControlsContextProps } from '../../context/ControlsContext';
import { Attachments } from './Attachments';
import { LoadingContext } from '../../context/LoadingContext';
+import { ConversationInputContext } from '../../context';
function isHTMLFormElement(target: EventTarget): target is HTMLFormElement {
return 'form' in target;
}
+/**
+ * Will conditionally render the DropZone if allowAttachments
+ * is true
+ */
+const FormWrapper = ({
+ children,
+ allowAttachments,
+ setInput,
+}: {
+ children: React.ReactNode;
+ allowAttachments?: boolean;
+ setInput: ConversationInputContext['setInput'];
+}) => {
+ if (allowAttachments) {
+ return (
+ {
+ setInput?.((prevInput) => ({
+ ...prevInput,
+ files: [...(prevInput?.files ?? []), ...acceptedFiles],
+ }));
+ }}
+ >
+ {children}
+
+ );
+ } else {
+ return children;
+ }
+};
+
export const Form: NonNullable = ({
setInput,
input,
handleSubmit,
+ allowAttachments,
}) => {
const icons = useIcons('aiConversation');
const sendIcon = icons?.send ?? ;
@@ -29,51 +63,46 @@ export const Form: NonNullable = ({
const isInputEmpty = !input?.text?.length && !input?.files?.length;
return (
- {
- setInput((prevInput) => ({
- ...prevInput,
- files: [...(prevInput?.files ?? []), ...acceptedFiles],
- }));
- }}
- >
+
-
+ {allowAttachments ? (
+
+ ) : null}
+
= ({
-
+
);
};
diff --git a/packages/react-ai/src/components/AIConversation/views/default/__tests__/Form.spec.tsx b/packages/react-ai/src/components/AIConversation/views/default/__tests__/Form.spec.tsx
index 4d7edeff095..ed166875a8c 100644
--- a/packages/react-ai/src/components/AIConversation/views/default/__tests__/Form.spec.tsx
+++ b/packages/react-ai/src/components/AIConversation/views/default/__tests__/Form.spec.tsx
@@ -14,7 +14,12 @@ describe('Form', () => {
it('renders a Form component with the correct elements', () => {
const result = render(
-
+
);
expect(result.container).toBeDefined();
@@ -31,7 +36,12 @@ describe('Form', () => {
it('can upload files to the input', async () => {
const result = render(
-
+
);
expect(result.container).toBeDefined();
diff --git a/packages/react-core/src/__tests__/__snapshots__/index.spec.ts.snap b/packages/react-core/src/__tests__/__snapshots__/index.spec.ts.snap
index ee8b6a5ec4f..84b70c9f994 100644
--- a/packages/react-core/src/__tests__/__snapshots__/index.spec.ts.snap
+++ b/packages/react-core/src/__tests__/__snapshots__/index.spec.ts.snap
@@ -13,6 +13,7 @@ exports[`@aws-amplify/ui-react-core exports should match snapshot 1`] = `
"useAuthenticatorRoute",
"useDataState",
"useDeprecationWarning",
+ "useDropZone",
"useField",
"useForm",
"useGetUrl",
diff --git a/packages/react/src/primitives/DropZone/__tests__/useDropZone.test.ts b/packages/react-core/src/hooks/__tests__/useDropZone.test.ts
similarity index 96%
rename from packages/react/src/primitives/DropZone/__tests__/useDropZone.test.ts
rename to packages/react-core/src/hooks/__tests__/useDropZone.test.ts
index 7c63d5df52d..2a0c1996e2a 100644
--- a/packages/react/src/primitives/DropZone/__tests__/useDropZone.test.ts
+++ b/packages/react-core/src/hooks/__tests__/useDropZone.test.ts
@@ -1,11 +1,11 @@
import * as React from 'react';
import { act, renderHook } from '@testing-library/react-hooks';
-import { useDropZone } from '../useDropZone';
-import { UseDropZoneProps } from '../types';
+import useDropZone from '../useDropZone';
+import type { UseDropZoneParams } from '../useDropZone';
const mockFile = new File(['hello'], 'hello.png', { type: 'image/png' });
const mockOnDropComplete = jest.fn();
-const dropZoneProps: UseDropZoneProps = {
+const dropZoneProps: UseDropZoneParams = {
onDropComplete: mockOnDropComplete,
};
type DragEvent = React.DragEvent;
diff --git a/packages/react-core/src/hooks/index.ts b/packages/react-core/src/hooks/index.ts
index a096997803f..b941809a63e 100644
--- a/packages/react-core/src/hooks/index.ts
+++ b/packages/react-core/src/hooks/index.ts
@@ -9,3 +9,4 @@ export { default as useHasValueUpdated } from './useHasValueUpdated';
export { default as usePreviousValue } from './usePreviousValue';
export { default as useSetUserAgent } from './useSetUserAgent';
export { default as useTimeout } from './useTimeout';
+export { default as useDropZone, UseDropZoneParams } from './useDropZone';
diff --git a/packages/react/src/primitives/DropZone/useDropZone.ts b/packages/react-core/src/hooks/useDropZone.ts
similarity index 67%
rename from packages/react/src/primitives/DropZone/useDropZone.ts
rename to packages/react-core/src/hooks/useDropZone.ts
index 377b67095db..06754c991f7 100644
--- a/packages/react/src/primitives/DropZone/useDropZone.ts
+++ b/packages/react-core/src/hooks/useDropZone.ts
@@ -1,9 +1,34 @@
import { useState } from 'react';
-import { UseDropZoneProps, UseDropZoneReturn, DragState } from './types';
import { isFunction } from '@aws-amplify/ui';
-import { filterAllowedFiles } from './filterAllowedFiles';
+import { filterAllowedFiles } from '../utils/filterAllowedFiles';
-export function useDropZone({
+interface DragEvents {
+ onDragStart: (event: React.DragEvent) => void;
+ onDragEnter: (event: React.DragEvent) => void;
+ onDragLeave: (event: React.DragEvent) => void;
+ onDragOver: (event: React.DragEvent) => void;
+ onDrop: (event: React.DragEvent) => void;
+}
+
+export interface UseDropZoneParams extends Partial {
+ onDropComplete?: (props: {
+ acceptedFiles: File[];
+ rejectedFiles: File[];
+ }) => void;
+ /**
+ * List of accepted File types, values of `['*']` or undefined allow any files
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
+ */
+ acceptedFileTypes?: string[];
+}
+
+type DragState = 'accept' | 'reject' | 'inactive';
+
+interface UseDropZoneReturn extends DragEvents {
+ dragState: DragState;
+}
+
+export default function useDropZone({
onDropComplete,
onDragEnter: _onDragEnter,
onDragLeave: _onDragLeave,
@@ -11,7 +36,7 @@ export function useDropZone({
onDragStart: _onDragStart,
onDrop: _onDrop,
acceptedFileTypes = [],
-}: UseDropZoneProps): UseDropZoneReturn {
+}: UseDropZoneParams): UseDropZoneReturn {
const [dragState, setDragState] = useState('inactive');
const onDragStart = (event: React.DragEvent) => {
diff --git a/packages/react-core/src/index.ts b/packages/react-core/src/index.ts
index b05c7c453e1..c445ae0c8a3 100644
--- a/packages/react-core/src/index.ts
+++ b/packages/react-core/src/index.ts
@@ -44,6 +44,8 @@ export {
useTimeout,
useDataState,
DataState,
+ useDropZone,
+ UseDropZoneParams,
} from './hooks';
export { MergeProps } from './types';
diff --git a/packages/react/src/primitives/DropZone/__tests__/filterAllowedFiles.test.ts b/packages/react-core/src/utils/__tests__/filterAllowedFiles.test.ts
similarity index 100%
rename from packages/react/src/primitives/DropZone/__tests__/filterAllowedFiles.test.ts
rename to packages/react-core/src/utils/__tests__/filterAllowedFiles.test.ts
diff --git a/packages/react/src/primitives/DropZone/filterAllowedFiles.ts b/packages/react-core/src/utils/filterAllowedFiles.ts
similarity index 100%
rename from packages/react/src/primitives/DropZone/filterAllowedFiles.ts
rename to packages/react-core/src/utils/filterAllowedFiles.ts
diff --git a/packages/react-storage/src/components/FileUploader/FileUploader.tsx b/packages/react-storage/src/components/FileUploader/FileUploader.tsx
index 3ce0def4ac2..755e551891a 100644
--- a/packages/react-storage/src/components/FileUploader/FileUploader.tsx
+++ b/packages/react-storage/src/components/FileUploader/FileUploader.tsx
@@ -6,7 +6,7 @@ import {
useDeprecationWarning,
useSetUserAgent,
} from '@aws-amplify/ui-react-core';
-import { useDropZone } from '@aws-amplify/ui-react/internal';
+import { useDropZone } from '@aws-amplify/ui-react-core';
import { useFileUploader, useUploadFiles } from './hooks';
import {
diff --git a/packages/react-storage/src/components/StorageManager/StorageManager.tsx b/packages/react-storage/src/components/StorageManager/StorageManager.tsx
index 13373e1402c..aacbc785759 100644
--- a/packages/react-storage/src/components/StorageManager/StorageManager.tsx
+++ b/packages/react-storage/src/components/StorageManager/StorageManager.tsx
@@ -6,7 +6,7 @@ import {
useDeprecationWarning,
useSetUserAgent,
} from '@aws-amplify/ui-react-core';
-import { useDropZone } from '@aws-amplify/ui-react/internal';
+import { useDropZone } from '@aws-amplify/ui-react-core';
import { useFileUploader, useUploadFiles } from '../FileUploader/hooks';
import { FileStatus } from '../FileUploader/types';
diff --git a/packages/react/src/internal.ts b/packages/react/src/internal.ts
index f4fd60bdbe4..7e24ef37d0e 100644
--- a/packages/react/src/internal.ts
+++ b/packages/react/src/internal.ts
@@ -3,11 +3,11 @@ export * from './hooks/useStorageURL';
export * from './hooks/useThemeBreakpoint';
export { useDeprecationWarning } from './hooks/useDeprecationWarning';
export { useColorMode } from './hooks/useTheme';
+export { useDropZone } from '@aws-amplify/ui-react-core';
export * from './components/FilterChildren';
export { AlertIcon } from './primitives/Alert/AlertIcon';
export * from './primitives/Icon/internal';
-export { useDropZone } from './primitives/DropZone/useDropZone';
export { Field } from './primitives/Field';
diff --git a/packages/react/src/primitives/DropZone/DropZone.tsx b/packages/react/src/primitives/DropZone/DropZone.tsx
index 24cfb288027..2d15d3ce684 100644
--- a/packages/react/src/primitives/DropZone/DropZone.tsx
+++ b/packages/react/src/primitives/DropZone/DropZone.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
-import { useDropZone } from './useDropZone';
+import { useDropZone } from '@aws-amplify/ui-react-core';
import { ForwardRefPrimitive, Primitive } from '../types';
import { DropZoneProps, BaseDropZoneProps } from './types';
import { DropZoneProvider } from './DropZoneProvider';
diff --git a/packages/react/src/primitives/DropZone/DropZoneProvider.tsx b/packages/react/src/primitives/DropZone/DropZoneProvider.tsx
index a322772fd01..e1b2992dfa5 100644
--- a/packages/react/src/primitives/DropZone/DropZoneProvider.tsx
+++ b/packages/react/src/primitives/DropZone/DropZoneProvider.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
-import { DragState } from './types';
+
+type DragState = 'accept' | 'reject' | 'inactive';
const DropZoneContext = React.createContext('inactive');
diff --git a/packages/react/src/primitives/DropZone/types.ts b/packages/react/src/primitives/DropZone/types.ts
index f257c9bcbb3..4a5d8ee39c0 100644
--- a/packages/react/src/primitives/DropZone/types.ts
+++ b/packages/react/src/primitives/DropZone/types.ts
@@ -1,21 +1,8 @@
import * as React from 'react';
import { ElementType, PrimitiveProps, BaseViewProps } from '../types';
+import { UseDropZoneParams } from '@aws-amplify/ui-react-core';
-interface DropProps {
- acceptedFiles: File[];
- rejectedFiles: File[];
-}
-
-export interface UseDropZoneProps extends Partial {
- onDropComplete?: (props: DropProps) => void;
- /**
- * List of accepted File types, values of `['*']` or undefined allow any files
- * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
- */
- acceptedFileTypes?: string[];
-}
-
-export interface BaseDropZoneProps extends BaseViewProps, UseDropZoneProps {}
+export interface BaseDropZoneProps extends BaseViewProps, UseDropZoneParams {}
export type DropZoneProps = PrimitiveProps<
BaseDropZoneProps,
@@ -30,12 +17,6 @@ interface DragEvents {
onDrop: (event: React.DragEvent) => void;
}
-export type DragState = 'accept' | 'reject' | 'inactive';
-
-export interface UseDropZoneReturn extends DragEvents {
- dragState: DragState;
-}
-
export interface BaseDropZoneContainerProps extends BaseViewProps, DragEvents {}
export type DropZoneContainerProps =