Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ai-conversation): add allowAttachments prop #5802

Merged
merged 10 commits into from
Sep 30, 2024
13 changes: 13 additions & 0 deletions .changeset/seven-bats-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@aws-amplify/ui-react-ai": minor
---

feat(ai-conversation): add allowAttachments prop

```jsx
<AIConversation
allowAttachments
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention that to keep current behavior customers need to add this prop

messages={messages}
handleSendMessage={handleSendMessage}
/>
```
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function Chat() {
messages={messages}
handleSendMessage={sendMessage}
isLoading={isLoading}
allowAttachments
suggestedPrompts={[
{
inputText: 'hello',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function AIConversationBase({
variant,
isLoading,
displayText,
allowAttachments,
}: AIConversationBaseProps): JSX.Element {
const icons = useIcons('aiConversation');
const defaultAvatars: Avatars = {
Expand Down Expand Up @@ -72,6 +73,7 @@ function AIConversationBase({
...avatars,
},
isLoading,
allowAttachments,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add this on the createAIConversation as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this comment resolved?

};

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<AttachmentContext.Provider value={allowAttachments ?? false}>
{children}
</AttachmentContext.Provider>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface ControlsContextProps {
Form?: React.ComponentType<
{
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
allowAttachments?: boolean;
} & Required<ConversationInputContext>
>;
MessageList?: React.ComponentType<{ messages: ConversationMessage[] }>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function createAIConversation(input: AIConversationInput = {}): {
variant,
controls,
displayText,
allowAttachments,
} = input;

const Provider = createProvider({
Expand All @@ -40,6 +41,7 @@ export function createAIConversation(input: AIConversationInput = {}): {
variant,
controls,
displayText,
allowAttachments,
});

function AIConversation(props: AIConversationProps): JSX.Element {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ResponseComponentsProvider,
SendMessageContextProvider,
} from './context';
import { AttachmentProvider } from './context/AttachmentContext';

export default function createProvider({
elements,
Expand All @@ -26,6 +27,7 @@ export default function createProvider({
variant,
controls,
displayText,
allowAttachments,
}: Pick<
AIConversationInput,
| 'elements'
Expand All @@ -35,6 +37,7 @@ export default function createProvider({
| 'variant'
| 'controls'
| 'displayText'
| 'allowAttachments'
>) {
return function Provider({
children,
Expand All @@ -57,25 +60,27 @@ export default function createProvider({
<ControlsProvider controls={controls}>
<SuggestedPromptProvider suggestedPrompts={suggestedPrompts}>
<ResponseComponentsProvider responseComponents={responseComponents}>
<ConversationDisplayTextProvider {..._displayText}>
<ConversationInputContextProvider>
<SendMessageContextProvider
handleSendMessage={handleSendMessage}
>
<AvatarsProvider avatars={avatars}>
<ActionsProvider actions={actions}>
<MessageVariantProvider variant={variant}>
<MessagesProvider messages={messages}>
<LoadingContextProvider isLoading={isLoading}>
{children}
</LoadingContextProvider>
</MessagesProvider>
</MessageVariantProvider>
</ActionsProvider>
</AvatarsProvider>
</SendMessageContextProvider>
</ConversationInputContextProvider>
</ConversationDisplayTextProvider>
<AttachmentProvider allowAttachments={allowAttachments}>
<ConversationDisplayTextProvider {..._displayText}>
<ConversationInputContextProvider>
<SendMessageContextProvider
handleSendMessage={handleSendMessage}
>
<AvatarsProvider avatars={avatars}>
<ActionsProvider actions={actions}>
<MessageVariantProvider variant={variant}>
<MessagesProvider messages={messages}>
<LoadingContextProvider isLoading={isLoading}>
{children}
</LoadingContextProvider>
</MessagesProvider>
</MessageVariantProvider>
</ActionsProvider>
</AvatarsProvider>
</SendMessageContextProvider>
</ConversationInputContextProvider>
</ConversationDisplayTextProvider>
</AttachmentProvider>
</ResponseComponentsProvider>
</SuggestedPromptProvider>
</ControlsProvider>
Expand Down
1 change: 1 addition & 0 deletions packages/react-ai/src/components/AIConversation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface AIConversationInput {
responseComponents?: ResponseComponents;
variant?: MessageVariant;
controls?: ControlsContextProps;
allowAttachments?: boolean;
}

export interface AIConversationProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<HTMLFormElement | null>(null);
const responseComponents = React.useContext(ResponseComponentsContext);
const controls = React.useContext(ControlsContext);
Expand Down Expand Up @@ -212,6 +214,7 @@ export const FieldControl: FieldControl = () => {
handleSubmit={handleSubmit}
input={input!}
setInput={setInput!}
allowAttachments={allowAttachments}
/>
);
}
Expand All @@ -223,7 +226,7 @@ export const FieldControl: FieldControl = () => {
method="post"
ref={ref}
>
<AttachFileControl />
{allowAttachments ? <AttachFileControl /> : null}
<InputContainer>
<VisuallyHidden>
<Label />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<FieldControl />);
const result = render(
<AttachmentProvider allowAttachments>
<FieldControl />
</AttachmentProvider>
);
expect(result.container).toBeDefined();

const form = screen.findByRole('form');
Expand All @@ -23,7 +28,11 @@ describe('FieldControl', () => {
});

it('renders FieldControl with the correct accessibility roles', () => {
render(<FieldControl />);
render(
<AttachmentProvider allowAttachments>
<FieldControl />
</AttachmentProvider>
);

const actionButtons = screen.getAllByRole('button');
const sendButton = actionButtons[1];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit confused on whether this addition is creating deltas between the behaviors of the default AIConversation and the Amplify UI version?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so the Amplify UI version uses the DropZone component that wraps the form so users can drag and drop a file onto the form and it gets attached (in addition to a button which triggers the file picker). So allowAttachments will affect bot the default component and the Amplify UI one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have drop event handlers configured in the default AiConversation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do now

children,
allowAttachments,
setInput,
}: {
children: React.ReactNode;
allowAttachments?: boolean;
setInput: ConversationInputContext['setInput'];
}) => {
if (allowAttachments) {
return (
<DropZone
className={ComponentClassName.AIConversationFormDropzone}
onDropComplete={({ acceptedFiles }) => {
setInput?.((prevInput) => ({
...prevInput,
files: [...(prevInput?.files ?? []), ...acceptedFiles],
}));
}}
>
{children}
</DropZone>
);
} else {
return children;
}
};

export const Form: NonNullable<ControlsContextProps['Form']> = ({
setInput,
input,
handleSubmit,
allowAttachments,
}) => {
const icons = useIcons('aiConversation');
const sendIcon = icons?.send ?? <IconSend />;
Expand All @@ -29,51 +63,46 @@ export const Form: NonNullable<ControlsContextProps['Form']> = ({
const isInputEmpty = !input?.text?.length && !input?.files?.length;

return (
<DropZone
className={ComponentClassName.AIConversationFormDropzone}
onDropComplete={({ acceptedFiles }) => {
setInput((prevInput) => ({
...prevInput,
files: [...(prevInput?.files ?? []), ...acceptedFiles],
}));
}}
>
<FormWrapper allowAttachments={allowAttachments} setInput={setInput}>
<View
as="form"
className={ComponentClassName.AIConversationForm}
onSubmit={handleSubmit}
>
<Button
className={ComponentClassName.AIConversationFormAttach}
onClick={() => {
hiddenInput?.current?.click();
if (hiddenInput?.current) {
hiddenInput.current.value = '';
}
}}
>
<span>{attachIcon}</span>
<VisuallyHidden>
<input
type="file"
tabIndex={-1}
ref={hiddenInput}
onChange={(e) => {
const { files } = e.target;
if (!files || files.length === 0) {
return;
}
setInput((prevValue) => ({
...prevValue,
files: [...(prevValue?.files ?? []), ...Array.from(files)],
}));
}}
multiple
accept="*"
data-testid="hidden-file-input"
/>
</VisuallyHidden>
</Button>
{allowAttachments ? (
<Button
className={ComponentClassName.AIConversationFormAttach}
onClick={() => {
hiddenInput?.current?.click();
if (hiddenInput?.current) {
hiddenInput.current.value = '';
}
}}
>
<span>{attachIcon}</span>
<VisuallyHidden>
<input
type="file"
tabIndex={-1}
ref={hiddenInput}
onChange={(e) => {
const { files } = e.target;
if (!files || files.length === 0) {
return;
}
setInput((prevValue) => ({
...prevValue,
files: [...(prevValue?.files ?? []), ...Array.from(files)],
}));
}}
multiple
accept="*"
data-testid="hidden-file-input"
/>
</VisuallyHidden>
</Button>
) : null}

<TextAreaField
className={ComponentClassName.AIConversationFormField}
label="input"
Expand Down Expand Up @@ -111,6 +140,6 @@ export const Form: NonNullable<ControlsContextProps['Form']> = ({
</Button>
</View>
<Attachments setInput={setInput} files={input?.files} />
</DropZone>
</FormWrapper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ describe('Form', () => {

it('renders a Form component with the correct elements', () => {
const result = render(
<Form setInput={setInput} input={input} handleSubmit={handleSubmit} />
<Form
allowAttachments
setInput={setInput}
input={input}
handleSubmit={handleSubmit}
/>
);
expect(result.container).toBeDefined();

Expand All @@ -31,7 +36,12 @@ describe('Form', () => {

it('can upload files to the input', async () => {
const result = render(
<Form setInput={setInput} input={input} handleSubmit={handleSubmit} />
<Form
allowAttachments
setInput={setInput}
input={input}
handleSubmit={handleSubmit}
/>
);
expect(result.container).toBeDefined();

Expand Down
Loading