@ai-sdk/react 0.0.39 → 0.0.41

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @ai-sdk/react@0.0.39 build /home/runner/work/ai/ai/packages/react
2
+ > @ai-sdk/react@0.0.41 build /home/runner/work/ai/ai/packages/react
3
3
  > tsup
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -11,11 +11,11 @@
11
11
  ESM Build start
12
12
  ESM dist/index.mjs 25.67 KB
13
13
  ESM dist/index.mjs.map 55.94 KB
14
- ESM ⚡️ Build success in 61ms
14
+ ESM ⚡️ Build success in 52ms
15
15
  CJS dist/index.js 28.14 KB
16
16
  CJS dist/index.js.map 56.02 KB
17
- CJS ⚡️ Build success in 64ms
17
+ CJS ⚡️ Build success in 53ms
18
18
  DTS Build start
19
- DTS ⚡️ Build success in 5189ms
19
+ DTS ⚡️ Build success in 5278ms
20
20
  DTS dist/index.d.ts 10.99 KB
21
21
  DTS dist/index.d.mts 10.99 KB
@@ -1,4 +1,4 @@
1
1
 
2
- > @ai-sdk/react@0.0.39 clean /home/runner/work/ai/ai/packages/react
2
+ > @ai-sdk/react@0.0.41 clean /home/runner/work/ai/ai/packages/react
3
3
  > rm -rf dist
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @ai-sdk/react
2
2
 
3
+ ## 0.0.41
4
+
5
+ ### Patch Changes
6
+
7
+ - e5b58f3: fix (ai/ui): forward streaming errors in useChat
8
+ - Updated dependencies [e5b58f3]
9
+ - @ai-sdk/ui-utils@0.0.29
10
+
11
+ ## 0.0.40
12
+
13
+ ### Patch Changes
14
+
15
+ - @ai-sdk/provider-utils@1.0.9
16
+ - @ai-sdk/ui-utils@0.0.28
17
+
3
18
  ## 0.0.39
4
19
 
5
20
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/react",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -15,14 +15,14 @@
15
15
  }
16
16
  },
17
17
  "dependencies": {
18
- "@ai-sdk/provider-utils": "1.0.8",
19
- "@ai-sdk/ui-utils": "0.0.27",
18
+ "@ai-sdk/provider-utils": "1.0.9",
19
+ "@ai-sdk/ui-utils": "0.0.29",
20
20
  "swr": "2.2.5"
21
21
  },
22
22
  "devDependencies": {
23
- "@testing-library/jest-dom": "^6.4.5",
24
- "@testing-library/user-event": "^14.5.1",
25
- "@testing-library/react": "^15.0.7",
23
+ "@testing-library/jest-dom": "^6.4.8",
24
+ "@testing-library/user-event": "^14.5.2",
25
+ "@testing-library/react": "^16.0.0",
26
26
  "@types/node": "^18",
27
27
  "@types/react": "^18",
28
28
  "@types/react-dom": "^18",
@@ -121,7 +121,7 @@ describe('stream data stream', () => {
121
121
  );
122
122
 
123
123
  it(
124
- 'should show error response',
124
+ 'should show error response when there is a server error',
125
125
  withTestServer(
126
126
  { type: 'error', url: '/api/chat', status: 404, content: 'Not found' },
127
127
  async () => {
@@ -135,6 +135,25 @@ describe('stream data stream', () => {
135
135
  ),
136
136
  );
137
137
 
138
+ it(
139
+ 'should show error response when there is a streaming error',
140
+ withTestServer(
141
+ {
142
+ type: 'stream-values',
143
+ url: '/api/chat',
144
+ content: ['3:"custom error message"\n'],
145
+ },
146
+ async () => {
147
+ await userEvent.click(screen.getByTestId('do-append'));
148
+
149
+ await screen.findByTestId('error');
150
+ expect(screen.getByTestId('error')).toHaveTextContent(
151
+ 'Error: custom error message',
152
+ );
153
+ },
154
+ ),
155
+ );
156
+
138
157
  describe('loading state', () => {
139
158
  it(
140
159
  'should show loading state',
@@ -1016,327 +1035,325 @@ describe('maxToolRoundtrips', () => {
1016
1035
  });
1017
1036
  });
1018
1037
 
1019
- // Disabled: unstable with React 18.3.3 TODO re-enable
1020
- //
1021
- // describe('file attachments with data url', () => {
1022
- // const TestComponent = () => {
1023
- // const { messages, handleSubmit, handleInputChange, isLoading, input } =
1024
- // useChat();
1025
-
1026
- // const [attachments, setAttachments] = useState<FileList | undefined>(
1027
- // undefined,
1028
- // );
1029
- // const fileInputRef = useRef<HTMLInputElement>(null);
1030
-
1031
- // return (
1032
- // <div>
1033
- // {messages.map((m, idx) => (
1034
- // <div data-testid={`message-${idx}`} key={m.id}>
1035
- // {m.role === 'user' ? 'User: ' : 'AI: '}
1036
- // {m.content}
1037
- // {m.experimental_attachments?.map(attachment => {
1038
- // if (attachment.contentType?.startsWith('image/')) {
1039
- // return (
1040
- // <img
1041
- // key={attachment.name}
1042
- // src={attachment.url}
1043
- // alt={attachment.name}
1044
- // data-testid={`attachment-${idx}`}
1045
- // />
1046
- // );
1047
- // } else if (attachment.contentType?.startsWith('text/')) {
1048
- // return (
1049
- // <div key={attachment.name} data-testid={`attachment-${idx}`}>
1050
- // {getTextFromDataUrl(attachment.url)}
1051
- // </div>
1052
- // );
1053
- // }
1054
- // })}
1055
- // </div>
1056
- // ))}
1057
-
1058
- // <form
1059
- // onSubmit={event => {
1060
- // handleSubmit(event, {
1061
- // experimental_attachments: attachments,
1062
- // });
1063
- // setAttachments(undefined);
1064
- // if (fileInputRef.current) {
1065
- // fileInputRef.current.value = '';
1066
- // }
1067
- // }}
1068
- // data-testid="chat-form"
1069
- // >
1070
- // <input
1071
- // type="file"
1072
- // onChange={event => {
1073
- // if (event.target.files) {
1074
- // setAttachments(event.target.files);
1075
- // }
1076
- // }}
1077
- // multiple
1078
- // ref={fileInputRef}
1079
- // data-testid="file-input"
1080
- // />
1081
- // <input
1082
- // value={input}
1083
- // onChange={handleInputChange}
1084
- // disabled={isLoading}
1085
- // data-testid="message-input"
1086
- // />
1087
- // <button type="submit" data-testid="submit-button">
1088
- // Send
1089
- // </button>
1090
- // </form>
1091
- // </div>
1092
- // );
1093
- // };
1094
-
1095
- // beforeEach(() => {
1096
- // render(<TestComponent />);
1097
- // });
1098
-
1099
- // afterEach(() => {
1100
- // vi.restoreAllMocks();
1101
- // cleanup();
1102
- // });
1103
-
1104
- // it(
1105
- // 'should handle text file attachment and submission',
1106
- // withTestServer(
1107
- // {
1108
- // url: '/api/chat',
1109
- // type: 'stream-values',
1110
- // content: ['0:"Response to message with text attachment"\n'],
1111
- // },
1112
- // async ({ call }) => {
1113
- // const file = new File(['test file content'], 'test.txt', {
1114
- // type: 'text/plain',
1115
- // });
1116
-
1117
- // const fileInput = screen.getByTestId('file-input');
1118
- // await userEvent.upload(fileInput, file);
1119
-
1120
- // const messageInput = screen.getByTestId('message-input');
1121
- // await userEvent.type(messageInput, 'Message with text attachment');
1122
-
1123
- // const submitButton = screen.getByTestId('submit-button');
1124
- // await userEvent.click(submitButton);
1125
-
1126
- // expect(await call(0).getRequestBodyJson()).toStrictEqual({
1127
- // messages: [
1128
- // {
1129
- // role: 'user',
1130
- // content: 'Message with text attachment',
1131
- // experimental_attachments: [
1132
- // {
1133
- // name: 'test.txt',
1134
- // contentType: 'text/plain',
1135
- // url: 'data:text/plain;base64,dGVzdCBmaWxlIGNvbnRlbnQ=',
1136
- // },
1137
- // ],
1138
- // },
1139
- // ],
1140
- // });
1141
-
1142
- // await screen.findByTestId('message-0');
1143
- // expect(screen.getByTestId('message-0')).toHaveTextContent(
1144
- // 'User: Message with text attachment',
1145
- // );
1146
-
1147
- // await screen.findByTestId('attachment-0');
1148
- // expect(screen.getByTestId('attachment-0')).toHaveTextContent(
1149
- // 'test file content',
1150
- // );
1151
-
1152
- // await screen.findByTestId('message-1');
1153
- // expect(screen.getByTestId('message-1')).toHaveTextContent(
1154
- // 'AI: Response to message with text attachment',
1155
- // );
1156
- // },
1157
- // ),
1158
- // );
1159
-
1160
- // it(
1161
- // 'should handle image file attachment and submission',
1162
- // withTestServer(
1163
- // {
1164
- // url: '/api/chat',
1165
- // type: 'stream-values',
1166
- // content: ['0:"Response to message with image attachment"\n'],
1167
- // },
1168
- // async ({ call }) => {
1169
- // const file = new File(['test image content'], 'test.png', {
1170
- // type: 'image/png',
1171
- // });
1172
-
1173
- // const fileInput = screen.getByTestId('file-input');
1174
- // await userEvent.upload(fileInput, file);
1175
-
1176
- // const messageInput = screen.getByTestId('message-input');
1177
- // await userEvent.type(messageInput, 'Message with image attachment');
1178
-
1179
- // const submitButton = screen.getByTestId('submit-button');
1180
- // await userEvent.click(submitButton);
1181
-
1182
- // expect(await call(0).getRequestBodyJson()).toStrictEqual({
1183
- // messages: [
1184
- // {
1185
- // role: 'user',
1186
- // content: 'Message with image attachment',
1187
- // experimental_attachments: [
1188
- // {
1189
- // name: 'test.png',
1190
- // contentType: 'image/png',
1191
- // url: 'data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50',
1192
- // },
1193
- // ],
1194
- // },
1195
- // ],
1196
- // });
1197
-
1198
- // await screen.findByTestId('message-0');
1199
- // expect(screen.getByTestId('message-0')).toHaveTextContent(
1200
- // 'User: Message with image attachment',
1201
- // );
1202
-
1203
- // await screen.findByTestId('attachment-0');
1204
- // expect(screen.getByTestId('attachment-0')).toHaveAttribute(
1205
- // 'src',
1206
- // expect.stringContaining('data:image/png;base64'),
1207
- // );
1208
-
1209
- // await screen.findByTestId('message-1');
1210
- // expect(screen.getByTestId('message-1')).toHaveTextContent(
1211
- // 'AI: Response to message with image attachment',
1212
- // );
1213
- // },
1214
- // ),
1215
- // );
1216
- // });
1217
-
1218
- // describe('file attachments with url', () => {
1219
- // const TestComponent = () => {
1220
- // const { messages, handleSubmit, handleInputChange, isLoading, input } =
1221
- // useChat();
1222
-
1223
- // return (
1224
- // <div>
1225
- // {messages.map((m, idx) => (
1226
- // <div data-testid={`message-${idx}`} key={m.id}>
1227
- // {m.role === 'user' ? 'User: ' : 'AI: '}
1228
- // {m.content}
1229
- // {m.experimental_attachments?.map(attachment => {
1230
- // if (attachment.contentType?.startsWith('image/')) {
1231
- // return (
1232
- // <img
1233
- // key={attachment.name}
1234
- // src={attachment.url}
1235
- // alt={attachment.name}
1236
- // data-testid={`attachment-${idx}`}
1237
- // />
1238
- // );
1239
- // } else if (attachment.contentType?.startsWith('text/')) {
1240
- // return (
1241
- // <div key={attachment.name} data-testid={`attachment-${idx}`}>
1242
- // {Buffer.from(
1243
- // attachment.url.split(',')[1],
1244
- // 'base64',
1245
- // ).toString('utf-8')}
1246
- // </div>
1247
- // );
1248
- // }
1249
- // })}
1250
- // </div>
1251
- // ))}
1252
-
1253
- // <form
1254
- // onSubmit={event => {
1255
- // handleSubmit(event, {
1256
- // experimental_attachments: [
1257
- // {
1258
- // name: 'test.png',
1259
- // contentType: 'image/png',
1260
- // url: 'https://example.com/image.png',
1261
- // },
1262
- // ],
1263
- // });
1264
- // }}
1265
- // data-testid="chat-form"
1266
- // >
1267
- // <input
1268
- // value={input}
1269
- // onChange={handleInputChange}
1270
- // disabled={isLoading}
1271
- // data-testid="message-input"
1272
- // />
1273
- // <button type="submit" data-testid="submit-button">
1274
- // Send
1275
- // </button>
1276
- // </form>
1277
- // </div>
1278
- // );
1279
- // };
1280
-
1281
- // beforeEach(() => {
1282
- // render(<TestComponent />);
1283
- // });
1284
-
1285
- // afterEach(() => {
1286
- // vi.restoreAllMocks();
1287
- // cleanup();
1288
- // });
1289
-
1290
- // it(
1291
- // 'should handle image file attachment and submission',
1292
- // withTestServer(
1293
- // {
1294
- // url: '/api/chat',
1295
- // type: 'stream-values',
1296
- // content: ['0:"Response to message with image attachment"\n'],
1297
- // },
1298
- // async ({ call }) => {
1299
- // const messageInput = screen.getByTestId('message-input');
1300
- // await userEvent.type(messageInput, 'Message with image attachment');
1301
-
1302
- // const submitButton = screen.getByTestId('submit-button');
1303
- // await userEvent.click(submitButton);
1304
-
1305
- // expect(await call(0).getRequestBodyJson()).toStrictEqual({
1306
- // messages: [
1307
- // {
1308
- // role: 'user',
1309
- // content: 'Message with image attachment',
1310
- // experimental_attachments: [
1311
- // {
1312
- // name: 'test.png',
1313
- // contentType: 'image/png',
1314
- // url: 'https://example.com/image.png',
1315
- // },
1316
- // ],
1317
- // },
1318
- // ],
1319
- // });
1320
-
1321
- // await screen.findByTestId('message-0');
1322
- // expect(screen.getByTestId('message-0')).toHaveTextContent(
1323
- // 'User: Message with image attachment',
1324
- // );
1325
-
1326
- // await screen.findByTestId('attachment-0');
1327
- // expect(screen.getByTestId('attachment-0')).toHaveAttribute(
1328
- // 'src',
1329
- // expect.stringContaining('https://example.com/image.png'),
1330
- // );
1331
-
1332
- // await screen.findByTestId('message-1');
1333
- // expect(screen.getByTestId('message-1')).toHaveTextContent(
1334
- // 'AI: Response to message with image attachment',
1335
- // );
1336
- // },
1337
- // ),
1338
- // );
1339
- // });
1038
+ describe('file attachments with data url', () => {
1039
+ const TestComponent = () => {
1040
+ const { messages, handleSubmit, handleInputChange, isLoading, input } =
1041
+ useChat();
1042
+
1043
+ const [attachments, setAttachments] = useState<FileList | undefined>(
1044
+ undefined,
1045
+ );
1046
+ const fileInputRef = useRef<HTMLInputElement>(null);
1047
+
1048
+ return (
1049
+ <div>
1050
+ {messages.map((m, idx) => (
1051
+ <div data-testid={`message-${idx}`} key={m.id}>
1052
+ {m.role === 'user' ? 'User: ' : 'AI: '}
1053
+ {m.content}
1054
+ {m.experimental_attachments?.map(attachment => {
1055
+ if (attachment.contentType?.startsWith('image/')) {
1056
+ return (
1057
+ <img
1058
+ key={attachment.name}
1059
+ src={attachment.url}
1060
+ alt={attachment.name}
1061
+ data-testid={`attachment-${idx}`}
1062
+ />
1063
+ );
1064
+ } else if (attachment.contentType?.startsWith('text/')) {
1065
+ return (
1066
+ <div key={attachment.name} data-testid={`attachment-${idx}`}>
1067
+ {getTextFromDataUrl(attachment.url)}
1068
+ </div>
1069
+ );
1070
+ }
1071
+ })}
1072
+ </div>
1073
+ ))}
1074
+
1075
+ <form
1076
+ onSubmit={event => {
1077
+ handleSubmit(event, {
1078
+ experimental_attachments: attachments,
1079
+ });
1080
+ setAttachments(undefined);
1081
+ if (fileInputRef.current) {
1082
+ fileInputRef.current.value = '';
1083
+ }
1084
+ }}
1085
+ data-testid="chat-form"
1086
+ >
1087
+ <input
1088
+ type="file"
1089
+ onChange={event => {
1090
+ if (event.target.files) {
1091
+ setAttachments(event.target.files);
1092
+ }
1093
+ }}
1094
+ multiple
1095
+ ref={fileInputRef}
1096
+ data-testid="file-input"
1097
+ />
1098
+ <input
1099
+ value={input}
1100
+ onChange={handleInputChange}
1101
+ disabled={isLoading}
1102
+ data-testid="message-input"
1103
+ />
1104
+ <button type="submit" data-testid="submit-button">
1105
+ Send
1106
+ </button>
1107
+ </form>
1108
+ </div>
1109
+ );
1110
+ };
1111
+
1112
+ beforeEach(() => {
1113
+ render(<TestComponent />);
1114
+ });
1115
+
1116
+ afterEach(() => {
1117
+ vi.restoreAllMocks();
1118
+ cleanup();
1119
+ });
1120
+
1121
+ it(
1122
+ 'should handle text file attachment and submission',
1123
+ withTestServer(
1124
+ {
1125
+ url: '/api/chat',
1126
+ type: 'stream-values',
1127
+ content: ['0:"Response to message with text attachment"\n'],
1128
+ },
1129
+ async ({ call }) => {
1130
+ const file = new File(['test file content'], 'test.txt', {
1131
+ type: 'text/plain',
1132
+ });
1133
+
1134
+ const fileInput = screen.getByTestId('file-input');
1135
+ await userEvent.upload(fileInput, file);
1136
+
1137
+ const messageInput = screen.getByTestId('message-input');
1138
+ await userEvent.type(messageInput, 'Message with text attachment');
1139
+
1140
+ const submitButton = screen.getByTestId('submit-button');
1141
+ await userEvent.click(submitButton);
1142
+
1143
+ await screen.findByTestId('message-0');
1144
+ expect(screen.getByTestId('message-0')).toHaveTextContent(
1145
+ 'User: Message with text attachment',
1146
+ );
1147
+
1148
+ await screen.findByTestId('attachment-0');
1149
+ expect(screen.getByTestId('attachment-0')).toHaveTextContent(
1150
+ 'test file content',
1151
+ );
1152
+
1153
+ await screen.findByTestId('message-1');
1154
+ expect(screen.getByTestId('message-1')).toHaveTextContent(
1155
+ 'AI: Response to message with text attachment',
1156
+ );
1157
+
1158
+ expect(await call(0).getRequestBodyJson()).toStrictEqual({
1159
+ messages: [
1160
+ {
1161
+ role: 'user',
1162
+ content: 'Message with text attachment',
1163
+ experimental_attachments: [
1164
+ {
1165
+ name: 'test.txt',
1166
+ contentType: 'text/plain',
1167
+ url: 'data:text/plain;base64,dGVzdCBmaWxlIGNvbnRlbnQ=',
1168
+ },
1169
+ ],
1170
+ },
1171
+ ],
1172
+ });
1173
+ },
1174
+ ),
1175
+ );
1176
+
1177
+ it(
1178
+ 'should handle image file attachment and submission',
1179
+ withTestServer(
1180
+ {
1181
+ url: '/api/chat',
1182
+ type: 'stream-values',
1183
+ content: ['0:"Response to message with image attachment"\n'],
1184
+ },
1185
+ async ({ call }) => {
1186
+ const file = new File(['test image content'], 'test.png', {
1187
+ type: 'image/png',
1188
+ });
1189
+
1190
+ const fileInput = screen.getByTestId('file-input');
1191
+ await userEvent.upload(fileInput, file);
1192
+
1193
+ const messageInput = screen.getByTestId('message-input');
1194
+ await userEvent.type(messageInput, 'Message with image attachment');
1195
+
1196
+ const submitButton = screen.getByTestId('submit-button');
1197
+ await userEvent.click(submitButton);
1198
+
1199
+ await screen.findByTestId('message-0');
1200
+ expect(screen.getByTestId('message-0')).toHaveTextContent(
1201
+ 'User: Message with image attachment',
1202
+ );
1203
+
1204
+ await screen.findByTestId('attachment-0');
1205
+ expect(screen.getByTestId('attachment-0')).toHaveAttribute(
1206
+ 'src',
1207
+ expect.stringContaining('data:image/png;base64'),
1208
+ );
1209
+
1210
+ await screen.findByTestId('message-1');
1211
+ expect(screen.getByTestId('message-1')).toHaveTextContent(
1212
+ 'AI: Response to message with image attachment',
1213
+ );
1214
+
1215
+ expect(await call(0).getRequestBodyJson()).toStrictEqual({
1216
+ messages: [
1217
+ {
1218
+ role: 'user',
1219
+ content: 'Message with image attachment',
1220
+ experimental_attachments: [
1221
+ {
1222
+ name: 'test.png',
1223
+ contentType: 'image/png',
1224
+ url: 'data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50',
1225
+ },
1226
+ ],
1227
+ },
1228
+ ],
1229
+ });
1230
+ },
1231
+ ),
1232
+ );
1233
+ });
1234
+
1235
+ describe('file attachments with url', () => {
1236
+ const TestComponent = () => {
1237
+ const { messages, handleSubmit, handleInputChange, isLoading, input } =
1238
+ useChat();
1239
+
1240
+ return (
1241
+ <div>
1242
+ {messages.map((m, idx) => (
1243
+ <div data-testid={`message-${idx}`} key={m.id}>
1244
+ {m.role === 'user' ? 'User: ' : 'AI: '}
1245
+ {m.content}
1246
+ {m.experimental_attachments?.map(attachment => {
1247
+ if (attachment.contentType?.startsWith('image/')) {
1248
+ return (
1249
+ <img
1250
+ key={attachment.name}
1251
+ src={attachment.url}
1252
+ alt={attachment.name}
1253
+ data-testid={`attachment-${idx}`}
1254
+ />
1255
+ );
1256
+ } else if (attachment.contentType?.startsWith('text/')) {
1257
+ return (
1258
+ <div key={attachment.name} data-testid={`attachment-${idx}`}>
1259
+ {Buffer.from(
1260
+ attachment.url.split(',')[1],
1261
+ 'base64',
1262
+ ).toString('utf-8')}
1263
+ </div>
1264
+ );
1265
+ }
1266
+ })}
1267
+ </div>
1268
+ ))}
1269
+
1270
+ <form
1271
+ onSubmit={event => {
1272
+ handleSubmit(event, {
1273
+ experimental_attachments: [
1274
+ {
1275
+ name: 'test.png',
1276
+ contentType: 'image/png',
1277
+ url: 'https://example.com/image.png',
1278
+ },
1279
+ ],
1280
+ });
1281
+ }}
1282
+ data-testid="chat-form"
1283
+ >
1284
+ <input
1285
+ value={input}
1286
+ onChange={handleInputChange}
1287
+ disabled={isLoading}
1288
+ data-testid="message-input"
1289
+ />
1290
+ <button type="submit" data-testid="submit-button">
1291
+ Send
1292
+ </button>
1293
+ </form>
1294
+ </div>
1295
+ );
1296
+ };
1297
+
1298
+ beforeEach(() => {
1299
+ render(<TestComponent />);
1300
+ });
1301
+
1302
+ afterEach(() => {
1303
+ vi.restoreAllMocks();
1304
+ cleanup();
1305
+ });
1306
+
1307
+ it(
1308
+ 'should handle image file attachment and submission',
1309
+ withTestServer(
1310
+ {
1311
+ url: '/api/chat',
1312
+ type: 'stream-values',
1313
+ content: ['0:"Response to message with image attachment"\n'],
1314
+ },
1315
+ async ({ call }) => {
1316
+ const messageInput = screen.getByTestId('message-input');
1317
+ await userEvent.type(messageInput, 'Message with image attachment');
1318
+
1319
+ const submitButton = screen.getByTestId('submit-button');
1320
+ await userEvent.click(submitButton);
1321
+
1322
+ await screen.findByTestId('message-0');
1323
+ expect(screen.getByTestId('message-0')).toHaveTextContent(
1324
+ 'User: Message with image attachment',
1325
+ );
1326
+
1327
+ await screen.findByTestId('attachment-0');
1328
+ expect(screen.getByTestId('attachment-0')).toHaveAttribute(
1329
+ 'src',
1330
+ expect.stringContaining('https://example.com/image.png'),
1331
+ );
1332
+
1333
+ await screen.findByTestId('message-1');
1334
+ expect(screen.getByTestId('message-1')).toHaveTextContent(
1335
+ 'AI: Response to message with image attachment',
1336
+ );
1337
+
1338
+ expect(await call(0).getRequestBodyJson()).toStrictEqual({
1339
+ messages: [
1340
+ {
1341
+ role: 'user',
1342
+ content: 'Message with image attachment',
1343
+ experimental_attachments: [
1344
+ {
1345
+ name: 'test.png',
1346
+ contentType: 'image/png',
1347
+ url: 'https://example.com/image.png',
1348
+ },
1349
+ ],
1350
+ },
1351
+ ],
1352
+ });
1353
+ },
1354
+ ),
1355
+ );
1356
+ });
1340
1357
 
1341
1358
  describe('reload', () => {
1342
1359
  const TestComponent = () => {