diff --git a/.changeset/honest-beans-suffer.md b/.changeset/honest-beans-suffer.md
new file mode 100644
index 00000000000..08d934dfd6b
--- /dev/null
+++ b/.changeset/honest-beans-suffer.md
@@ -0,0 +1,16 @@
+---
+"@aws-amplify/ui-react-ai": minor
+"@aws-amplify/ui": patch
+---
+
+feat(ai) add attachment validations
+
+The current limitations on the Amplify AI kit for attachments is 400kb (of base64'd size) per image, and 20 images per message are now being enforced before the message is sent.
+These limits can be adjusted via props as well.
+
+```tsx
+
+```
diff --git a/docs/src/components/ComponentsMetadata.ts b/docs/src/components/ComponentsMetadata.ts
index 7948e9c9b07..6ab6b509cb0 100644
--- a/docs/src/components/ComponentsMetadata.ts
+++ b/docs/src/components/ComponentsMetadata.ts
@@ -135,6 +135,11 @@ export const ComponentsMetadata: ComponentClassNameItems = {
components: ['AIConversation'],
description: 'Class applied to the form element',
},
+ AIConversationFormError: {
+ className: ComponentClassName.AIConversationFormError,
+ components: ['AIConversation'],
+ description: 'Class applied to the error message of the form',
+ },
AIConversationFormAttach: {
className: ComponentClassName.AIConversationFormAttach,
components: ['AIConversation'],
diff --git a/examples/next/pages/ui/components/ai/ai-conversation/attachments.page.tsx b/examples/next/pages/ui/components/ai/ai-conversation/attachments.page.tsx
new file mode 100644
index 00000000000..51d7857ffb1
--- /dev/null
+++ b/examples/next/pages/ui/components/ai/ai-conversation/attachments.page.tsx
@@ -0,0 +1,55 @@
+import * as React from 'react';
+import { Amplify } from 'aws-amplify';
+import { signOut } from 'aws-amplify/auth';
+import { createAIHooks, AIConversation } from '@aws-amplify/ui-react-ai';
+import { generateClient } from 'aws-amplify/api';
+import '@aws-amplify/ui-react/styles.css';
+
+import outputs from './amplify_outputs';
+import type { Schema } from '@environments/ai/gen2/amplify/data/resource';
+import { Authenticator, Button, Card, Flex } from '@aws-amplify/ui-react';
+
+const client = generateClient({ authMode: 'userPool' });
+const { useAIConversation } = createAIHooks(client);
+
+Amplify.configure(outputs);
+
+function Chat() {
+ const [
+ {
+ data: { messages },
+ isLoading,
+ },
+ sendMessage,
+ ] = useAIConversation('pirateChat');
+
+ return (
+
+ );
+}
+
+export default function Example() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/examples/next/pages/ui/components/storage/storage-browser/composable-playground/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/composable-playground/index.page.tsx
index 66db5fb3850..f1ca05f4d87 100644
--- a/examples/next/pages/ui/components/storage/storage-browser/composable-playground/index.page.tsx
+++ b/examples/next/pages/ui/components/storage/storage-browser/composable-playground/index.page.tsx
@@ -11,7 +11,6 @@ import { Auth } from '../managedAuthAdapter';
import { Button, Flex, Breadcrumbs } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react-storage/styles.css';
-import '@aws-amplify/ui-react-storage/storage-browser-styles.css';
const components: CreateStorageBrowserInput['components'] = {
Navigation: ({ items }) => (
diff --git a/examples/next/pages/ui/components/storage/storage-browser/custom-actions/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/custom-actions/index.page.tsx
new file mode 100644
index 00000000000..f5eccbf88b1
--- /dev/null
+++ b/examples/next/pages/ui/components/storage/storage-browser/custom-actions/index.page.tsx
@@ -0,0 +1,129 @@
+import React from 'react';
+
+import { createStorageBrowser } from '@aws-amplify/ui-react-storage/browser';
+
+import { Flex } from '@aws-amplify/ui-react';
+
+import '@aws-amplify/ui-react-storage/styles.css';
+
+const { StorageBrowser } = createStorageBrowser({
+ actions: {
+ default: {
+ copy: {
+ actionListItem: {
+ icon: 'copy-file',
+ label: 'Override Copy',
+ },
+ handler: ({ data }) => {
+ const { key } = data;
+ return {
+ result: Promise.resolve({ status: 'COMPLETE', value: { key } }),
+ };
+ },
+ viewName: 'CopyView',
+ },
+ createFolder: {
+ actionListItem: {
+ icon: 'create-folder',
+ label: 'Override Create Folder',
+ },
+ handler: ({ data }) => {
+ const { key } = data;
+ return {
+ result: Promise.resolve({ status: 'COMPLETE', value: { key } }),
+ };
+ },
+ viewName: 'CreateFolderView',
+ },
+ delete: {
+ actionListItem: {
+ icon: 'delete-file',
+ label: 'Override Delete',
+ },
+ handler: ({ data }) => {
+ const { key } = data;
+ return {
+ result: Promise.resolve({ status: 'COMPLETE', value: { key } }),
+ };
+ },
+ viewName: 'DeleteView',
+ },
+ download: () => {
+ return {
+ result: Promise.resolve({
+ status: 'COMPLETE',
+ value: { url: new URL('') },
+ }),
+ };
+ },
+ upload: {
+ actionListItem: {
+ icon: 'upload-file',
+ label: 'Override Upload',
+ },
+ handler: ({ data }) => {
+ const { key } = data;
+ return {
+ result: Promise.resolve({ status: 'COMPLETE', value: { key } }),
+ };
+ },
+ viewName: 'UploadView',
+ },
+ listLocationItems: () =>
+ Promise.resolve({
+ items: [
+ {
+ id: 'jaskjkaska',
+ key: 'item-key',
+ lastModified: new Date(),
+ size: 1008,
+ type: 'FILE' as const,
+ },
+ ],
+ nextToken: undefined,
+ }),
+ },
+ },
+ config: {
+ getLocationCredentials: () =>
+ Promise.resolve({
+ credentials: {
+ accessKeyId: '',
+ expiration: new Date(),
+ secretAccessKey: '',
+ sessionToken: '',
+ },
+ }),
+ region: '',
+ registerAuthListener: () => null,
+ listLocations: () =>
+ Promise.resolve({
+ items: [
+ {
+ bucket: 'my-bucket',
+ id: crypto.randomUUID(),
+ permissions: ['delete', 'get', 'list', 'write'],
+ prefix: 'my-prefix',
+ type: 'PREFIX',
+ },
+ ],
+ nextToken: undefined,
+ }),
+ },
+});
+
+function Example() {
+ return (
+
+
+
+ );
+}
+
+export default Example;
diff --git a/examples/next/pages/ui/components/storage/storage-browser/default-auth/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/default-auth/index.page.tsx
index 494fc67d94f..2af0d66bc65 100644
--- a/examples/next/pages/ui/components/storage/storage-browser/default-auth/index.page.tsx
+++ b/examples/next/pages/ui/components/storage/storage-browser/default-auth/index.page.tsx
@@ -12,7 +12,6 @@ import {
import { StorageBrowser } from '@aws-amplify/ui-react-storage';
import '@aws-amplify/ui-react-storage/styles.css';
-import '@aws-amplify/ui-react-storage/storage-browser-styles.css';
import config from './aws-exports';
diff --git a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/StorageBrowser.ts b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/StorageBrowser.ts
index de650f99e9c..30eb2ac6093 100644
--- a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/StorageBrowser.ts
+++ b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/StorageBrowser.ts
@@ -4,8 +4,6 @@ import {
createAmplifyAuthAdapter,
createStorageBrowser,
} from '@aws-amplify/ui-react-storage/browser';
-import '@aws-amplify/ui-react-storage/styles.css';
-import '@aws-amplify/ui-react-storage/storage-browser-styles.css';
import config from './aws-exports';
diff --git a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/[location-detail]/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/[location-detail]/index.page.tsx
index be01ddb40ca..32ab12f4431 100644
--- a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/[location-detail]/index.page.tsx
+++ b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/[location-detail]/index.page.tsx
@@ -6,7 +6,6 @@ import { Button, Flex } from '@aws-amplify/ui-react';
import { StorageBrowser } from '../../StorageBrowser';
-import '@aws-amplify/ui-react-storage/storage-browser-styles.css';
import '@aws-amplify/ui-react-storage/styles.css';
export default function Page() {
diff --git a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/index.page.tsx
index b50fd485c02..8d3093dd95c 100644
--- a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/index.page.tsx
+++ b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/index.page.tsx
@@ -6,7 +6,6 @@ import { Button, Flex } from '@aws-amplify/ui-react';
import { StorageBrowser } from '../StorageBrowser';
-import '@aws-amplify/ui-react-storage/storage-browser-styles.css';
import '@aws-amplify/ui-react-storage/styles.css';
function Locations() {
diff --git a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/index.page.tsx
index 9cfc40de58b..d298cb47bc0 100644
--- a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/index.page.tsx
+++ b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/index.page.tsx
@@ -6,7 +6,6 @@ import useIsSignedIn from './useIsSignedIn';
import { Authenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react-storage/styles.css';
-import '@aws-amplify/ui-react-storage/storage-browser-styles.css';
function Example() {
const router = useRouter();
diff --git a/examples/next/pages/ui/components/storage/storage-browser/managed-auth/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/index.page.tsx
index 18f2c1f20e9..a718060b9bb 100644
--- a/examples/next/pages/ui/components/storage/storage-browser/managed-auth/index.page.tsx
+++ b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/index.page.tsx
@@ -1,19 +1,104 @@
import React from 'react';
+import { getUrl } from '@aws-amplify/storage/internals';
-import { createStorageBrowser } from '@aws-amplify/ui-react-storage/browser';
+import {
+ ActionViewConfig,
+ ActionHandler,
+ createStorageBrowser,
+} from '@aws-amplify/ui-react-storage/browser';
import { managedAuthAdapter } from '../managedAuthAdapter';
import { SignIn, SignOutButton } from './routed/components';
-
-import { Flex, View } from '@aws-amplify/ui-react';
+import {
+ Button,
+ Flex,
+ Link,
+ StepperField,
+ Text,
+ View,
+} from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react-storage/styles.css';
-import '@aws-amplify/ui-react-storage/storage-browser-styles.css';
-const { StorageBrowser } = createStorageBrowser({
+type GetLink = ActionHandler<{ duration: number; fileKey: string }, string>;
+
+const getLink: GetLink = ({ data, config }) => {
+ const result = getUrl({
+ path: data.key,
+ options: {
+ bucket: { bucketName: config.bucket, region: config.region },
+ locationCredentialsProvider: config.credentials,
+ expiresIn: data.duration * 60,
+ validateObjectExistence: true,
+ },
+ }).then((res) => ({
+ status: 'COMPLETE' as const,
+ value: res.url.toString(),
+ }));
+
+ return { result };
+};
+
+const generateLink: ActionViewConfig = {
+ handler: getLink,
+ viewName: 'LinkActionView',
+ actionListItem: {
+ icon: 'download',
+ label: 'Generate Download Links',
+ disable: (selected) => !selected?.length,
+ },
+};
+
+const { StorageBrowser, useAction, useView } = createStorageBrowser({
+ actions: { custom: { generateLink } },
config: managedAuthAdapter,
});
+const LinkActionView = () => {
+ const [duration, setDuration] = React.useState(60);
+
+ const locationDetailState = useView('LocationDetail');
+ const { onActionExit, fileDataItems } = locationDetailState;
+
+ const items = React.useMemo(
+ () =>
+ !fileDataItems
+ ? []
+ : fileDataItems.map((item) => ({ ...item, duration })),
+ [fileDataItems, duration]
+ );
+
+ const [{ tasks }, handleCreate] = useAction('generateLink', { items });
+
+ return (
+
+
+ {
+ setDuration(value);
+ }}
+ />
+
+ {!tasks
+ ? null
+ : tasks.map(({ data, status, value }) => {
+ return (
+
+ {data.fileKey}
+ {value ? link : null}
+ {status}
+
+ );
+ })}
+
+ );
+};
+
function Example() {
const [showSignIn, setShowSignIn] = React.useState(false);
@@ -29,9 +114,8 @@ function Example() {
>
setShowSignIn(false)} />
-
+
+
);
diff --git a/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/[locations]/[location-detail]/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/[locations]/[location-detail]/index.page.tsx
index f97c16a4e6c..4ddefd7cadc 100644
--- a/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/[locations]/[location-detail]/index.page.tsx
+++ b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/[locations]/[location-detail]/index.page.tsx
@@ -6,7 +6,6 @@ import { SignOutButton } from '../../components';
import { StorageBrowser } from '../../StorageBrowser';
import '@aws-amplify/ui-react-storage/styles.css';
-import '@aws-amplify/ui-react-storage/storage-browser-styles.css';
export default function Page() {
const { back, query, pathname, replace } = useRouter();
@@ -50,7 +49,10 @@ export default function Page() {
}}
/>
{typeof query.actionType === 'string' ? (
-