@ai-sdk/react 3.0.47 → 3.0.49

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # @ai-sdk/react
2
2
 
3
+ ## 3.0.49
4
+
5
+ ### Patch Changes
6
+
7
+ - ai@6.0.47
8
+
9
+ ## 3.0.48
10
+
11
+ ### Patch Changes
12
+
13
+ - 8dc54db: chore: add src folders to package bundle
14
+ - ai@6.0.46
15
+
3
16
  ## 3.0.47
4
17
 
5
18
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/react",
3
- "version": "3.0.47",
3
+ "version": "3.0.49",
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.45"
27
+ "ai": "6.0.47"
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.1",
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,4 @@
1
+ export * from './use-chat';
2
+ export { Chat } from './chat.react';
3
+ export * from './use-completion';
4
+ export * from './use-object';
@@ -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
+ };
@@ -0,0 +1,8 @@
1
+ import throttleFunction from 'throttleit';
2
+
3
+ export function throttle<T extends (...args: any[]) => any>(
4
+ fn: T,
5
+ waitMs: number | undefined,
6
+ ): T {
7
+ return waitMs != null ? throttleFunction(fn, waitMs) : fn;
8
+ }
@@ -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
+ }