@ai-sdk/react 3.0.47 → 3.0.48
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/CHANGELOG.md +7 -0
- package/package.json +4 -3
- package/src/chat.react.ts +130 -0
- package/src/index.ts +4 -0
- package/src/setup-test-component.tsx +26 -0
- package/src/throttle.ts +8 -0
- package/src/use-chat.ts +173 -0
- package/src/use-chat.ui.test.tsx +2532 -0
- package/src/use-completion.ts +208 -0
- package/src/use-completion.ui.test.tsx +165 -0
- package/src/use-object.ts +269 -0
- package/src/use-object.ui.test.tsx +389 -0
- package/src/util/use-stable-value.ts +18 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/react",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.48",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist/**/*",
|
|
19
|
+
"src",
|
|
19
20
|
"CHANGELOG.md",
|
|
20
21
|
"README.md"
|
|
21
22
|
],
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
"swr": "^2.2.5",
|
|
24
25
|
"throttleit": "2.1.0",
|
|
25
26
|
"@ai-sdk/provider-utils": "4.0.8",
|
|
26
|
-
"ai": "6.0.
|
|
27
|
+
"ai": "6.0.46"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@testing-library/jest-dom": "^6.6.3",
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
"tsup": "^7.2.0",
|
|
40
41
|
"typescript": "5.8.3",
|
|
41
42
|
"zod": "3.25.76",
|
|
42
|
-
"@ai-sdk/test-server": "1.0.
|
|
43
|
+
"@ai-sdk/test-server": "1.0.2",
|
|
43
44
|
"@vercel/ai-tsconfig": "0.0.0",
|
|
44
45
|
"eslint-config-vercel-ai": "0.0.0"
|
|
45
46
|
},
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { AbstractChat, ChatInit, ChatState, ChatStatus, UIMessage } from 'ai';
|
|
2
|
+
import { throttle } from './throttle';
|
|
3
|
+
|
|
4
|
+
class ReactChatState<UI_MESSAGE extends UIMessage>
|
|
5
|
+
implements ChatState<UI_MESSAGE>
|
|
6
|
+
{
|
|
7
|
+
#messages: UI_MESSAGE[];
|
|
8
|
+
#status: ChatStatus = 'ready';
|
|
9
|
+
#error: Error | undefined = undefined;
|
|
10
|
+
|
|
11
|
+
#messagesCallbacks = new Set<() => void>();
|
|
12
|
+
#statusCallbacks = new Set<() => void>();
|
|
13
|
+
#errorCallbacks = new Set<() => void>();
|
|
14
|
+
|
|
15
|
+
constructor(initialMessages: UI_MESSAGE[] = []) {
|
|
16
|
+
this.#messages = initialMessages;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get status(): ChatStatus {
|
|
20
|
+
return this.#status;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set status(newStatus: ChatStatus) {
|
|
24
|
+
this.#status = newStatus;
|
|
25
|
+
this.#callStatusCallbacks();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get error(): Error | undefined {
|
|
29
|
+
return this.#error;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
set error(newError: Error | undefined) {
|
|
33
|
+
this.#error = newError;
|
|
34
|
+
this.#callErrorCallbacks();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get messages(): UI_MESSAGE[] {
|
|
38
|
+
return this.#messages;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
set messages(newMessages: UI_MESSAGE[]) {
|
|
42
|
+
this.#messages = [...newMessages];
|
|
43
|
+
this.#callMessagesCallbacks();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pushMessage = (message: UI_MESSAGE) => {
|
|
47
|
+
this.#messages = this.#messages.concat(message);
|
|
48
|
+
this.#callMessagesCallbacks();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
popMessage = () => {
|
|
52
|
+
this.#messages = this.#messages.slice(0, -1);
|
|
53
|
+
this.#callMessagesCallbacks();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
replaceMessage = (index: number, message: UI_MESSAGE) => {
|
|
57
|
+
this.#messages = [
|
|
58
|
+
...this.#messages.slice(0, index),
|
|
59
|
+
// We deep clone the message here to ensure the new React Compiler (currently in RC) detects deeply nested parts/metadata changes:
|
|
60
|
+
this.snapshot(message),
|
|
61
|
+
...this.#messages.slice(index + 1),
|
|
62
|
+
];
|
|
63
|
+
this.#callMessagesCallbacks();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
snapshot = <T>(value: T): T => structuredClone(value);
|
|
67
|
+
|
|
68
|
+
'~registerMessagesCallback' = (
|
|
69
|
+
onChange: () => void,
|
|
70
|
+
throttleWaitMs?: number,
|
|
71
|
+
): (() => void) => {
|
|
72
|
+
const callback = throttleWaitMs
|
|
73
|
+
? throttle(onChange, throttleWaitMs)
|
|
74
|
+
: onChange;
|
|
75
|
+
this.#messagesCallbacks.add(callback);
|
|
76
|
+
return () => {
|
|
77
|
+
this.#messagesCallbacks.delete(callback);
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
'~registerStatusCallback' = (onChange: () => void): (() => void) => {
|
|
82
|
+
this.#statusCallbacks.add(onChange);
|
|
83
|
+
return () => {
|
|
84
|
+
this.#statusCallbacks.delete(onChange);
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
'~registerErrorCallback' = (onChange: () => void): (() => void) => {
|
|
89
|
+
this.#errorCallbacks.add(onChange);
|
|
90
|
+
return () => {
|
|
91
|
+
this.#errorCallbacks.delete(onChange);
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
#callMessagesCallbacks = () => {
|
|
96
|
+
this.#messagesCallbacks.forEach(callback => callback());
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
#callStatusCallbacks = () => {
|
|
100
|
+
this.#statusCallbacks.forEach(callback => callback());
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
#callErrorCallbacks = () => {
|
|
104
|
+
this.#errorCallbacks.forEach(callback => callback());
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class Chat<
|
|
109
|
+
UI_MESSAGE extends UIMessage,
|
|
110
|
+
> extends AbstractChat<UI_MESSAGE> {
|
|
111
|
+
#state: ReactChatState<UI_MESSAGE>;
|
|
112
|
+
|
|
113
|
+
constructor({ messages, ...init }: ChatInit<UI_MESSAGE>) {
|
|
114
|
+
const state = new ReactChatState(messages);
|
|
115
|
+
super({ ...init, state });
|
|
116
|
+
this.#state = state;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
'~registerMessagesCallback' = (
|
|
120
|
+
onChange: () => void,
|
|
121
|
+
throttleWaitMs?: number,
|
|
122
|
+
): (() => void) =>
|
|
123
|
+
this.#state['~registerMessagesCallback'](onChange, throttleWaitMs);
|
|
124
|
+
|
|
125
|
+
'~registerStatusCallback' = (onChange: () => void): (() => void) =>
|
|
126
|
+
this.#state['~registerStatusCallback'](onChange);
|
|
127
|
+
|
|
128
|
+
'~registerErrorCallback' = (onChange: () => void): (() => void) =>
|
|
129
|
+
this.#state['~registerErrorCallback'](onChange);
|
|
130
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { cleanup, render } from '@testing-library/react';
|
|
2
|
+
import { SWRConfig } from 'swr';
|
|
3
|
+
import { beforeEach, afterEach, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
export const setupTestComponent = (
|
|
6
|
+
TestComponent: React.ComponentType<any>,
|
|
7
|
+
{
|
|
8
|
+
init,
|
|
9
|
+
}: {
|
|
10
|
+
init?: (TestComponent: React.ComponentType<any>) => React.ReactNode;
|
|
11
|
+
} = {},
|
|
12
|
+
) => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// reset SWR cache to isolate tests:
|
|
15
|
+
render(
|
|
16
|
+
<SWRConfig value={{ provider: () => new Map() }}>
|
|
17
|
+
{init?.(TestComponent) ?? <TestComponent />}
|
|
18
|
+
</SWRConfig>,
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
cleanup();
|
|
25
|
+
});
|
|
26
|
+
};
|
package/src/throttle.ts
ADDED
package/src/use-chat.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AbstractChat,
|
|
3
|
+
ChatInit,
|
|
4
|
+
type CreateUIMessage,
|
|
5
|
+
type UIMessage,
|
|
6
|
+
} from 'ai';
|
|
7
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react';
|
|
8
|
+
import { Chat } from './chat.react';
|
|
9
|
+
|
|
10
|
+
export type { CreateUIMessage, UIMessage };
|
|
11
|
+
|
|
12
|
+
export type UseChatHelpers<UI_MESSAGE extends UIMessage> = {
|
|
13
|
+
/**
|
|
14
|
+
* The id of the chat.
|
|
15
|
+
*/
|
|
16
|
+
readonly id: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Update the `messages` state locally. This is useful when you want to
|
|
20
|
+
* edit the messages on the client, and then trigger the `reload` method
|
|
21
|
+
* manually to regenerate the AI response.
|
|
22
|
+
*/
|
|
23
|
+
setMessages: (
|
|
24
|
+
messages: UI_MESSAGE[] | ((messages: UI_MESSAGE[]) => UI_MESSAGE[]),
|
|
25
|
+
) => void;
|
|
26
|
+
|
|
27
|
+
error: Error | undefined;
|
|
28
|
+
} & Pick<
|
|
29
|
+
AbstractChat<UI_MESSAGE>,
|
|
30
|
+
| 'sendMessage'
|
|
31
|
+
| 'regenerate'
|
|
32
|
+
| 'stop'
|
|
33
|
+
| 'resumeStream'
|
|
34
|
+
| 'addToolResult'
|
|
35
|
+
| 'addToolOutput'
|
|
36
|
+
| 'addToolApprovalResponse'
|
|
37
|
+
| 'status'
|
|
38
|
+
| 'messages'
|
|
39
|
+
| 'clearError'
|
|
40
|
+
>;
|
|
41
|
+
|
|
42
|
+
export type UseChatOptions<UI_MESSAGE extends UIMessage> = (
|
|
43
|
+
| { chat: Chat<UI_MESSAGE> }
|
|
44
|
+
| ChatInit<UI_MESSAGE>
|
|
45
|
+
) & {
|
|
46
|
+
/**
|
|
47
|
+
Custom throttle wait in ms for the chat messages and data updates.
|
|
48
|
+
Default is undefined, which disables throttling.
|
|
49
|
+
*/
|
|
50
|
+
experimental_throttle?: number;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether to resume an ongoing chat generation stream.
|
|
54
|
+
*/
|
|
55
|
+
resume?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function useChat<UI_MESSAGE extends UIMessage = UIMessage>({
|
|
59
|
+
experimental_throttle: throttleWaitMs,
|
|
60
|
+
resume = false,
|
|
61
|
+
...options
|
|
62
|
+
}: UseChatOptions<UI_MESSAGE> = {}): UseChatHelpers<UI_MESSAGE> {
|
|
63
|
+
// Create a single ref for all callbacks to avoid stale closures
|
|
64
|
+
const callbacksRef = useRef(
|
|
65
|
+
!('chat' in options)
|
|
66
|
+
? {
|
|
67
|
+
onToolCall: options.onToolCall,
|
|
68
|
+
onData: options.onData,
|
|
69
|
+
onFinish: options.onFinish,
|
|
70
|
+
onError: options.onError,
|
|
71
|
+
sendAutomaticallyWhen: options.sendAutomaticallyWhen,
|
|
72
|
+
}
|
|
73
|
+
: {},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Update callbacks ref on each render to keep them current
|
|
77
|
+
if (!('chat' in options)) {
|
|
78
|
+
callbacksRef.current = {
|
|
79
|
+
onToolCall: options.onToolCall,
|
|
80
|
+
onData: options.onData,
|
|
81
|
+
onFinish: options.onFinish,
|
|
82
|
+
onError: options.onError,
|
|
83
|
+
sendAutomaticallyWhen: options.sendAutomaticallyWhen,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Ensure the Chat instance has the latest callbacks
|
|
88
|
+
const optionsWithCallbacks: typeof options = {
|
|
89
|
+
...options,
|
|
90
|
+
onToolCall: arg => callbacksRef.current.onToolCall?.(arg),
|
|
91
|
+
onData: arg => callbacksRef.current.onData?.(arg),
|
|
92
|
+
onFinish: arg => callbacksRef.current.onFinish?.(arg),
|
|
93
|
+
onError: arg => callbacksRef.current.onError?.(arg),
|
|
94
|
+
sendAutomaticallyWhen: arg =>
|
|
95
|
+
callbacksRef.current.sendAutomaticallyWhen?.(arg) ?? false,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const chatRef = useRef<Chat<UI_MESSAGE>>(
|
|
99
|
+
'chat' in options ? options.chat : new Chat(optionsWithCallbacks),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const shouldRecreateChat =
|
|
103
|
+
('chat' in options && options.chat !== chatRef.current) ||
|
|
104
|
+
('id' in options && chatRef.current.id !== options.id);
|
|
105
|
+
|
|
106
|
+
if (shouldRecreateChat) {
|
|
107
|
+
chatRef.current =
|
|
108
|
+
'chat' in options ? options.chat : new Chat(optionsWithCallbacks);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const subscribeToMessages = useCallback(
|
|
112
|
+
(update: () => void) =>
|
|
113
|
+
chatRef.current['~registerMessagesCallback'](update, throttleWaitMs),
|
|
114
|
+
// `chatRef.current.id` is required to trigger re-subscription when the chat ID changes
|
|
115
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
116
|
+
[throttleWaitMs, chatRef.current.id],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const messages = useSyncExternalStore(
|
|
120
|
+
subscribeToMessages,
|
|
121
|
+
() => chatRef.current.messages,
|
|
122
|
+
() => chatRef.current.messages,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const status = useSyncExternalStore(
|
|
126
|
+
chatRef.current['~registerStatusCallback'],
|
|
127
|
+
() => chatRef.current.status,
|
|
128
|
+
() => chatRef.current.status,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const error = useSyncExternalStore(
|
|
132
|
+
chatRef.current['~registerErrorCallback'],
|
|
133
|
+
() => chatRef.current.error,
|
|
134
|
+
() => chatRef.current.error,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const setMessages = useCallback(
|
|
138
|
+
(
|
|
139
|
+
messagesParam: UI_MESSAGE[] | ((messages: UI_MESSAGE[]) => UI_MESSAGE[]),
|
|
140
|
+
) => {
|
|
141
|
+
if (typeof messagesParam === 'function') {
|
|
142
|
+
messagesParam = messagesParam(chatRef.current.messages);
|
|
143
|
+
}
|
|
144
|
+
chatRef.current.messages = messagesParam;
|
|
145
|
+
},
|
|
146
|
+
[chatRef],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (resume) {
|
|
151
|
+
chatRef.current.resumeStream();
|
|
152
|
+
}
|
|
153
|
+
}, [resume, chatRef]);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
id: chatRef.current.id,
|
|
157
|
+
messages,
|
|
158
|
+
setMessages,
|
|
159
|
+
sendMessage: chatRef.current.sendMessage,
|
|
160
|
+
regenerate: chatRef.current.regenerate,
|
|
161
|
+
clearError: chatRef.current.clearError,
|
|
162
|
+
stop: chatRef.current.stop,
|
|
163
|
+
error,
|
|
164
|
+
resumeStream: chatRef.current.resumeStream,
|
|
165
|
+
status,
|
|
166
|
+
/**
|
|
167
|
+
* @deprecated Use `addToolOutput` instead.
|
|
168
|
+
*/
|
|
169
|
+
addToolResult: chatRef.current.addToolOutput,
|
|
170
|
+
addToolOutput: chatRef.current.addToolOutput,
|
|
171
|
+
addToolApprovalResponse: chatRef.current.addToolApprovalResponse,
|
|
172
|
+
};
|
|
173
|
+
}
|