@tanstack/ai-react 0.14.1 → 0.15.1
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/dist/esm/use-chat.js +47 -15
- package/dist/esm/use-chat.js.map +1 -1
- package/package.json +4 -4
- package/src/use-chat.ts +49 -19
package/dist/esm/use-chat.js
CHANGED
|
@@ -17,15 +17,19 @@ function useChat(options) {
|
|
|
17
17
|
options.initialMessages || []
|
|
18
18
|
);
|
|
19
19
|
const isFirstMountRef = useRef(true);
|
|
20
|
+
const activeClientRef = useRef(null);
|
|
21
|
+
const cleanupInvalidationRef = useRef(
|
|
22
|
+
null
|
|
23
|
+
);
|
|
20
24
|
messagesRef.current = messages;
|
|
21
25
|
const optionsRef = useRef(options);
|
|
22
26
|
optionsRef.current = options;
|
|
23
27
|
const client = useMemo(() => {
|
|
24
|
-
const messagesToUse =
|
|
28
|
+
const messagesToUse = options.initialMessages || [];
|
|
25
29
|
isFirstMountRef.current = false;
|
|
26
30
|
const initialOptions = optionsRef.current;
|
|
27
31
|
const transport = initialOptions.connection ? { connection: initialOptions.connection } : { fetcher: initialOptions.fetcher };
|
|
28
|
-
|
|
32
|
+
const instance = new ChatClient({
|
|
29
33
|
devtoolsBridgeFactory: createChatDevtoolsBridge,
|
|
30
34
|
...transport,
|
|
31
35
|
id: clientId,
|
|
@@ -34,6 +38,9 @@ function useChat(options) {
|
|
|
34
38
|
...initialOptions.forwardedProps !== void 0 && {
|
|
35
39
|
forwardedProps: initialOptions.forwardedProps
|
|
36
40
|
},
|
|
41
|
+
...initialOptions.persistence !== void 0 && {
|
|
42
|
+
persistence: initialOptions.persistence
|
|
43
|
+
},
|
|
37
44
|
...initialOptions.context !== void 0 && {
|
|
38
45
|
context: initialOptions.context
|
|
39
46
|
},
|
|
@@ -44,49 +51,69 @@ function useChat(options) {
|
|
|
44
51
|
outputKind: initialOptions.outputSchema ? "structured" : "chat"
|
|
45
52
|
},
|
|
46
53
|
onResponse: (response) => {
|
|
54
|
+
if (activeClientRef.current !== instance) return;
|
|
47
55
|
void optionsRef.current.onResponse?.(response);
|
|
48
56
|
},
|
|
49
57
|
onChunk: (chunk) => {
|
|
58
|
+
if (activeClientRef.current !== instance) return;
|
|
50
59
|
optionsRef.current.onChunk?.(chunk);
|
|
51
60
|
},
|
|
52
61
|
onFinish: (message) => {
|
|
62
|
+
if (activeClientRef.current !== instance) return;
|
|
53
63
|
optionsRef.current.onFinish?.(message);
|
|
54
64
|
},
|
|
55
65
|
onError: (error2) => {
|
|
66
|
+
if (activeClientRef.current !== instance) return;
|
|
56
67
|
optionsRef.current.onError?.(error2);
|
|
57
68
|
},
|
|
58
69
|
...initialOptions.tools !== void 0 && {
|
|
59
70
|
tools: initialOptions.tools
|
|
60
71
|
},
|
|
61
72
|
onCustomEvent: (eventType, data, context) => {
|
|
73
|
+
if (activeClientRef.current !== instance) return;
|
|
62
74
|
optionsRef.current.onCustomEvent?.(eventType, data, context);
|
|
63
75
|
},
|
|
64
76
|
...options.streamProcessor !== void 0 && {
|
|
65
77
|
streamProcessor: options.streamProcessor
|
|
66
78
|
},
|
|
67
79
|
onMessagesChange: (newMessages) => {
|
|
80
|
+
if (activeClientRef.current !== instance) return;
|
|
68
81
|
setMessages(newMessages);
|
|
69
82
|
},
|
|
70
83
|
onLoadingChange: (newIsLoading) => {
|
|
84
|
+
if (activeClientRef.current !== instance) return;
|
|
71
85
|
setIsLoading(newIsLoading);
|
|
72
86
|
},
|
|
73
87
|
onErrorChange: (newError) => {
|
|
88
|
+
if (activeClientRef.current !== instance) return;
|
|
74
89
|
setError(newError);
|
|
75
90
|
},
|
|
76
91
|
onStatusChange: (status2) => {
|
|
92
|
+
if (activeClientRef.current !== instance) return;
|
|
77
93
|
setStatus(status2);
|
|
78
94
|
},
|
|
79
95
|
onSubscriptionChange: (nextIsSubscribed) => {
|
|
96
|
+
if (activeClientRef.current !== instance) return;
|
|
80
97
|
setIsSubscribed(nextIsSubscribed);
|
|
81
98
|
},
|
|
82
99
|
onConnectionStatusChange: (nextStatus) => {
|
|
100
|
+
if (activeClientRef.current !== instance) return;
|
|
83
101
|
setConnectionStatus(nextStatus);
|
|
84
102
|
},
|
|
85
103
|
onSessionGeneratingChange: (isGenerating) => {
|
|
104
|
+
if (activeClientRef.current !== instance) return;
|
|
86
105
|
setSessionGenerating(isGenerating);
|
|
87
106
|
}
|
|
88
107
|
});
|
|
108
|
+
activeClientRef.current = instance;
|
|
109
|
+
return instance;
|
|
89
110
|
}, [clientId]);
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const clientMessages = client.getMessages();
|
|
113
|
+
if (clientMessages !== messagesRef.current) {
|
|
114
|
+
setMessages(clientMessages);
|
|
115
|
+
}
|
|
116
|
+
}, [client]);
|
|
90
117
|
useEffect(() => {
|
|
91
118
|
client.updateOptions({
|
|
92
119
|
body: options.body,
|
|
@@ -96,13 +123,6 @@ function useChat(options) {
|
|
|
96
123
|
context: options.context
|
|
97
124
|
});
|
|
98
125
|
}, [client, options.body, options.forwardedProps, options.context]);
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
if (options.initialMessages && options.initialMessages.length > 0) {
|
|
101
|
-
if (messages.length === 0) {
|
|
102
|
-
client.setMessagesManually(options.initialMessages);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}, []);
|
|
106
126
|
useEffect(() => {
|
|
107
127
|
if (options.live) {
|
|
108
128
|
client.subscribe();
|
|
@@ -111,8 +131,19 @@ function useChat(options) {
|
|
|
111
131
|
}
|
|
112
132
|
}, [client, options.live]);
|
|
113
133
|
useEffect(() => {
|
|
134
|
+
if (cleanupInvalidationRef.current) {
|
|
135
|
+
clearTimeout(cleanupInvalidationRef.current);
|
|
136
|
+
cleanupInvalidationRef.current = null;
|
|
137
|
+
}
|
|
138
|
+
activeClientRef.current = client;
|
|
114
139
|
client.mountDevtools();
|
|
115
140
|
return () => {
|
|
141
|
+
cleanupInvalidationRef.current = setTimeout(() => {
|
|
142
|
+
if (activeClientRef.current === client) {
|
|
143
|
+
activeClientRef.current = null;
|
|
144
|
+
}
|
|
145
|
+
cleanupInvalidationRef.current = null;
|
|
146
|
+
}, 0);
|
|
116
147
|
if (optionsRef.current.live) {
|
|
117
148
|
client.unsubscribe();
|
|
118
149
|
} else {
|
|
@@ -160,17 +191,18 @@ function useChat(options) {
|
|
|
160
191
|
},
|
|
161
192
|
[client]
|
|
162
193
|
);
|
|
194
|
+
const renderedMessages = client.getMessages();
|
|
163
195
|
const activeStructuredPart = useMemo(() => {
|
|
164
196
|
let lastUserIndex = -1;
|
|
165
|
-
for (let i =
|
|
166
|
-
if (
|
|
197
|
+
for (let i = renderedMessages.length - 1; i >= 0; i--) {
|
|
198
|
+
if (renderedMessages[i]?.role === "user") {
|
|
167
199
|
lastUserIndex = i;
|
|
168
200
|
break;
|
|
169
201
|
}
|
|
170
202
|
}
|
|
171
203
|
if (lastUserIndex === -1) return null;
|
|
172
|
-
for (let i =
|
|
173
|
-
const m =
|
|
204
|
+
for (let i = renderedMessages.length - 1; i > lastUserIndex; i--) {
|
|
205
|
+
const m = renderedMessages[i];
|
|
174
206
|
if (m?.role !== "assistant") continue;
|
|
175
207
|
const part = m.parts.find(
|
|
176
208
|
(p) => p.type === "structured-output"
|
|
@@ -178,7 +210,7 @@ function useChat(options) {
|
|
|
178
210
|
if (part) return part;
|
|
179
211
|
}
|
|
180
212
|
return null;
|
|
181
|
-
}, [
|
|
213
|
+
}, [renderedMessages]);
|
|
182
214
|
const partial = useMemo(() => {
|
|
183
215
|
if (!activeStructuredPart) return {};
|
|
184
216
|
const v = activeStructuredPart.partial ?? activeStructuredPart.data;
|
|
@@ -191,7 +223,7 @@ function useChat(options) {
|
|
|
191
223
|
return activeStructuredPart.data;
|
|
192
224
|
}, [activeStructuredPart]);
|
|
193
225
|
return {
|
|
194
|
-
messages,
|
|
226
|
+
messages: renderedMessages,
|
|
195
227
|
sendMessage,
|
|
196
228
|
append,
|
|
197
229
|
reload,
|
package/dist/esm/use-chat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type {\n AnyClientTool,\n InferSchemaType,\n ModelMessage,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type {\n ChatClientState,\n ConnectionStatus,\n InferredClientContext,\n StructuredOutputPart,\n} from '@tanstack/ai-client'\n\nimport type {\n DeepPartial,\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n>(\n options: UseChatOptions<TTools, TSchema, TContext>,\n): UseChatReturn<TTools, TSchema> {\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = useState(false)\n const [connectionStatus, setConnectionStatus] =\n useState<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = useState(false)\n\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n\n // Track current messages in a ref to preserve them when client is recreated\n const messagesRef = useRef<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const isFirstMountRef = useRef(true)\n\n // Update ref synchronously during render so it's always current when useMemo runs.\n messagesRef.current = messages\n\n // Track current options in a ref to avoid recreating client when options change\n const optionsRef = useRef<UseChatOptions<TTools, TSchema, TContext>>(options)\n optionsRef.current = options\n\n // Create ChatClient instance with callbacks to sync state\n const client = useMemo(() => {\n const messagesToUse = isFirstMountRef.current\n ? options.initialMessages || []\n : messagesRef.current\n\n isFirstMountRef.current = false\n\n // Build options with conditional spreads for fields whose source\n // type is `T | undefined` but the ChatClient target uses a strict\n // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects\n // assigning `undefined` to those, so we omit the key when absent.\n const initialOptions = optionsRef.current\n const transport = initialOptions.connection\n ? { connection: initialOptions.connection }\n : { fetcher: initialOptions.fetcher }\n\n return new ChatClient<TTools, TContext>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n id: clientId,\n initialMessages: messagesToUse,\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\n }),\n ...(initialOptions.context !== undefined && {\n context: initialOptions.context,\n }),\n devtools: {\n ...initialOptions.devtools,\n framework: 'react',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n onResponse: (response) => {\n void optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk: StreamChunk) => {\n optionsRef.current.onChunk?.(chunk)\n },\n onFinish: (message: UIMessage<TTools>) => {\n optionsRef.current.onFinish?.(message)\n },\n onError: (error: Error) => {\n optionsRef.current.onError?.(error)\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n onCustomEvent: (eventType, data, context) => {\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n },\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\n },\n onErrorChange: (newError: Error | undefined) => {\n setError(newError)\n },\n onStatusChange: (status: ChatClientState) => {\n setStatus(status)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n setSessionGenerating(isGenerating)\n },\n })\n }, [clientId])\n\n useEffect(() => {\n // Conditional spread: `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT.\n client.updateOptions({\n body: options.body,\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n context: options.context,\n })\n }, [client, options.body, options.forwardedProps, options.context])\n\n useEffect(() => {\n if (options.initialMessages && options.initialMessages.length > 0) {\n if (messages.length === 0) {\n client.setMessagesManually(options.initialMessages)\n }\n }\n }, [])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\n\n useEffect(() => {\n client.mountDevtools()\n\n return () => {\n // Subscribe/unsubscribe on `options.live` is owned by the dedicated\n // effect above. This cleanup only fires on unmount or client swap,\n // so read `live` through the ref to avoid disposing the client every\n // time `live` toggles.\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n client.dispose()\n }\n }, [client])\n\n const sendMessage = useCallback(\n async (content: string | MultimodalContent) => {\n await client.sendMessage(content)\n },\n [client],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n await client.append(message)\n },\n [client],\n )\n\n const reload = useCallback(async () => {\n await client.reload()\n }, [client])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n }, [client])\n\n const setMessagesManually = useCallback(\n (newMessages: Array<UIMessage<TTools>>) => {\n client.setMessagesManually(newMessages)\n },\n [client],\n )\n\n const addToolResult = useCallback(\n async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client.addToolResult(result)\n },\n [client],\n )\n\n const addToolApprovalResponse = useCallback(\n async (response: { id: string; approved: boolean }) => {\n await client.addToolApprovalResponse(response)\n },\n [client],\n )\n\n // The \"active\" structured-output part is the one on the assistant message\n // that follows the latest user message. No such message exists between\n // sendMessage() and the first chunk, so partial/final naturally read as\n // cleared. Historical parts on earlier assistant messages remain available\n // via `messages` directly.\n //\n // When there is NO user message yet (e.g. `initialMessages` contains only\n // a stale assistant turn or a system prompt) we deliberately return null\n // rather than scanning historical assistants — otherwise a `final` from a\n // previous session would leak into the hook value on first render.\n const activeStructuredPart = useMemo<StructuredOutputPart | null>(() => {\n let lastUserIndex = -1\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = messages.length - 1; i > lastUserIndex; i--) {\n const m = messages[i]\n if (m?.role !== 'assistant') continue\n const part = m.parts.find(\n (p): p is StructuredOutputPart => p.type === 'structured-output',\n )\n if (part) return part\n }\n return null\n }, [messages])\n\n const partial = useMemo<Partial>(() => {\n if (!activeStructuredPart) return {} as Partial\n const v = activeStructuredPart.partial ?? activeStructuredPart.data\n return (v ?? {}) as Partial\n }, [activeStructuredPart])\n\n const final = useMemo<Final | null>(() => {\n if (!activeStructuredPart || activeStructuredPart.status !== 'complete') {\n return null\n }\n return activeStructuredPart.data as Final\n }, [activeStructuredPart])\n\n // The runtime shape unconditionally exposes partial/final; the public\n // return type hides them when no outputSchema was supplied. TS can't\n // structurally narrow across that conditional, so the `as` is the seam.\n // eslint-disable-next-line no-restricted-syntax -- hook return shape diverges from generic UseChatReturn<TTools, TSchema> due to conditional type on TSchema; TS can't structurally narrow\n return {\n messages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema>\n}\n"],"names":["error","status"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type {\n AnyClientTool,\n InferSchemaType,\n ModelMessage,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type {\n ChatClientState,\n ConnectionStatus,\n InferredClientContext,\n StructuredOutputPart,\n} from '@tanstack/ai-client'\n\nimport type {\n DeepPartial,\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n>(\n options: UseChatOptions<TTools, TSchema, TContext>,\n): UseChatReturn<TTools, TSchema> {\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = useState(false)\n const [connectionStatus, setConnectionStatus] =\n useState<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = useState(false)\n\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n\n // Track current messages in a ref to preserve them when client is recreated\n const messagesRef = useRef<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const isFirstMountRef = useRef(true)\n const activeClientRef = useRef<ChatClient | null>(null)\n const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n\n // Update ref synchronously during render so it's always current when useMemo runs.\n messagesRef.current = messages\n\n // Track current options in a ref to avoid recreating client when options change\n const optionsRef = useRef<UseChatOptions<TTools, TSchema, TContext>>(options)\n optionsRef.current = options\n\n // Create ChatClient instance with callbacks to sync state\n const client = useMemo(() => {\n const messagesToUse = options.initialMessages || []\n isFirstMountRef.current = false\n\n // Build options with conditional spreads for fields whose source\n // type is `T | undefined` but the ChatClient target uses a strict\n // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects\n // assigning `undefined` to those, so we omit the key when absent.\n const initialOptions = optionsRef.current\n const transport = initialOptions.connection\n ? { connection: initialOptions.connection }\n : { fetcher: initialOptions.fetcher }\n\n const instance = new ChatClient<TTools, TContext>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n id: clientId,\n initialMessages: messagesToUse,\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\n }),\n ...(initialOptions.persistence !== undefined && {\n persistence: initialOptions.persistence,\n }),\n ...(initialOptions.context !== undefined && {\n context: initialOptions.context,\n }),\n devtools: {\n ...initialOptions.devtools,\n framework: 'react',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n onResponse: (response) => {\n if (activeClientRef.current !== instance) return\n void optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk: StreamChunk) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onChunk?.(chunk)\n },\n onFinish: (message: UIMessage<TTools>) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onFinish?.(message)\n },\n onError: (error: Error) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onError?.(error)\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n onCustomEvent: (eventType, data, context) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n },\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n if (activeClientRef.current !== instance) return\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsLoading(newIsLoading)\n },\n onErrorChange: (newError: Error | undefined) => {\n if (activeClientRef.current !== instance) return\n setError(newError)\n },\n onStatusChange: (status: ChatClientState) => {\n if (activeClientRef.current !== instance) return\n setStatus(status)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n if (activeClientRef.current !== instance) return\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n if (activeClientRef.current !== instance) return\n setSessionGenerating(isGenerating)\n },\n })\n activeClientRef.current = instance\n return instance\n }, [clientId])\n\n useEffect(() => {\n const clientMessages = client.getMessages()\n if (clientMessages !== messagesRef.current) {\n setMessages(clientMessages)\n }\n }, [client])\n\n useEffect(() => {\n // Conditional spread: `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT.\n client.updateOptions({\n body: options.body,\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n context: options.context,\n })\n }, [client, options.body, options.forwardedProps, options.context])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\n\n useEffect(() => {\n if (cleanupInvalidationRef.current) {\n clearTimeout(cleanupInvalidationRef.current)\n cleanupInvalidationRef.current = null\n }\n activeClientRef.current = client\n client.mountDevtools()\n\n return () => {\n cleanupInvalidationRef.current = setTimeout(() => {\n if (activeClientRef.current === client) {\n activeClientRef.current = null\n }\n cleanupInvalidationRef.current = null\n }, 0)\n // Subscribe/unsubscribe on `options.live` is owned by the dedicated\n // effect above. This cleanup only fires on unmount or client swap,\n // so read `live` through the ref to avoid disposing the client every\n // time `live` toggles.\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n client.dispose()\n }\n }, [client])\n\n const sendMessage = useCallback(\n async (content: string | MultimodalContent) => {\n await client.sendMessage(content)\n },\n [client],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n await client.append(message)\n },\n [client],\n )\n\n const reload = useCallback(async () => {\n await client.reload()\n }, [client])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n }, [client])\n\n const setMessagesManually = useCallback(\n (newMessages: Array<UIMessage<TTools>>) => {\n client.setMessagesManually(newMessages)\n },\n [client],\n )\n\n const addToolResult = useCallback(\n async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client.addToolResult(result)\n },\n [client],\n )\n\n const addToolApprovalResponse = useCallback(\n async (response: { id: string; approved: boolean }) => {\n await client.addToolApprovalResponse(response)\n },\n [client],\n )\n\n // The \"active\" structured-output part is the one on the assistant message\n // that follows the latest user message. No such message exists between\n // sendMessage() and the first chunk, so partial/final naturally read as\n // cleared. Historical parts on earlier assistant messages remain available\n // via `messages` directly.\n //\n // When there is NO user message yet (e.g. `initialMessages` contains only\n // a stale assistant turn or a system prompt) we deliberately return null\n // rather than scanning historical assistants — otherwise a `final` from a\n // previous session would leak into the hook value on first render.\n const renderedMessages = client.getMessages()\n\n const activeStructuredPart = useMemo<StructuredOutputPart | null>(() => {\n let lastUserIndex = -1\n for (let i = renderedMessages.length - 1; i >= 0; i--) {\n if (renderedMessages[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = renderedMessages.length - 1; i > lastUserIndex; i--) {\n const m = renderedMessages[i]\n if (m?.role !== 'assistant') continue\n const part = m.parts.find(\n (p): p is StructuredOutputPart => p.type === 'structured-output',\n )\n if (part) return part\n }\n return null\n }, [renderedMessages])\n\n const partial = useMemo<Partial>(() => {\n if (!activeStructuredPart) return {} as Partial\n const v = activeStructuredPart.partial ?? activeStructuredPart.data\n return (v ?? {}) as Partial\n }, [activeStructuredPart])\n\n const final = useMemo<Final | null>(() => {\n if (!activeStructuredPart || activeStructuredPart.status !== 'complete') {\n return null\n }\n return activeStructuredPart.data as Final\n }, [activeStructuredPart])\n\n // The runtime shape unconditionally exposes partial/final; the public\n // return type hides them when no outputSchema was supplied. TS can't\n // structurally narrow across that conditional, so the `as` is the seam.\n // eslint-disable-next-line no-restricted-syntax -- hook return shape diverges from generic UseChatReturn<TTools, TSchema> due to conditional type on TSchema; TS can't structurally narrow\n return {\n messages: renderedMessages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema>\n}\n"],"names":["error","status"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "React hooks for TanStack AI streaming chat, realtime voice, structured outputs, and media generation.",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
"media-generation"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@tanstack/ai-client": "0.
|
|
40
|
+
"@tanstack/ai-client": "0.16.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@types/react": ">=18.0.0",
|
|
44
44
|
"react": ">=18.0.0",
|
|
45
|
-
"@tanstack/ai": "^0.
|
|
45
|
+
"@tanstack/ai": "^0.26.1"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@standard-schema/spec": "^1.1.0",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"jsdom": "^27.2.0",
|
|
53
53
|
"react": "^19.2.3",
|
|
54
54
|
"vite": "^7.3.3",
|
|
55
|
-
"@tanstack/ai": "0.
|
|
55
|
+
"@tanstack/ai": "0.26.1"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"clean": "premove ./build ./dist",
|
package/src/use-chat.ts
CHANGED
|
@@ -52,6 +52,10 @@ export function useChat<
|
|
|
52
52
|
options.initialMessages || [],
|
|
53
53
|
)
|
|
54
54
|
const isFirstMountRef = useRef(true)
|
|
55
|
+
const activeClientRef = useRef<ChatClient | null>(null)
|
|
56
|
+
const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
57
|
+
null,
|
|
58
|
+
)
|
|
55
59
|
|
|
56
60
|
// Update ref synchronously during render so it's always current when useMemo runs.
|
|
57
61
|
messagesRef.current = messages
|
|
@@ -62,10 +66,7 @@ export function useChat<
|
|
|
62
66
|
|
|
63
67
|
// Create ChatClient instance with callbacks to sync state
|
|
64
68
|
const client = useMemo(() => {
|
|
65
|
-
const messagesToUse =
|
|
66
|
-
? options.initialMessages || []
|
|
67
|
-
: messagesRef.current
|
|
68
|
-
|
|
69
|
+
const messagesToUse = options.initialMessages || []
|
|
69
70
|
isFirstMountRef.current = false
|
|
70
71
|
|
|
71
72
|
// Build options with conditional spreads for fields whose source
|
|
@@ -77,7 +78,7 @@ export function useChat<
|
|
|
77
78
|
? { connection: initialOptions.connection }
|
|
78
79
|
: { fetcher: initialOptions.fetcher }
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
const instance = new ChatClient<TTools, TContext>({
|
|
81
82
|
devtoolsBridgeFactory: createChatDevtoolsBridge,
|
|
82
83
|
...transport,
|
|
83
84
|
id: clientId,
|
|
@@ -86,6 +87,9 @@ export function useChat<
|
|
|
86
87
|
...(initialOptions.forwardedProps !== undefined && {
|
|
87
88
|
forwardedProps: initialOptions.forwardedProps,
|
|
88
89
|
}),
|
|
90
|
+
...(initialOptions.persistence !== undefined && {
|
|
91
|
+
persistence: initialOptions.persistence,
|
|
92
|
+
}),
|
|
89
93
|
...(initialOptions.context !== undefined && {
|
|
90
94
|
context: initialOptions.context,
|
|
91
95
|
}),
|
|
@@ -96,50 +100,71 @@ export function useChat<
|
|
|
96
100
|
outputKind: initialOptions.outputSchema ? 'structured' : 'chat',
|
|
97
101
|
},
|
|
98
102
|
onResponse: (response) => {
|
|
103
|
+
if (activeClientRef.current !== instance) return
|
|
99
104
|
void optionsRef.current.onResponse?.(response)
|
|
100
105
|
},
|
|
101
106
|
onChunk: (chunk: StreamChunk) => {
|
|
107
|
+
if (activeClientRef.current !== instance) return
|
|
102
108
|
optionsRef.current.onChunk?.(chunk)
|
|
103
109
|
},
|
|
104
110
|
onFinish: (message: UIMessage<TTools>) => {
|
|
111
|
+
if (activeClientRef.current !== instance) return
|
|
105
112
|
optionsRef.current.onFinish?.(message)
|
|
106
113
|
},
|
|
107
114
|
onError: (error: Error) => {
|
|
115
|
+
if (activeClientRef.current !== instance) return
|
|
108
116
|
optionsRef.current.onError?.(error)
|
|
109
117
|
},
|
|
110
118
|
...(initialOptions.tools !== undefined && {
|
|
111
119
|
tools: initialOptions.tools,
|
|
112
120
|
}),
|
|
113
121
|
onCustomEvent: (eventType, data, context) => {
|
|
122
|
+
if (activeClientRef.current !== instance) return
|
|
114
123
|
optionsRef.current.onCustomEvent?.(eventType, data, context)
|
|
115
124
|
},
|
|
116
125
|
...(options.streamProcessor !== undefined && {
|
|
117
126
|
streamProcessor: options.streamProcessor,
|
|
118
127
|
}),
|
|
119
128
|
onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
|
|
129
|
+
if (activeClientRef.current !== instance) return
|
|
120
130
|
setMessages(newMessages)
|
|
121
131
|
},
|
|
122
132
|
onLoadingChange: (newIsLoading: boolean) => {
|
|
133
|
+
if (activeClientRef.current !== instance) return
|
|
123
134
|
setIsLoading(newIsLoading)
|
|
124
135
|
},
|
|
125
136
|
onErrorChange: (newError: Error | undefined) => {
|
|
137
|
+
if (activeClientRef.current !== instance) return
|
|
126
138
|
setError(newError)
|
|
127
139
|
},
|
|
128
140
|
onStatusChange: (status: ChatClientState) => {
|
|
141
|
+
if (activeClientRef.current !== instance) return
|
|
129
142
|
setStatus(status)
|
|
130
143
|
},
|
|
131
144
|
onSubscriptionChange: (nextIsSubscribed: boolean) => {
|
|
145
|
+
if (activeClientRef.current !== instance) return
|
|
132
146
|
setIsSubscribed(nextIsSubscribed)
|
|
133
147
|
},
|
|
134
148
|
onConnectionStatusChange: (nextStatus: ConnectionStatus) => {
|
|
149
|
+
if (activeClientRef.current !== instance) return
|
|
135
150
|
setConnectionStatus(nextStatus)
|
|
136
151
|
},
|
|
137
152
|
onSessionGeneratingChange: (isGenerating: boolean) => {
|
|
153
|
+
if (activeClientRef.current !== instance) return
|
|
138
154
|
setSessionGenerating(isGenerating)
|
|
139
155
|
},
|
|
140
156
|
})
|
|
157
|
+
activeClientRef.current = instance
|
|
158
|
+
return instance
|
|
141
159
|
}, [clientId])
|
|
142
160
|
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const clientMessages = client.getMessages()
|
|
163
|
+
if (clientMessages !== messagesRef.current) {
|
|
164
|
+
setMessages(clientMessages)
|
|
165
|
+
}
|
|
166
|
+
}, [client])
|
|
167
|
+
|
|
143
168
|
useEffect(() => {
|
|
144
169
|
// Conditional spread: `updateOptions` declares strict-optional
|
|
145
170
|
// fields and rejects explicit `undefined` under EOPT.
|
|
@@ -152,14 +177,6 @@ export function useChat<
|
|
|
152
177
|
})
|
|
153
178
|
}, [client, options.body, options.forwardedProps, options.context])
|
|
154
179
|
|
|
155
|
-
useEffect(() => {
|
|
156
|
-
if (options.initialMessages && options.initialMessages.length > 0) {
|
|
157
|
-
if (messages.length === 0) {
|
|
158
|
-
client.setMessagesManually(options.initialMessages)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}, [])
|
|
162
|
-
|
|
163
180
|
useEffect(() => {
|
|
164
181
|
if (options.live) {
|
|
165
182
|
client.subscribe()
|
|
@@ -169,9 +186,20 @@ export function useChat<
|
|
|
169
186
|
}, [client, options.live])
|
|
170
187
|
|
|
171
188
|
useEffect(() => {
|
|
189
|
+
if (cleanupInvalidationRef.current) {
|
|
190
|
+
clearTimeout(cleanupInvalidationRef.current)
|
|
191
|
+
cleanupInvalidationRef.current = null
|
|
192
|
+
}
|
|
193
|
+
activeClientRef.current = client
|
|
172
194
|
client.mountDevtools()
|
|
173
195
|
|
|
174
196
|
return () => {
|
|
197
|
+
cleanupInvalidationRef.current = setTimeout(() => {
|
|
198
|
+
if (activeClientRef.current === client) {
|
|
199
|
+
activeClientRef.current = null
|
|
200
|
+
}
|
|
201
|
+
cleanupInvalidationRef.current = null
|
|
202
|
+
}, 0)
|
|
175
203
|
// Subscribe/unsubscribe on `options.live` is owned by the dedicated
|
|
176
204
|
// effect above. This cleanup only fires on unmount or client swap,
|
|
177
205
|
// so read `live` through the ref to avoid disposing the client every
|
|
@@ -248,17 +276,19 @@ export function useChat<
|
|
|
248
276
|
// a stale assistant turn or a system prompt) we deliberately return null
|
|
249
277
|
// rather than scanning historical assistants — otherwise a `final` from a
|
|
250
278
|
// previous session would leak into the hook value on first render.
|
|
279
|
+
const renderedMessages = client.getMessages()
|
|
280
|
+
|
|
251
281
|
const activeStructuredPart = useMemo<StructuredOutputPart | null>(() => {
|
|
252
282
|
let lastUserIndex = -1
|
|
253
|
-
for (let i =
|
|
254
|
-
if (
|
|
283
|
+
for (let i = renderedMessages.length - 1; i >= 0; i--) {
|
|
284
|
+
if (renderedMessages[i]?.role === 'user') {
|
|
255
285
|
lastUserIndex = i
|
|
256
286
|
break
|
|
257
287
|
}
|
|
258
288
|
}
|
|
259
289
|
if (lastUserIndex === -1) return null
|
|
260
|
-
for (let i =
|
|
261
|
-
const m =
|
|
290
|
+
for (let i = renderedMessages.length - 1; i > lastUserIndex; i--) {
|
|
291
|
+
const m = renderedMessages[i]
|
|
262
292
|
if (m?.role !== 'assistant') continue
|
|
263
293
|
const part = m.parts.find(
|
|
264
294
|
(p): p is StructuredOutputPart => p.type === 'structured-output',
|
|
@@ -266,7 +296,7 @@ export function useChat<
|
|
|
266
296
|
if (part) return part
|
|
267
297
|
}
|
|
268
298
|
return null
|
|
269
|
-
}, [
|
|
299
|
+
}, [renderedMessages])
|
|
270
300
|
|
|
271
301
|
const partial = useMemo<Partial>(() => {
|
|
272
302
|
if (!activeStructuredPart) return {} as Partial
|
|
@@ -286,7 +316,7 @@ export function useChat<
|
|
|
286
316
|
// structurally narrow across that conditional, so the `as` is the seam.
|
|
287
317
|
// eslint-disable-next-line no-restricted-syntax -- hook return shape diverges from generic UseChatReturn<TTools, TSchema> due to conditional type on TSchema; TS can't structurally narrow
|
|
288
318
|
return {
|
|
289
|
-
messages,
|
|
319
|
+
messages: renderedMessages,
|
|
290
320
|
sendMessage,
|
|
291
321
|
append,
|
|
292
322
|
reload,
|