Skip to content

Commit 58d7b1b

Browse files
dbanksdesignjoon-woncalebpollman
authored
feat(ai): adding document support (#6389)
Co-authored-by: JoonWon Choi <[email protected]> Co-authored-by: Caleb Pollman <[email protected]>
1 parent 1c34fbb commit 58d7b1b

File tree

26 files changed

+529
-83
lines changed

26 files changed

+529
-83
lines changed

.changeset/quick-crabs-camp.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@aws-amplify/ui-react-ai": minor
3+
"@aws-amplify/ui-react": minor
4+
---
5+
6+
feat(ai): adding document support
7+
8+
The AIConversation component now accepts documents as attachments. The document file types are the [ones Bedrock supports](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_DocumentBlock.html). This also fixes a bug where a user can submit empty messages in succession by pressing enter rapidly.

.github/workflows/reusable-unit.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ jobs:
146146
- name: Check ${{ matrix.package }} bundle size
147147
if: |
148148
matrix.package == 'react' ||
149+
matrix.package == 'react-ai' ||
149150
matrix.package == 'react-auth' ||
150151
matrix.package == 'react-geo' ||
151152
matrix.package == 'react-liveness' ||

examples/next/pages/ui/components/ai/ai-conversation/attachments.page.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,30 @@ function Chat() {
1919
{
2020
data: { messages },
2121
isLoading,
22+
hasError,
23+
messages: errorMessages,
2224
},
2325
sendMessage,
2426
] = useAIConversation('pirateChat');
2527

28+
if (hasError) {
29+
return (
30+
<div>
31+
<h2>Error</h2>
32+
<ul>
33+
{errorMessages.map((message, i) => (
34+
<li key={i}>{message.message}</li>
35+
))}
36+
</ul>
37+
</div>
38+
);
39+
}
2640
return (
2741
<AIConversation
2842
messages={messages}
2943
isLoading={isLoading}
3044
handleSendMessage={sendMessage}
3145
allowAttachments
32-
maxAttachmentSize={100_000}
33-
maxAttachments={2}
3446
/>
3547
);
3648
}

examples/next/pages/ui/components/ai/constants.tsx

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Icon } from '@aws-amplify/ui-react';
22
import { convertBufferToBase64 } from './utils';
3+
import { ConversationMessage } from '@aws-amplify/ui-react-ai';
34

45
export const PROMPTS = [
56
{
@@ -39,7 +40,7 @@ export const ACTIONS = [
3940
},
4041
];
4142

42-
export const INITIAL_MESSAGES = [
43+
export const INITIAL_MESSAGES: ConversationMessage[] = [
4344
{
4445
conversationId: 'foobar',
4546
id: '1',
@@ -54,6 +55,96 @@ export const INITIAL_MESSAGES = [
5455
{
5556
text: 'I have a really long question. This is a long message This is a long message This is a long message This is a long message This is a long message',
5657
},
58+
{
59+
document: {
60+
name: 'some_document',
61+
format: 'doc',
62+
source: {
63+
bytes: new Uint8Array([
64+
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0,
65+
0, 0, 28, 0, 0, 0, 28, 8, 3, 0, 0, 0, 69, 211, 47, 166, 0, 0, 0,
66+
168, 80, 76, 84, 69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64,
67+
0, 51, 51, 26, 26, 26, 23, 23, 46, 20, 20, 39, 18, 18, 36, 12, 24,
68+
36, 9, 26, 35, 15, 23, 38, 15, 29, 36, 12, 23, 41, 11, 28, 40, 13,
69+
26, 38, 12, 27, 39, 15, 26, 36, 14, 25, 39, 14, 24, 38, 13, 26,
70+
36, 13, 27, 38, 12, 27, 39, 12, 27, 37, 12, 27, 37, 12, 27, 39,
71+
12, 26, 38, 14, 26, 38, 13, 26, 38, 12, 26, 39, 13, 27, 38, 13,
72+
27, 38, 12, 26, 39, 14, 26, 38, 13, 26, 38, 13, 26, 38, 13, 26,
73+
38, 13, 26, 38, 13, 26, 38, 13, 26, 38, 13, 26, 38, 13, 27, 38,
74+
12, 26, 39, 14, 26, 38, 13, 26, 38, 13, 26, 38, 13, 26, 38, 12,
75+
26, 39, 13, 26, 38, 13, 26, 38, 13, 26, 38, 13, 26, 38, 13, 26,
76+
38, 13, 26, 38, 255, 255, 255, 26, 7, 228, 96, 0, 0, 0, 54, 116,
77+
82, 78, 83, 0, 1, 2, 3, 4, 5, 10, 11, 13, 14, 21, 29, 34, 35, 44,
78+
45, 60, 65, 70, 72, 73, 77, 95, 105, 123, 124, 125, 126, 128, 135,
79+
145, 153, 162, 165, 170, 174, 175, 176, 179, 209, 215, 220, 221,
80+
225, 226, 228, 229, 243, 245, 247, 249, 250, 252, 253, 138, 189,
81+
133, 44, 0, 0, 0, 1, 98, 75, 71, 68, 55, 48, 184, 184, 71, 0, 0,
82+
0, 214, 73, 68, 65, 84, 40, 207, 125, 147, 233, 22, 130, 32, 16,
83+
133, 49, 181, 210, 74, 203, 74, 219, 168, 44, 91, 21, 211, 22,
84+
222, 255, 209, 82, 212, 57, 160, 232, 253, 197, 229, 67, 206, 48,
85+
115, 69, 136, 147, 110, 219, 26, 146, 75, 245, 98, 74, 137, 167,
86+
202, 216, 236, 70, 153, 238, 139, 6, 26, 29, 41, 232, 58, 17, 208,
87+
96, 151, 82, 78, 159, 189, 1, 72, 113, 227, 114, 55, 140, 202, 69,
88+
236, 42, 101, 141, 151, 234, 139, 196, 52, 147, 106, 29, 232, 12,
89+
110, 225, 58, 31, 33, 31, 204, 154, 193, 39, 120, 7, 33, 7, 204,
90+
131, 193, 95, 101, 73, 246, 68, 149, 84, 238, 203, 32, 156, 197,
91+
185, 195, 96, 121, 72, 240, 48, 119, 6, 38, 117, 152, 158, 150,
92+
208, 213, 222, 244, 240, 226, 97, 104, 138, 253, 50, 35, 14, 70,
93+
93, 176, 243, 218, 162, 32, 163, 165, 160, 214, 167, 116, 54, 161,
94+
179, 125, 27, 121, 227, 87, 197, 200, 2, 217, 200, 206, 122, 125,
95+
216, 81, 99, 216, 153, 250, 98, 76, 222, 92, 76, 26, 1, 27, 183,
96+
71, 115, 46, 15, 53, 105, 13, 117, 38, 205, 178, 132, 223, 225,
97+
15, 14, 216, 81, 244, 178, 122, 71, 86, 0, 0, 0, 0, 73, 69, 78,
98+
68, 174, 66, 96, 130,
99+
]),
100+
},
101+
},
102+
},
103+
{
104+
document: {
105+
name: 'some_document',
106+
format: 'doc',
107+
source: {
108+
bytes: new Uint8Array([
109+
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0,
110+
0, 0, 28, 0, 0, 0, 28, 8, 3, 0, 0, 0, 69, 211, 47, 166, 0, 0, 0,
111+
168, 80, 76, 84, 69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64,
112+
0, 51, 51, 26, 26, 26, 23, 23, 46, 20, 20, 39, 18, 18, 36, 12, 24,
113+
36, 9, 26, 35, 15, 23, 38, 15, 29, 36, 12, 23, 41, 11, 28, 40, 13,
114+
26, 38, 12, 27, 39, 15, 26, 36, 14, 25, 39, 14, 24, 38, 13, 26,
115+
36, 13, 27, 38, 12, 27, 39, 12, 27, 37, 12, 27, 37, 12, 27, 39,
116+
12, 26, 38, 14, 26, 38, 13, 26, 38, 12, 26, 39, 13, 27, 38, 13,
117+
27, 38, 12, 26, 39, 14, 26, 38, 13, 26, 38, 13, 26, 38, 13, 26,
118+
38, 13, 26, 38, 13, 26, 38, 13, 26, 38, 13, 26, 38, 13, 27, 38,
119+
12, 26, 39, 14, 26, 38, 13, 26, 38, 13, 26, 38, 13, 26, 38, 12,
120+
26, 39, 13, 26, 38, 13, 26, 38, 13, 26, 38, 13, 26, 38, 13, 26,
121+
38, 13, 26, 38, 255, 255, 255, 26, 7, 228, 96, 0, 0, 0, 54, 116,
122+
82, 78, 83, 0, 1, 2, 3, 4, 5, 10, 11, 13, 14, 21, 29, 34, 35, 44,
123+
45, 60, 65, 70, 72, 73, 77, 95, 105, 123, 124, 125, 126, 128, 135,
124+
145, 153, 162, 165, 170, 174, 175, 176, 179, 209, 215, 220, 221,
125+
225, 226, 228, 229, 243, 245, 247, 249, 250, 252, 253, 138, 189,
126+
133, 44, 0, 0, 0, 1, 98, 75, 71, 68, 55, 48, 184, 184, 71, 0, 0,
127+
0, 214, 73, 68, 65, 84, 40, 207, 125, 147, 233, 22, 130, 32, 16,
128+
133, 49, 181, 210, 74, 203, 74, 219, 168, 44, 91, 21, 211, 22,
129+
222, 255, 209, 82, 212, 57, 160, 232, 253, 197, 229, 67, 206, 48,
130+
115, 69, 136, 147, 110, 219, 26, 146, 75, 245, 98, 74, 137, 167,
131+
202, 216, 236, 70, 153, 238, 139, 6, 26, 29, 41, 232, 58, 17, 208,
132+
96, 151, 82, 78, 159, 189, 1, 72, 113, 227, 114, 55, 140, 202, 69,
133+
236, 42, 101, 141, 151, 234, 139, 196, 52, 147, 106, 29, 232, 12,
134+
110, 225, 58, 31, 33, 31, 204, 154, 193, 39, 120, 7, 33, 7, 204,
135+
131, 193, 95, 101, 73, 246, 68, 149, 84, 238, 203, 32, 156, 197,
136+
185, 195, 96, 121, 72, 240, 48, 119, 6, 38, 117, 152, 158, 150,
137+
208, 213, 222, 244, 240, 226, 97, 104, 138, 253, 50, 35, 14, 70,
138+
93, 176, 243, 218, 162, 32, 163, 165, 160, 214, 167, 116, 54, 161,
139+
179, 125, 27, 121, 227, 87, 197, 200, 2, 217, 200, 206, 122, 125,
140+
216, 81, 99, 216, 153, 250, 98, 76, 222, 92, 76, 26, 1, 27, 183,
141+
71, 115, 46, 15, 53, 105, 13, 117, 38, 205, 178, 132, 223, 225,
142+
15, 14, 216, 81, 244, 178, 122, 71, 86, 0, 0, 0, 0, 73, 69, 78,
143+
68, 174, 66, 96, 130,
144+
]),
145+
},
146+
},
147+
},
57148
],
58149
role: 'user' as const,
59150
createdAt: new Date(2023, 4, 21, 15, 24).toDateString(),

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"**/@size-limit/webpack/webpack": "^5.76.0",
8383
"**/serve/serve-handler/minimatch": "3.0.5",
8484
"**/serve/serve-handler/path-to-regexp": "3.3.0",
85+
"@aws-amplify/data-schema": "^1.19.0",
8586
"@adobe/css-tools": "^4.3.2",
8687
"@sideway/formula": "^3.0.1",
8788
"@types/react": "^18.3.0",

packages/react-ai/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"typecheck": "tsc --noEmit"
4343
},
4444
"peerDependencies": {
45-
"@aws-amplify/api-graphql": "unstable",
45+
"@aws-amplify/data-schema": "^1.19.0",
4646
"aws-amplify": "^6.9.0",
4747
"react": "^16.14 || ^17 || ^18 || ^19",
4848
"react-dom": "^16.14 || ^17 || ^18 || ^19"
@@ -57,13 +57,13 @@
5757
"name": "AIConversation",
5858
"path": "dist/esm/index.mjs",
5959
"import": "{ AIConversation }",
60-
"limit": "25 kB"
60+
"limit": "27 kB"
6161
},
6262
{
6363
"name": "createAIConversation",
6464
"path": "dist/esm/index.mjs",
6565
"import": "{ createAIConversation }",
66-
"limit": "7 kB"
66+
"limit": "20 kB"
6767
}
6868
]
6969
}

packages/react-ai/src/components/AIConversation/__tests__/__snapshots__/displayText.test.tsx.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
exports[`displayText should match snapshot 1`] = `
44
[
5+
"getAttachmentFormatErrorText",
56
"getAttachmentSizeErrorText",
67
"getMaxAttachmentErrorText",
78
"getMessageTimestampText",

packages/react-ai/src/components/AIConversation/__tests__/utils.test.tsx

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {
22
convertBufferToBase64,
33
formatDate,
4-
getImageTypeFromMimeType,
4+
getAttachmentFormat,
55
attachmentsValidator,
6+
getValidDocumentName,
67
} from '../utils';
78

89
describe('convertBufferToBase64', () => {
@@ -50,12 +51,45 @@ describe('formatDate', () => {
5051
});
5152
});
5253

53-
describe('getImageTypeFromMimeType', () => {
54-
it('should return the image type', () => {
55-
expect(getImageTypeFromMimeType('image/jpeg')).toBe('jpeg');
56-
expect(getImageTypeFromMimeType('image/gif')).toBe('gif');
57-
expect(getImageTypeFromMimeType('image/png')).toBe('png');
58-
expect(getImageTypeFromMimeType('image/webp')).toBe('webp');
54+
describe('getValidDocumentName', () => {
55+
it('should remove invalid characters from the file name', () => {
56+
const file = new File([''], 'test!@#$%^&*()+-.txt');
57+
const validName = getValidDocumentName(file);
58+
expect(validName).toBe('test');
59+
});
60+
61+
it('should handle files with multiple dots correctly', () => {
62+
const file = new File([''], 'test..txt');
63+
const validName = getValidDocumentName(file);
64+
expect(validName).toBe('test');
65+
});
66+
67+
it('should handle files with no extension correctly', () => {
68+
const file = new File([''], 'test');
69+
const validName = getValidDocumentName(file);
70+
expect(validName).toBe('test');
71+
});
72+
73+
it('should handle files with spaces correctly', () => {
74+
const file = new File([''], 'test file.txt');
75+
const validName = getValidDocumentName(file);
76+
expect(validName).toBe('test_file');
77+
});
78+
});
79+
80+
describe('getAttachmentFormat', () => {
81+
it('should get format from the extension', () => {
82+
const file = new File([''], 'test.docx', {
83+
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
84+
});
85+
const format = getAttachmentFormat(file);
86+
expect(format).toBe('docx');
87+
});
88+
89+
it('should get the format from mimetype if there is no extension', () => {
90+
const file = new File([''], 'test', { type: 'image/png' });
91+
const format = getAttachmentFormat(file);
92+
expect(format).toBe('png');
5993
});
6094
});
6195

@@ -145,4 +179,21 @@ describe('attachmentsValidator', () => {
145179
expect(result.hasMaxAttachmentSizeError).toBeFalsy();
146180
expect(result.hasMaxAttachmentsError).toBeFalsy();
147181
});
182+
183+
it('should handle unsupported file types', async () => {
184+
const files = [
185+
new File([''], 'test.exe', { type: 'application/x-msdownload' }),
186+
];
187+
const result = await attachmentsValidator({
188+
files,
189+
maxAttachments: 3,
190+
maxAttachmentSize: 1000,
191+
});
192+
193+
expect(result.acceptedFiles).toHaveLength(0);
194+
expect(result.rejectedFiles).toHaveLength(1);
195+
expect(result.hasMaxAttachmentSizeError).toBeFalsy();
196+
expect(result.hasMaxAttachmentsError).toBeFalsy();
197+
expect(result.hasUnsupportedFileError).toBeTruthy();
198+
});
148199
});

packages/react-ai/src/components/AIConversation/displayText.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,23 @@ export type ConversationDisplayText = {
55
getMessageTimestampText?: (date: Date) => string;
66
getMaxAttachmentErrorText?: (count: number) => string;
77
getAttachmentSizeErrorText?: (sizeText: string) => string;
8+
getAttachmentFormatErrorText?: (formats: string[]) => string;
89
};
910

1011
export const defaultAIConversationDisplayTextEn: Required<AIConversationDisplayText> =
1112
{
12-
getMessageTimestampText: (date: Date) => formatDate(date),
13-
getMaxAttachmentErrorText(count: number): string {
13+
getMessageTimestampText: (date) => formatDate(date),
14+
getMaxAttachmentErrorText(count) {
1415
return `Cannot choose more than ${count} ${
1516
count === 1 ? 'file' : 'files'
1617
}. `;
1718
},
18-
getAttachmentSizeErrorText(sizeText: string): string {
19+
getAttachmentSizeErrorText(sizeText) {
1920
return `File size must be below ${sizeText}.`;
2021
},
22+
getAttachmentFormatErrorText(formats) {
23+
return `Files must be one of the supported types: ${formats.join(', ')}.`;
24+
},
2125
};
2226

2327
export type AIConversationDisplayText =

0 commit comments

Comments
 (0)