@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.
@@ -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 { formatStreamPart, getTextFromDataUrl } from '@ai-sdk/ui-utils';
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({ id });
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({ streamMode: 'text' });
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({ streamMode: 'text' });
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({ streamMode: 'text' });
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
+ });
@@ -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
- streamMode,
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
- streamMode,
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>