@ai-sdk/react 0.0.29 → 0.0.31
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.
- package/.turbo/turbo-build.log +9 -9
- package/.turbo/turbo-clean.log +1 -1
- package/CHANGELOG.md +16 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +30 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +30 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/use-chat.ts +30 -10
- package/src/use-chat.ui.test.tsx +215 -6
- package/src/use-completion.ts +8 -2
- package/src/use-completion.ui.test.tsx +1 -3
package/src/use-chat.ui.test.tsx
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @next/next/no-img-element */
|
|
2
2
|
import { withTestServer } from '@ai-sdk/provider-utils/test';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
formatStreamPart,
|
|
5
|
+
getTextFromDataUrl,
|
|
6
|
+
Message,
|
|
7
|
+
} from '@ai-sdk/ui-utils';
|
|
4
8
|
import '@testing-library/jest-dom/vitest';
|
|
5
9
|
import {
|
|
6
|
-
RenderResult,
|
|
7
10
|
cleanup,
|
|
8
11
|
findByText,
|
|
9
12
|
render,
|
|
@@ -15,9 +18,26 @@ import React, { useRef, useState } from 'react';
|
|
|
15
18
|
import { useChat } from './use-chat';
|
|
16
19
|
|
|
17
20
|
describe('stream data stream', () => {
|
|
21
|
+
let onFinishCalls: Array<{
|
|
22
|
+
message: Message;
|
|
23
|
+
options: {
|
|
24
|
+
finishReason: string;
|
|
25
|
+
usage: {
|
|
26
|
+
completionTokens: number;
|
|
27
|
+
promptTokens: number;
|
|
28
|
+
totalTokens: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
}> = [];
|
|
32
|
+
|
|
18
33
|
const TestComponent = () => {
|
|
19
34
|
const [id, setId] = React.useState<string>('first-id');
|
|
20
|
-
const { messages, append, error, data, isLoading } = useChat({
|
|
35
|
+
const { messages, append, error, data, isLoading } = useChat({
|
|
36
|
+
id,
|
|
37
|
+
onFinish: (message, options) => {
|
|
38
|
+
onFinishCalls.push({ message, options });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
21
41
|
|
|
22
42
|
return (
|
|
23
43
|
<div>
|
|
@@ -49,11 +69,13 @@ describe('stream data stream', () => {
|
|
|
49
69
|
|
|
50
70
|
beforeEach(() => {
|
|
51
71
|
render(<TestComponent />);
|
|
72
|
+
onFinishCalls = [];
|
|
52
73
|
});
|
|
53
74
|
|
|
54
75
|
afterEach(() => {
|
|
55
76
|
vi.restoreAllMocks();
|
|
56
77
|
cleanup();
|
|
78
|
+
onFinishCalls = [];
|
|
57
79
|
});
|
|
58
80
|
|
|
59
81
|
it(
|
|
@@ -148,6 +170,50 @@ describe('stream data stream', () => {
|
|
|
148
170
|
);
|
|
149
171
|
});
|
|
150
172
|
|
|
173
|
+
it(
|
|
174
|
+
'should invoke onFinish when the stream finishes',
|
|
175
|
+
withTestServer(
|
|
176
|
+
{
|
|
177
|
+
url: '/api/chat',
|
|
178
|
+
type: 'stream-values',
|
|
179
|
+
content: [
|
|
180
|
+
formatStreamPart('text', 'Hello'),
|
|
181
|
+
formatStreamPart('text', ','),
|
|
182
|
+
formatStreamPart('text', ' world'),
|
|
183
|
+
formatStreamPart('text', '.'),
|
|
184
|
+
formatStreamPart('finish_message', {
|
|
185
|
+
finishReason: 'stop',
|
|
186
|
+
usage: { completionTokens: 1, promptTokens: 3 },
|
|
187
|
+
}),
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
async () => {
|
|
191
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
192
|
+
|
|
193
|
+
await screen.findByTestId('message-1');
|
|
194
|
+
|
|
195
|
+
expect(onFinishCalls).toStrictEqual([
|
|
196
|
+
{
|
|
197
|
+
message: {
|
|
198
|
+
id: expect.any(String),
|
|
199
|
+
createdAt: expect.any(Date),
|
|
200
|
+
role: 'assistant',
|
|
201
|
+
content: 'Hello, world.',
|
|
202
|
+
},
|
|
203
|
+
options: {
|
|
204
|
+
finishReason: 'stop',
|
|
205
|
+
usage: {
|
|
206
|
+
completionTokens: 1,
|
|
207
|
+
promptTokens: 3,
|
|
208
|
+
totalTokens: 4,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
]);
|
|
213
|
+
},
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
|
|
151
217
|
describe('id', () => {
|
|
152
218
|
it(
|
|
153
219
|
'should clear out messages when the id changes',
|
|
@@ -175,8 +241,25 @@ describe('stream data stream', () => {
|
|
|
175
241
|
});
|
|
176
242
|
|
|
177
243
|
describe('text stream', () => {
|
|
244
|
+
let onFinishCalls: Array<{
|
|
245
|
+
message: Message;
|
|
246
|
+
options: {
|
|
247
|
+
finishReason: string;
|
|
248
|
+
usage: {
|
|
249
|
+
completionTokens: number;
|
|
250
|
+
promptTokens: number;
|
|
251
|
+
totalTokens: number;
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
}> = [];
|
|
255
|
+
|
|
178
256
|
const TestComponent = () => {
|
|
179
|
-
const { messages, append } = useChat({
|
|
257
|
+
const { messages, append } = useChat({
|
|
258
|
+
streamProtocol: 'text',
|
|
259
|
+
onFinish: (message, options) => {
|
|
260
|
+
onFinishCalls.push({ message, options });
|
|
261
|
+
},
|
|
262
|
+
});
|
|
180
263
|
|
|
181
264
|
return (
|
|
182
265
|
<div>
|
|
@@ -199,11 +282,13 @@ describe('text stream', () => {
|
|
|
199
282
|
|
|
200
283
|
beforeEach(() => {
|
|
201
284
|
render(<TestComponent />);
|
|
285
|
+
onFinishCalls = [];
|
|
202
286
|
});
|
|
203
287
|
|
|
204
288
|
afterEach(() => {
|
|
205
289
|
vi.restoreAllMocks();
|
|
206
290
|
cleanup();
|
|
291
|
+
onFinishCalls = [];
|
|
207
292
|
});
|
|
208
293
|
|
|
209
294
|
it(
|
|
@@ -229,12 +314,47 @@ describe('text stream', () => {
|
|
|
229
314
|
},
|
|
230
315
|
),
|
|
231
316
|
);
|
|
317
|
+
|
|
318
|
+
it(
|
|
319
|
+
'should invoke onFinish when the stream finishes',
|
|
320
|
+
withTestServer(
|
|
321
|
+
{
|
|
322
|
+
url: '/api/chat',
|
|
323
|
+
type: 'stream-values',
|
|
324
|
+
content: ['Hello', ',', ' world', '.'],
|
|
325
|
+
},
|
|
326
|
+
async () => {
|
|
327
|
+
await userEvent.click(screen.getByTestId('do-append-text-stream'));
|
|
328
|
+
|
|
329
|
+
await screen.findByTestId('message-1-text-stream');
|
|
330
|
+
|
|
331
|
+
expect(onFinishCalls).toStrictEqual([
|
|
332
|
+
{
|
|
333
|
+
message: {
|
|
334
|
+
id: expect.any(String),
|
|
335
|
+
createdAt: expect.any(Date),
|
|
336
|
+
role: 'assistant',
|
|
337
|
+
content: 'Hello, world.',
|
|
338
|
+
},
|
|
339
|
+
options: {
|
|
340
|
+
finishReason: 'unknown',
|
|
341
|
+
usage: {
|
|
342
|
+
completionTokens: NaN,
|
|
343
|
+
promptTokens: NaN,
|
|
344
|
+
totalTokens: NaN,
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
]);
|
|
349
|
+
},
|
|
350
|
+
),
|
|
351
|
+
);
|
|
232
352
|
});
|
|
233
353
|
|
|
234
354
|
describe('form actions', () => {
|
|
235
355
|
const TestComponent = () => {
|
|
236
356
|
const { messages, handleSubmit, handleInputChange, isLoading, input } =
|
|
237
|
-
useChat({
|
|
357
|
+
useChat({ streamProtocol: 'text' });
|
|
238
358
|
|
|
239
359
|
return (
|
|
240
360
|
<div>
|
|
@@ -307,7 +427,7 @@ describe('form actions', () => {
|
|
|
307
427
|
describe('form actions (with options)', () => {
|
|
308
428
|
const TestComponent = () => {
|
|
309
429
|
const { messages, handleSubmit, handleInputChange, isLoading, input } =
|
|
310
|
-
useChat({
|
|
430
|
+
useChat({ streamProtocol: 'text' });
|
|
311
431
|
|
|
312
432
|
return (
|
|
313
433
|
<div>
|
|
@@ -1192,3 +1312,92 @@ describe('file attachments with url', () => {
|
|
|
1192
1312
|
),
|
|
1193
1313
|
);
|
|
1194
1314
|
});
|
|
1315
|
+
|
|
1316
|
+
describe('reload', () => {
|
|
1317
|
+
const TestComponent = () => {
|
|
1318
|
+
const { messages, append, reload } = useChat();
|
|
1319
|
+
|
|
1320
|
+
return (
|
|
1321
|
+
<div>
|
|
1322
|
+
{messages.map((m, idx) => (
|
|
1323
|
+
<div data-testid={`message-${idx}`} key={m.id}>
|
|
1324
|
+
{m.role === 'user' ? 'User: ' : 'AI: '}
|
|
1325
|
+
{m.content}
|
|
1326
|
+
</div>
|
|
1327
|
+
))}
|
|
1328
|
+
|
|
1329
|
+
<button
|
|
1330
|
+
data-testid="do-append"
|
|
1331
|
+
onClick={() => {
|
|
1332
|
+
append({ role: 'user', content: 'hi' });
|
|
1333
|
+
}}
|
|
1334
|
+
/>
|
|
1335
|
+
|
|
1336
|
+
<button
|
|
1337
|
+
data-testid="do-reload"
|
|
1338
|
+
onClick={() => {
|
|
1339
|
+
reload({
|
|
1340
|
+
data: { 'test-data-key': 'test-data-value' },
|
|
1341
|
+
body: { 'request-body-key': 'request-body-value' },
|
|
1342
|
+
headers: { 'header-key': 'header-value' },
|
|
1343
|
+
});
|
|
1344
|
+
}}
|
|
1345
|
+
/>
|
|
1346
|
+
</div>
|
|
1347
|
+
);
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
beforeEach(() => {
|
|
1351
|
+
render(<TestComponent />);
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
afterEach(() => {
|
|
1355
|
+
vi.restoreAllMocks();
|
|
1356
|
+
cleanup();
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
it(
|
|
1360
|
+
'should show streamed response',
|
|
1361
|
+
withTestServer(
|
|
1362
|
+
[
|
|
1363
|
+
{
|
|
1364
|
+
url: '/api/chat',
|
|
1365
|
+
type: 'stream-values',
|
|
1366
|
+
content: ['0:"first response"\n'],
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
url: '/api/chat',
|
|
1370
|
+
type: 'stream-values',
|
|
1371
|
+
content: ['0:"second response"\n'],
|
|
1372
|
+
},
|
|
1373
|
+
],
|
|
1374
|
+
async ({ call }) => {
|
|
1375
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
1376
|
+
|
|
1377
|
+
await screen.findByTestId('message-0');
|
|
1378
|
+
expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi');
|
|
1379
|
+
|
|
1380
|
+
await screen.findByTestId('message-1');
|
|
1381
|
+
|
|
1382
|
+
// setup done, click reload:
|
|
1383
|
+
await userEvent.click(screen.getByTestId('do-reload'));
|
|
1384
|
+
|
|
1385
|
+
expect(await call(1).getRequestBodyJson()).toStrictEqual({
|
|
1386
|
+
messages: [{ content: 'hi', role: 'user' }],
|
|
1387
|
+
data: { 'test-data-key': 'test-data-value' },
|
|
1388
|
+
'request-body-key': 'request-body-value',
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
expect(call(1).getRequestHeaders()).toStrictEqual({
|
|
1392
|
+
'content-type': 'application/json',
|
|
1393
|
+
'header-key': 'header-value',
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
await screen.findByTestId('message-1');
|
|
1397
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
1398
|
+
'AI: second response',
|
|
1399
|
+
);
|
|
1400
|
+
},
|
|
1401
|
+
),
|
|
1402
|
+
);
|
|
1403
|
+
});
|
package/src/use-completion.ts
CHANGED
|
@@ -72,11 +72,17 @@ export function useCompletion({
|
|
|
72
72
|
headers,
|
|
73
73
|
body,
|
|
74
74
|
streamMode,
|
|
75
|
+
streamProtocol,
|
|
75
76
|
fetch,
|
|
76
77
|
onResponse,
|
|
77
78
|
onFinish,
|
|
78
79
|
onError,
|
|
79
80
|
}: UseCompletionOptions = {}): UseCompletionHelpers {
|
|
81
|
+
// streamMode is deprecated, use streamProtocol instead.
|
|
82
|
+
if (streamMode) {
|
|
83
|
+
streamProtocol ??= streamMode === 'text' ? 'text' : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
80
86
|
// Generate an unique id for the completion if not provided.
|
|
81
87
|
const hookId = useId();
|
|
82
88
|
const completionId = id || hookId;
|
|
@@ -126,7 +132,7 @@ export function useCompletion({
|
|
|
126
132
|
...extraMetadataRef.current.body,
|
|
127
133
|
...options?.body,
|
|
128
134
|
},
|
|
129
|
-
|
|
135
|
+
streamProtocol,
|
|
130
136
|
fetch,
|
|
131
137
|
setCompletion: completion => mutate(completion, false),
|
|
132
138
|
setLoading: mutateLoading,
|
|
@@ -150,7 +156,7 @@ export function useCompletion({
|
|
|
150
156
|
onError,
|
|
151
157
|
setError,
|
|
152
158
|
streamData,
|
|
153
|
-
|
|
159
|
+
streamProtocol,
|
|
154
160
|
fetch,
|
|
155
161
|
mutateStreamData,
|
|
156
162
|
],
|
|
@@ -124,9 +124,7 @@ describe('stream data stream', () => {
|
|
|
124
124
|
describe('text stream', () => {
|
|
125
125
|
const TestComponent = () => {
|
|
126
126
|
const { completion, handleSubmit, handleInputChange, input } =
|
|
127
|
-
useCompletion({
|
|
128
|
-
streamMode: 'text',
|
|
129
|
-
});
|
|
127
|
+
useCompletion({ streamProtocol: 'text' });
|
|
130
128
|
|
|
131
129
|
return (
|
|
132
130
|
<div>
|