@ai-sdk/react 0.0.35 → 0.0.37

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.
@@ -265,8 +265,11 @@ describe('text stream', () => {
265
265
  <div>
266
266
  {messages.map((m, idx) => (
267
267
  <div data-testid={`message-${idx}-text-stream`} key={m.id}>
268
- {m.role === 'user' ? 'User: ' : 'AI: '}
269
- {m.content}
268
+ <div data-testid={`message-${idx}-id`}>{m.id}</div>
269
+ <div data-testid={`message-${idx}-role`}>
270
+ {m.role === 'user' ? 'User: ' : 'AI: '}
271
+ </div>
272
+ <div data-testid={`message-${idx}-content`}>{m.content}</div>
270
273
  </div>
271
274
  ))}
272
275
 
@@ -302,15 +305,39 @@ describe('text stream', () => {
302
305
  async () => {
303
306
  await userEvent.click(screen.getByTestId('do-append-text-stream'));
304
307
 
305
- await screen.findByTestId('message-0-text-stream');
306
- expect(screen.getByTestId('message-0-text-stream')).toHaveTextContent(
307
- 'User: hi',
308
+ await screen.findByTestId('message-0-content');
309
+ expect(screen.getByTestId('message-0-content')).toHaveTextContent('hi');
310
+
311
+ await screen.findByTestId('message-1-content');
312
+ expect(screen.getByTestId('message-1-content')).toHaveTextContent(
313
+ 'Hello, world.',
308
314
  );
315
+ },
316
+ ),
317
+ );
309
318
 
310
- await screen.findByTestId('message-1-text-stream');
311
- expect(screen.getByTestId('message-1-text-stream')).toHaveTextContent(
312
- 'AI: Hello, world.',
319
+ it(
320
+ 'should have stable message ids',
321
+ withTestServer(
322
+ { url: '/api/chat', type: 'controlled-stream' },
323
+ async ({ streamController }) => {
324
+ streamController.enqueue('He');
325
+
326
+ await userEvent.click(screen.getByTestId('do-append-text-stream'));
327
+
328
+ await screen.findByTestId('message-1-content');
329
+ expect(screen.getByTestId('message-1-content')).toHaveTextContent('He');
330
+
331
+ const id = screen.getByTestId('message-1-id').textContent;
332
+
333
+ streamController.enqueue('llo');
334
+ streamController.close();
335
+
336
+ await screen.findByTestId('message-1-content');
337
+ expect(screen.getByTestId('message-1-content')).toHaveTextContent(
338
+ 'Hello',
313
339
  );
340
+ expect(screen.getByTestId('message-1-id').textContent).toBe(id);
314
341
  },
315
342
  ),
316
343
  );
@@ -989,325 +1016,327 @@ describe('maxToolRoundtrips', () => {
989
1016
  });
990
1017
  });
991
1018
 
992
- describe('file attachments with data url', () => {
993
- const TestComponent = () => {
994
- const { messages, handleSubmit, handleInputChange, isLoading, input } =
995
- useChat();
996
-
997
- const [attachments, setAttachments] = useState<FileList | undefined>(
998
- undefined,
999
- );
1000
- const fileInputRef = useRef<HTMLInputElement>(null);
1001
-
1002
- return (
1003
- <div>
1004
- {messages.map((m, idx) => (
1005
- <div data-testid={`message-${idx}`} key={m.id}>
1006
- {m.role === 'user' ? 'User: ' : 'AI: '}
1007
- {m.content}
1008
- {m.experimental_attachments?.map(attachment => {
1009
- if (attachment.contentType?.startsWith('image/')) {
1010
- return (
1011
- <img
1012
- key={attachment.name}
1013
- src={attachment.url}
1014
- alt={attachment.name}
1015
- data-testid={`attachment-${idx}`}
1016
- />
1017
- );
1018
- } else if (attachment.contentType?.startsWith('text/')) {
1019
- return (
1020
- <div key={attachment.name} data-testid={`attachment-${idx}`}>
1021
- {getTextFromDataUrl(attachment.url)}
1022
- </div>
1023
- );
1024
- }
1025
- })}
1026
- </div>
1027
- ))}
1028
-
1029
- <form
1030
- onSubmit={event => {
1031
- handleSubmit(event, {
1032
- experimental_attachments: attachments,
1033
- });
1034
- setAttachments(undefined);
1035
- if (fileInputRef.current) {
1036
- fileInputRef.current.value = '';
1037
- }
1038
- }}
1039
- data-testid="chat-form"
1040
- >
1041
- <input
1042
- type="file"
1043
- onChange={event => {
1044
- if (event.target.files) {
1045
- setAttachments(event.target.files);
1046
- }
1047
- }}
1048
- multiple
1049
- ref={fileInputRef}
1050
- data-testid="file-input"
1051
- />
1052
- <input
1053
- value={input}
1054
- onChange={handleInputChange}
1055
- disabled={isLoading}
1056
- data-testid="message-input"
1057
- />
1058
- <button type="submit" data-testid="submit-button">
1059
- Send
1060
- </button>
1061
- </form>
1062
- </div>
1063
- );
1064
- };
1065
-
1066
- beforeEach(() => {
1067
- render(<TestComponent />);
1068
- });
1069
-
1070
- afterEach(() => {
1071
- vi.restoreAllMocks();
1072
- cleanup();
1073
- });
1074
-
1075
- it(
1076
- 'should handle text file attachment and submission',
1077
- withTestServer(
1078
- {
1079
- url: '/api/chat',
1080
- type: 'stream-values',
1081
- content: ['0:"Response to message with text attachment"\n'],
1082
- },
1083
- async ({ call }) => {
1084
- const file = new File(['test file content'], 'test.txt', {
1085
- type: 'text/plain',
1086
- });
1087
-
1088
- const fileInput = screen.getByTestId('file-input');
1089
- await userEvent.upload(fileInput, file);
1090
-
1091
- const messageInput = screen.getByTestId('message-input');
1092
- await userEvent.type(messageInput, 'Message with text attachment');
1093
-
1094
- const submitButton = screen.getByTestId('submit-button');
1095
- await userEvent.click(submitButton);
1096
-
1097
- expect(await call(0).getRequestBodyJson()).toStrictEqual({
1098
- messages: [
1099
- {
1100
- role: 'user',
1101
- content: 'Message with text attachment',
1102
- experimental_attachments: [
1103
- {
1104
- name: 'test.txt',
1105
- contentType: 'text/plain',
1106
- url: 'data:text/plain;base64,dGVzdCBmaWxlIGNvbnRlbnQ=',
1107
- },
1108
- ],
1109
- },
1110
- ],
1111
- });
1112
-
1113
- await screen.findByTestId('message-0');
1114
- expect(screen.getByTestId('message-0')).toHaveTextContent(
1115
- 'User: Message with text attachment',
1116
- );
1117
-
1118
- await screen.findByTestId('attachment-0');
1119
- expect(screen.getByTestId('attachment-0')).toHaveTextContent(
1120
- 'test file content',
1121
- );
1122
-
1123
- await screen.findByTestId('message-1');
1124
- expect(screen.getByTestId('message-1')).toHaveTextContent(
1125
- 'AI: Response to message with text attachment',
1126
- );
1127
- },
1128
- ),
1129
- );
1130
-
1131
- it(
1132
- 'should handle image file attachment and submission',
1133
- withTestServer(
1134
- {
1135
- url: '/api/chat',
1136
- type: 'stream-values',
1137
- content: ['0:"Response to message with image attachment"\n'],
1138
- },
1139
- async ({ call }) => {
1140
- const file = new File(['test image content'], 'test.png', {
1141
- type: 'image/png',
1142
- });
1143
-
1144
- const fileInput = screen.getByTestId('file-input');
1145
- await userEvent.upload(fileInput, file);
1146
-
1147
- const messageInput = screen.getByTestId('message-input');
1148
- await userEvent.type(messageInput, 'Message with image attachment');
1149
-
1150
- const submitButton = screen.getByTestId('submit-button');
1151
- await userEvent.click(submitButton);
1152
-
1153
- expect(await call(0).getRequestBodyJson()).toStrictEqual({
1154
- messages: [
1155
- {
1156
- role: 'user',
1157
- content: 'Message with image attachment',
1158
- experimental_attachments: [
1159
- {
1160
- name: 'test.png',
1161
- contentType: 'image/png',
1162
- url: 'data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50',
1163
- },
1164
- ],
1165
- },
1166
- ],
1167
- });
1168
-
1169
- await screen.findByTestId('message-0');
1170
- expect(screen.getByTestId('message-0')).toHaveTextContent(
1171
- 'User: Message with image attachment',
1172
- );
1173
-
1174
- await screen.findByTestId('attachment-0');
1175
- expect(screen.getByTestId('attachment-0')).toHaveAttribute(
1176
- 'src',
1177
- expect.stringContaining('data:image/png;base64'),
1178
- );
1179
-
1180
- await screen.findByTestId('message-1');
1181
- expect(screen.getByTestId('message-1')).toHaveTextContent(
1182
- 'AI: Response to message with image attachment',
1183
- );
1184
- },
1185
- ),
1186
- );
1187
- });
1188
-
1189
- describe('file attachments with url', () => {
1190
- const TestComponent = () => {
1191
- const { messages, handleSubmit, handleInputChange, isLoading, input } =
1192
- useChat();
1193
-
1194
- return (
1195
- <div>
1196
- {messages.map((m, idx) => (
1197
- <div data-testid={`message-${idx}`} key={m.id}>
1198
- {m.role === 'user' ? 'User: ' : 'AI: '}
1199
- {m.content}
1200
- {m.experimental_attachments?.map(attachment => {
1201
- if (attachment.contentType?.startsWith('image/')) {
1202
- return (
1203
- <img
1204
- key={attachment.name}
1205
- src={attachment.url}
1206
- alt={attachment.name}
1207
- data-testid={`attachment-${idx}`}
1208
- />
1209
- );
1210
- } else if (attachment.contentType?.startsWith('text/')) {
1211
- return (
1212
- <div key={attachment.name} data-testid={`attachment-${idx}`}>
1213
- {Buffer.from(
1214
- attachment.url.split(',')[1],
1215
- 'base64',
1216
- ).toString('utf-8')}
1217
- </div>
1218
- );
1219
- }
1220
- })}
1221
- </div>
1222
- ))}
1223
-
1224
- <form
1225
- onSubmit={event => {
1226
- handleSubmit(event, {
1227
- experimental_attachments: [
1228
- {
1229
- name: 'test.png',
1230
- contentType: 'image/png',
1231
- url: 'https://example.com/image.png',
1232
- },
1233
- ],
1234
- });
1235
- }}
1236
- data-testid="chat-form"
1237
- >
1238
- <input
1239
- value={input}
1240
- onChange={handleInputChange}
1241
- disabled={isLoading}
1242
- data-testid="message-input"
1243
- />
1244
- <button type="submit" data-testid="submit-button">
1245
- Send
1246
- </button>
1247
- </form>
1248
- </div>
1249
- );
1250
- };
1251
-
1252
- beforeEach(() => {
1253
- render(<TestComponent />);
1254
- });
1255
-
1256
- afterEach(() => {
1257
- vi.restoreAllMocks();
1258
- cleanup();
1259
- });
1260
-
1261
- it(
1262
- 'should handle image file attachment and submission',
1263
- withTestServer(
1264
- {
1265
- url: '/api/chat',
1266
- type: 'stream-values',
1267
- content: ['0:"Response to message with image attachment"\n'],
1268
- },
1269
- async ({ call }) => {
1270
- const messageInput = screen.getByTestId('message-input');
1271
- await userEvent.type(messageInput, 'Message with image attachment');
1272
-
1273
- const submitButton = screen.getByTestId('submit-button');
1274
- await userEvent.click(submitButton);
1275
-
1276
- expect(await call(0).getRequestBodyJson()).toStrictEqual({
1277
- messages: [
1278
- {
1279
- role: 'user',
1280
- content: 'Message with image attachment',
1281
- experimental_attachments: [
1282
- {
1283
- name: 'test.png',
1284
- contentType: 'image/png',
1285
- url: 'https://example.com/image.png',
1286
- },
1287
- ],
1288
- },
1289
- ],
1290
- });
1291
-
1292
- await screen.findByTestId('message-0');
1293
- expect(screen.getByTestId('message-0')).toHaveTextContent(
1294
- 'User: Message with image attachment',
1295
- );
1296
-
1297
- await screen.findByTestId('attachment-0');
1298
- expect(screen.getByTestId('attachment-0')).toHaveAttribute(
1299
- 'src',
1300
- expect.stringContaining('https://example.com/image.png'),
1301
- );
1302
-
1303
- await screen.findByTestId('message-1');
1304
- expect(screen.getByTestId('message-1')).toHaveTextContent(
1305
- 'AI: Response to message with image attachment',
1306
- );
1307
- },
1308
- ),
1309
- );
1310
- });
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
+ // });
1311
1340
 
1312
1341
  describe('reload', () => {
1313
1342
  const TestComponent = () => {