@tanstack/ai-react 0.8.2 → 0.10.0
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/index.d.ts +1 -1
- package/dist/esm/types.d.ts +45 -3
- package/dist/esm/use-chat.d.ts +2 -2
- package/dist/esm/use-chat.js +32 -4
- package/dist/esm/use-chat.js.map +1 -1
- package/package.json +5 -4
- package/src/index.ts +1 -0
- package/src/types.ts +79 -21
- package/src/use-chat.ts +71 -11
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { useChat } from './use-chat.js';
|
|
2
2
|
export { useRealtimeChat } from './use-realtime-chat.js';
|
|
3
|
-
export type { UseChatOptions, UseChatReturn, UIMessage, ChatRequestBody, } from './types.js';
|
|
3
|
+
export type { DeepPartial, UseChatOptions, UseChatReturn, UIMessage, ChatRequestBody, } from './types.js';
|
|
4
4
|
export type { UseRealtimeChatOptions, UseRealtimeChatReturn, } from './realtime-types.js';
|
|
5
5
|
export { useGeneration } from './use-generation.js';
|
|
6
6
|
export type { UseGenerationOptions, UseGenerationReturn, } from './use-generation.js';
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { AnyClientTool, ModelMessage } from '@tanstack/ai';
|
|
1
|
+
import { AnyClientTool, InferSchemaType, ModelMessage, SchemaInput } from '@tanstack/ai';
|
|
2
2
|
import { ChatClientOptions, ChatClientState, ChatRequestBody, ConnectionStatus, MultimodalContent, UIMessage } from '@tanstack/ai-client';
|
|
3
3
|
export type { ChatRequestBody, MultimodalContent, UIMessage };
|
|
4
|
+
/**
|
|
5
|
+
* Recursive partial — every property and every nested array element is optional.
|
|
6
|
+
* Used to type the in-flight `partial` value the hook exposes while a structured
|
|
7
|
+
* output stream is still arriving (the JSON has shape but is incomplete).
|
|
8
|
+
*/
|
|
9
|
+
export type DeepPartial<T> = T extends ReadonlyArray<infer U> ? Array<DeepPartial<U>> : T extends object ? {
|
|
10
|
+
[K in keyof T]?: DeepPartial<T[K]>;
|
|
11
|
+
} : T;
|
|
4
12
|
/**
|
|
5
13
|
* Options for the useChat hook.
|
|
6
14
|
*
|
|
@@ -14,17 +22,51 @@ export type { ChatRequestBody, MultimodalContent, UIMessage };
|
|
|
14
22
|
* All other callbacks (onResponse, onChunk, onFinish, onError) are
|
|
15
23
|
* passed through to the underlying ChatClient and can be used for side effects.
|
|
16
24
|
*
|
|
25
|
+
* When `outputSchema` is supplied, the hook returns a typed `partial` (live
|
|
26
|
+
* progressive object, updated from `TEXT_MESSAGE_CONTENT` deltas via
|
|
27
|
+
* `parsePartialJSON`) and `final` (validated terminal payload from the
|
|
28
|
+
* `structured-output.complete` event). The schema is used purely for type
|
|
29
|
+
* inference on the client — server-side validation still runs against the
|
|
30
|
+
* schema you pass to `chat({ outputSchema })` on the server route.
|
|
31
|
+
*
|
|
17
32
|
* Note: Connection and body changes will recreate the ChatClient instance.
|
|
18
33
|
* To update these options, remount the component or use a key prop.
|
|
19
34
|
*/
|
|
20
|
-
export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange'> & {
|
|
35
|
+
export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange'> & {
|
|
21
36
|
/**
|
|
22
37
|
* Opt into mount-time live subscription behavior.
|
|
23
38
|
* When enabled, the hook subscribes on mount and unsubscribes on unmount.
|
|
24
39
|
*/
|
|
25
40
|
live?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Standard-schema-compatible schema (Zod, Valibot, ArkType, or a plain JSON
|
|
43
|
+
* Schema). Used to infer the shape of `partial` and `final` in the return.
|
|
44
|
+
* The schema is **not** sent to the server — server-side validation runs
|
|
45
|
+
* against the schema passed to `chat({ outputSchema })` on the server route.
|
|
46
|
+
*/
|
|
47
|
+
outputSchema?: TSchema;
|
|
26
48
|
};
|
|
27
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Discriminated return shape: when `outputSchema` is supplied, the hook adds
|
|
51
|
+
* typed `partial` / `final` fields; when it is omitted (default), the return
|
|
52
|
+
* is unchanged.
|
|
53
|
+
*/
|
|
54
|
+
export type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = BaseUseChatReturn<TTools> & (TSchema extends SchemaInput ? {
|
|
55
|
+
/**
|
|
56
|
+
* Live, progressively-parsed structured output. Updated from
|
|
57
|
+
* `TEXT_MESSAGE_CONTENT` deltas via `parsePartialJSON` while the stream
|
|
58
|
+
* is still arriving, and snapped to the validated payload when
|
|
59
|
+
* `structured-output.complete` fires. Resets on every new run
|
|
60
|
+
* (`sendMessage` / `reload`).
|
|
61
|
+
*/
|
|
62
|
+
partial: DeepPartial<InferSchemaType<TSchema>>;
|
|
63
|
+
/**
|
|
64
|
+
* Final, schema-validated structured output. `null` until the terminal
|
|
65
|
+
* `structured-output.complete` event arrives. Resets on every new run.
|
|
66
|
+
*/
|
|
67
|
+
final: InferSchemaType<TSchema> | null;
|
|
68
|
+
} : Record<never, never>);
|
|
69
|
+
interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
28
70
|
/**
|
|
29
71
|
* Current messages in the conversation
|
|
30
72
|
*/
|
package/dist/esm/use-chat.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { AnyClientTool } from '@tanstack/ai';
|
|
1
|
+
import { AnyClientTool, SchemaInput } from '@tanstack/ai';
|
|
2
2
|
import { UseChatOptions, UseChatReturn } from './types.js';
|
|
3
|
-
export declare function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(options: UseChatOptions<TTools>): UseChatReturn<TTools>;
|
|
3
|
+
export declare function useChat<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined>(options: UseChatOptions<TTools, TSchema>): UseChatReturn<TTools, TSchema>;
|
package/dist/esm/use-chat.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChatClient } from "@tanstack/ai-client";
|
|
2
|
+
import { parsePartialJSON } from "@tanstack/ai";
|
|
2
3
|
import { useId, useState, useRef, useMemo, useEffect, useCallback } from "react";
|
|
3
4
|
function useChat(options) {
|
|
4
5
|
const hookId = useId();
|
|
@@ -12,6 +13,9 @@ function useChat(options) {
|
|
|
12
13
|
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
13
14
|
const [connectionStatus, setConnectionStatus] = useState("disconnected");
|
|
14
15
|
const [sessionGenerating, setSessionGenerating] = useState(false);
|
|
16
|
+
const [partial, setPartial] = useState({});
|
|
17
|
+
const [final, setFinal] = useState(null);
|
|
18
|
+
const rawJsonRef = useRef("");
|
|
15
19
|
const messagesRef = useRef(
|
|
16
20
|
options.initialMessages || []
|
|
17
21
|
);
|
|
@@ -27,11 +31,30 @@ function useChat(options) {
|
|
|
27
31
|
id: clientId,
|
|
28
32
|
initialMessages: messagesToUse,
|
|
29
33
|
body: optionsRef.current.body,
|
|
34
|
+
forwardedProps: optionsRef.current.forwardedProps,
|
|
30
35
|
// Wrap every callback so the latest options are read at call time.
|
|
31
36
|
// Capturing the function reference directly would freeze it to whatever
|
|
32
37
|
// the parent passed on the first render.
|
|
33
38
|
onResponse: (response) => optionsRef.current.onResponse?.(response),
|
|
34
|
-
onChunk: (chunk) =>
|
|
39
|
+
onChunk: (chunk) => {
|
|
40
|
+
if (optionsRef.current.outputSchema !== void 0) {
|
|
41
|
+
if (chunk.type === "RUN_STARTED") {
|
|
42
|
+
rawJsonRef.current = "";
|
|
43
|
+
setPartial({});
|
|
44
|
+
setFinal(null);
|
|
45
|
+
} else if (chunk.type === "TEXT_MESSAGE_CONTENT" && chunk.delta) {
|
|
46
|
+
rawJsonRef.current += chunk.delta;
|
|
47
|
+
const progressive = parsePartialJSON(rawJsonRef.current);
|
|
48
|
+
if (progressive && typeof progressive === "object") {
|
|
49
|
+
setPartial(progressive);
|
|
50
|
+
}
|
|
51
|
+
} else if (chunk.type === "CUSTOM" && chunk.name === "structured-output.complete") {
|
|
52
|
+
const value = chunk.value;
|
|
53
|
+
setFinal(value.object);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
optionsRef.current.onChunk?.(chunk);
|
|
57
|
+
},
|
|
35
58
|
onFinish: (message) => {
|
|
36
59
|
optionsRef.current.onFinish?.(message);
|
|
37
60
|
},
|
|
@@ -65,8 +88,11 @@ function useChat(options) {
|
|
|
65
88
|
});
|
|
66
89
|
}, [clientId]);
|
|
67
90
|
useEffect(() => {
|
|
68
|
-
client.updateOptions({
|
|
69
|
-
|
|
91
|
+
client.updateOptions({
|
|
92
|
+
body: options.body,
|
|
93
|
+
forwardedProps: options.forwardedProps
|
|
94
|
+
});
|
|
95
|
+
}, [client, options.body, options.forwardedProps]);
|
|
70
96
|
useEffect(() => {
|
|
71
97
|
if (options.initialMessages && options.initialMessages.length > 0) {
|
|
72
98
|
if (messages.length === 0) {
|
|
@@ -144,7 +170,9 @@ function useChat(options) {
|
|
|
144
170
|
setMessages: setMessagesManually,
|
|
145
171
|
clear,
|
|
146
172
|
addToolResult,
|
|
147
|
-
addToolApprovalResponse
|
|
173
|
+
addToolApprovalResponse,
|
|
174
|
+
partial,
|
|
175
|
+
final
|
|
148
176
|
};
|
|
149
177
|
}
|
|
150
178
|
export {
|
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 { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type { AnyClientTool, ModelMessage } from '@tanstack/ai'\nimport type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'\n\nimport type {\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(\n options: UseChatOptions<TTools>,\n): UseChatReturn<TTools> {\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 // 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 // A useEffect here would be async and messagesRef could be stale on client recreation.\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>>(options)\n optionsRef.current = options\n\n // Create ChatClient instance with callbacks to sync state\n // Note: Options are captured at client creation time.\n // The connection adapter can use functions for dynamic values (url, headers, etc.)\n // which are evaluated lazily on each request.\n const client = useMemo(() => {\n // On first mount, use initialMessages. On subsequent recreations, preserve existing messages.\n const messagesToUse = isFirstMountRef.current\n ? options.initialMessages || []\n : messagesRef.current\n\n isFirstMountRef.current = false\n\n return new ChatClient({\n connection: optionsRef.current.connection,\n id: clientId,\n initialMessages: messagesToUse,\n body: optionsRef.current.body,\n // Wrap every callback so the latest options are read at call time.\n // Capturing the function reference directly would freeze it to whatever\n // the parent passed on the first render.\n onResponse: (response) => optionsRef.current.onResponse?.(response),\n onChunk: (chunk) => optionsRef.current.onChunk?.(chunk),\n onFinish: (message: UIMessage<TTools>) => {\n optionsRef.current.onFinish?.(message)\n },\n onError: (error: Error) => {\n optionsRef.current.onError?.(error)\n },\n tools: optionsRef.current.tools,\n onCustomEvent: (eventType, data, context) =>\n optionsRef.current.onCustomEvent?.(eventType, data, context),\n streamProcessor: options.streamProcessor,\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 // Sync body changes to the client\n // This allows dynamic body values (like model selection) to be updated without recreating the client\n useEffect(() => {\n client.updateOptions({ body: options.body })\n }, [client, options.body])\n\n // Sync initial messages on mount only\n // Note: initialMessages are passed to ChatClient constructor, but we also\n // set them here to ensure React state is in sync\n useEffect(() => {\n if (options.initialMessages && options.initialMessages.length > 0) {\n // Only set if current messages are empty (initial state)\n if (messages.length === 0) {\n client.setMessagesManually(options.initialMessages)\n }\n }\n }, []) // Only run on mount - initialMessages are handled by ChatClient constructor\n\n // Keep connection lifecycle opt-in and explicit.\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\n\n // Cleanup on unmount: stop any in-flight requests\n // Note: We only cleanup when client changes or component unmounts.\n // DO NOT include isLoading in dependencies - that would cause the cleanup\n // to run when isLoading changes, aborting continuation requests.\n useEffect(() => {\n return () => {\n // live mode owns the connection lifecycle; non-live keeps request-only stop.\n if (options.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n }\n }, [client, options.live])\n\n // All callback options are read through optionsRef at call time, so fresh\n // closures from each render are picked up without recreating the 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 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 }\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 { parsePartialJSON } from '@tanstack/ai'\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'\nimport type { ChatClientState, ConnectionStatus } 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>(options: UseChatOptions<TTools, TSchema>): 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 // Structured-output state. Only meaningful when `outputSchema` is supplied;\n // when it isn't, these stay at their initial values and are hidden from the\n // return type by the conditional in UseChatReturn. Runtime always tracks\n // them — the type system gates visibility, not the runtime.\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n const [partial, setPartial] = useState<Partial>({} as Partial)\n const [final, setFinal] = useState<Final | null>(null)\n // Raw JSON accumulator for parsePartialJSON. Ref instead of state — partial\n // JSON parsing happens synchronously inside the chunk handler; we don't want\n // a re-render per delta solely to track the buffer.\n const rawJsonRef = useRef('')\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 // A useEffect here would be async and messagesRef could be stale on client recreation.\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>>(options)\n optionsRef.current = options\n\n // Create ChatClient instance with callbacks to sync state\n // Note: Options are captured at client creation time.\n // The connection adapter can use functions for dynamic values (url, headers, etc.)\n // which are evaluated lazily on each request.\n const client = useMemo(() => {\n // On first mount, use initialMessages. On subsequent recreations, preserve existing messages.\n const messagesToUse = isFirstMountRef.current\n ? options.initialMessages || []\n : messagesRef.current\n\n isFirstMountRef.current = false\n\n return new ChatClient({\n connection: optionsRef.current.connection,\n id: clientId,\n initialMessages: messagesToUse,\n body: optionsRef.current.body,\n forwardedProps: optionsRef.current.forwardedProps,\n // Wrap every callback so the latest options are read at call time.\n // Capturing the function reference directly would freeze it to whatever\n // the parent passed on the first render.\n onResponse: (response) => optionsRef.current.onResponse?.(response),\n onChunk: (chunk: StreamChunk) => {\n // Internal structured-output tracking — runs before the user callback\n // so user code observes the same state the hook does. Only active when\n // a schema is supplied; otherwise the branches are no-ops.\n if (optionsRef.current.outputSchema !== undefined) {\n if (chunk.type === 'RUN_STARTED') {\n // New run — reset both views.\n rawJsonRef.current = ''\n setPartial({} as Partial)\n setFinal(null)\n } else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {\n rawJsonRef.current += chunk.delta\n const progressive = parsePartialJSON(rawJsonRef.current)\n if (progressive && typeof progressive === 'object') {\n setPartial(progressive as Partial)\n }\n } else if (\n chunk.type === 'CUSTOM' &&\n chunk.name === 'structured-output.complete'\n ) {\n const value = chunk.value as { object: unknown }\n setFinal(value.object as Final)\n }\n }\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 tools: optionsRef.current.tools,\n onCustomEvent: (eventType, data, context) =>\n optionsRef.current.onCustomEvent?.(eventType, data, context),\n streamProcessor: options.streamProcessor,\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 // Sync body / forwardedProps changes to the client.\n // This allows dynamic values (like model selection) to be updated\n // without recreating the client. Both fields populate the same\n // wire payload; `forwardedProps` is preferred and `body` is\n // deprecated but still supported.\n useEffect(() => {\n client.updateOptions({\n body: options.body,\n forwardedProps: options.forwardedProps,\n })\n }, [client, options.body, options.forwardedProps])\n\n // Sync initial messages on mount only\n // Note: initialMessages are passed to ChatClient constructor, but we also\n // set them here to ensure React state is in sync\n useEffect(() => {\n if (options.initialMessages && options.initialMessages.length > 0) {\n // Only set if current messages are empty (initial state)\n if (messages.length === 0) {\n client.setMessagesManually(options.initialMessages)\n }\n }\n }, []) // Only run on mount - initialMessages are handled by ChatClient constructor\n\n // Keep connection lifecycle opt-in and explicit.\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\n\n // Cleanup on unmount: stop any in-flight requests\n // Note: We only cleanup when client changes or component unmounts.\n // DO NOT include isLoading in dependencies - that would cause the cleanup\n // to run when isLoading changes, aborting continuation requests.\n useEffect(() => {\n return () => {\n // live mode owns the connection lifecycle; non-live keeps request-only stop.\n if (options.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n }\n }, [client, options.live])\n\n // All callback options are read through optionsRef at call time, so fresh\n // closures from each render are picked up without recreating the 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 // partial / final are runtime-tracked unconditionally; the conditional\n // return type (UseChatReturn<TTools, TSchema>) hides them from callers that\n // didn't supply `outputSchema`. The `as` cast is the seam between the\n // unconditional runtime shape and the schema-discriminated public shape.\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "React hooks for TanStack AI",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,21 +31,22 @@
|
|
|
31
31
|
"streaming"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@tanstack/ai-client": "0.
|
|
34
|
+
"@tanstack/ai-client": "0.10.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@types/react": ">=18.0.0",
|
|
38
38
|
"react": ">=18.0.0",
|
|
39
|
-
"@tanstack/ai": "^0.
|
|
39
|
+
"@tanstack/ai": "^0.18.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
+
"@standard-schema/spec": "^1.1.0",
|
|
42
43
|
"@testing-library/react": "^16.3.0",
|
|
43
44
|
"@types/react": "^19.2.7",
|
|
44
45
|
"@vitest/coverage-v8": "4.0.14",
|
|
45
46
|
"jsdom": "^27.2.0",
|
|
46
47
|
"react": "^19.2.3",
|
|
47
48
|
"vite": "^7.2.7",
|
|
48
|
-
"@tanstack/ai": "0.
|
|
49
|
+
"@tanstack/ai": "0.18.0"
|
|
49
50
|
},
|
|
50
51
|
"scripts": {
|
|
51
52
|
"clean": "premove ./build ./dist",
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AnyClientTool,
|
|
3
|
+
InferSchemaType,
|
|
4
|
+
ModelMessage,
|
|
5
|
+
SchemaInput,
|
|
6
|
+
} from '@tanstack/ai'
|
|
2
7
|
import type {
|
|
3
8
|
ChatClientOptions,
|
|
4
9
|
ChatClientState,
|
|
@@ -11,6 +16,18 @@ import type {
|
|
|
11
16
|
// Re-export types from ai-client
|
|
12
17
|
export type { ChatRequestBody, MultimodalContent, UIMessage }
|
|
13
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Recursive partial — every property and every nested array element is optional.
|
|
21
|
+
* Used to type the in-flight `partial` value the hook exposes while a structured
|
|
22
|
+
* output stream is still arriving (the JSON has shape but is incomplete).
|
|
23
|
+
*/
|
|
24
|
+
export type DeepPartial<T> =
|
|
25
|
+
T extends ReadonlyArray<infer U>
|
|
26
|
+
? Array<DeepPartial<U>>
|
|
27
|
+
: T extends object
|
|
28
|
+
? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
29
|
+
: T
|
|
30
|
+
|
|
14
31
|
/**
|
|
15
32
|
* Options for the useChat hook.
|
|
16
33
|
*
|
|
@@ -24,30 +41,71 @@ export type { ChatRequestBody, MultimodalContent, UIMessage }
|
|
|
24
41
|
* All other callbacks (onResponse, onChunk, onFinish, onError) are
|
|
25
42
|
* passed through to the underlying ChatClient and can be used for side effects.
|
|
26
43
|
*
|
|
44
|
+
* When `outputSchema` is supplied, the hook returns a typed `partial` (live
|
|
45
|
+
* progressive object, updated from `TEXT_MESSAGE_CONTENT` deltas via
|
|
46
|
+
* `parsePartialJSON`) and `final` (validated terminal payload from the
|
|
47
|
+
* `structured-output.complete` event). The schema is used purely for type
|
|
48
|
+
* inference on the client — server-side validation still runs against the
|
|
49
|
+
* schema you pass to `chat({ outputSchema })` on the server route.
|
|
50
|
+
*
|
|
27
51
|
* Note: Connection and body changes will recreate the ChatClient instance.
|
|
28
52
|
* To update these options, remount the component or use a key prop.
|
|
29
53
|
*/
|
|
30
|
-
export type UseChatOptions<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
export type UseChatOptions<
|
|
55
|
+
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
56
|
+
TSchema extends SchemaInput | undefined = undefined,
|
|
57
|
+
> = Omit<
|
|
58
|
+
ChatClientOptions<TTools>,
|
|
59
|
+
| 'onMessagesChange'
|
|
60
|
+
| 'onLoadingChange'
|
|
61
|
+
| 'onErrorChange'
|
|
62
|
+
| 'onStatusChange'
|
|
63
|
+
| 'onSubscriptionChange'
|
|
64
|
+
| 'onConnectionStatusChange'
|
|
65
|
+
| 'onSessionGeneratingChange'
|
|
66
|
+
> & {
|
|
67
|
+
/**
|
|
68
|
+
* Opt into mount-time live subscription behavior.
|
|
69
|
+
* When enabled, the hook subscribes on mount and unsubscribes on unmount.
|
|
70
|
+
*/
|
|
71
|
+
live?: boolean
|
|
72
|
+
/**
|
|
73
|
+
* Standard-schema-compatible schema (Zod, Valibot, ArkType, or a plain JSON
|
|
74
|
+
* Schema). Used to infer the shape of `partial` and `final` in the return.
|
|
75
|
+
* The schema is **not** sent to the server — server-side validation runs
|
|
76
|
+
* against the schema passed to `chat({ outputSchema })` on the server route.
|
|
77
|
+
*/
|
|
78
|
+
outputSchema?: TSchema
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Discriminated return shape: when `outputSchema` is supplied, the hook adds
|
|
83
|
+
* typed `partial` / `final` fields; when it is omitted (default), the return
|
|
84
|
+
* is unchanged.
|
|
85
|
+
*/
|
|
86
|
+
export type UseChatReturn<
|
|
49
87
|
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
50
|
-
|
|
88
|
+
TSchema extends SchemaInput | undefined = undefined,
|
|
89
|
+
> = BaseUseChatReturn<TTools> &
|
|
90
|
+
(TSchema extends SchemaInput
|
|
91
|
+
? {
|
|
92
|
+
/**
|
|
93
|
+
* Live, progressively-parsed structured output. Updated from
|
|
94
|
+
* `TEXT_MESSAGE_CONTENT` deltas via `parsePartialJSON` while the stream
|
|
95
|
+
* is still arriving, and snapped to the validated payload when
|
|
96
|
+
* `structured-output.complete` fires. Resets on every new run
|
|
97
|
+
* (`sendMessage` / `reload`).
|
|
98
|
+
*/
|
|
99
|
+
partial: DeepPartial<InferSchemaType<TSchema>>
|
|
100
|
+
/**
|
|
101
|
+
* Final, schema-validated structured output. `null` until the terminal
|
|
102
|
+
* `structured-output.complete` event arrives. Resets on every new run.
|
|
103
|
+
*/
|
|
104
|
+
final: InferSchemaType<TSchema> | null
|
|
105
|
+
}
|
|
106
|
+
: Record<never, never>)
|
|
107
|
+
|
|
108
|
+
interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
51
109
|
/**
|
|
52
110
|
* Current messages in the conversation
|
|
53
111
|
*/
|
package/src/use-chat.ts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import { ChatClient } from '@tanstack/ai-client'
|
|
2
|
+
import { parsePartialJSON } from '@tanstack/ai'
|
|
2
3
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
3
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AnyClientTool,
|
|
6
|
+
InferSchemaType,
|
|
7
|
+
ModelMessage,
|
|
8
|
+
SchemaInput,
|
|
9
|
+
StreamChunk,
|
|
10
|
+
} from '@tanstack/ai'
|
|
4
11
|
import type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'
|
|
5
12
|
|
|
6
13
|
import type {
|
|
14
|
+
DeepPartial,
|
|
7
15
|
MultimodalContent,
|
|
8
16
|
UIMessage,
|
|
9
17
|
UseChatOptions,
|
|
10
18
|
UseChatReturn,
|
|
11
19
|
} from './types'
|
|
12
20
|
|
|
13
|
-
export function useChat<
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
export function useChat<
|
|
22
|
+
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
23
|
+
TSchema extends SchemaInput | undefined = undefined,
|
|
24
|
+
>(options: UseChatOptions<TTools, TSchema>): UseChatReturn<TTools, TSchema> {
|
|
16
25
|
const hookId = useId()
|
|
17
26
|
const clientId = options.id || hookId
|
|
18
27
|
|
|
@@ -27,6 +36,19 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
|
27
36
|
useState<ConnectionStatus>('disconnected')
|
|
28
37
|
const [sessionGenerating, setSessionGenerating] = useState(false)
|
|
29
38
|
|
|
39
|
+
// Structured-output state. Only meaningful when `outputSchema` is supplied;
|
|
40
|
+
// when it isn't, these stay at their initial values and are hidden from the
|
|
41
|
+
// return type by the conditional in UseChatReturn. Runtime always tracks
|
|
42
|
+
// them — the type system gates visibility, not the runtime.
|
|
43
|
+
type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>
|
|
44
|
+
type Final = InferSchemaType<NonNullable<TSchema>>
|
|
45
|
+
const [partial, setPartial] = useState<Partial>({} as Partial)
|
|
46
|
+
const [final, setFinal] = useState<Final | null>(null)
|
|
47
|
+
// Raw JSON accumulator for parsePartialJSON. Ref instead of state — partial
|
|
48
|
+
// JSON parsing happens synchronously inside the chunk handler; we don't want
|
|
49
|
+
// a re-render per delta solely to track the buffer.
|
|
50
|
+
const rawJsonRef = useRef('')
|
|
51
|
+
|
|
30
52
|
// Track current messages in a ref to preserve them when client is recreated
|
|
31
53
|
const messagesRef = useRef<Array<UIMessage<TTools>>>(
|
|
32
54
|
options.initialMessages || [],
|
|
@@ -38,7 +60,7 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
|
38
60
|
messagesRef.current = messages
|
|
39
61
|
|
|
40
62
|
// Track current options in a ref to avoid recreating client when options change
|
|
41
|
-
const optionsRef = useRef<UseChatOptions<TTools>>(options)
|
|
63
|
+
const optionsRef = useRef<UseChatOptions<TTools, TSchema>>(options)
|
|
42
64
|
optionsRef.current = options
|
|
43
65
|
|
|
44
66
|
// Create ChatClient instance with callbacks to sync state
|
|
@@ -58,11 +80,37 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
|
58
80
|
id: clientId,
|
|
59
81
|
initialMessages: messagesToUse,
|
|
60
82
|
body: optionsRef.current.body,
|
|
83
|
+
forwardedProps: optionsRef.current.forwardedProps,
|
|
61
84
|
// Wrap every callback so the latest options are read at call time.
|
|
62
85
|
// Capturing the function reference directly would freeze it to whatever
|
|
63
86
|
// the parent passed on the first render.
|
|
64
87
|
onResponse: (response) => optionsRef.current.onResponse?.(response),
|
|
65
|
-
onChunk: (chunk) =>
|
|
88
|
+
onChunk: (chunk: StreamChunk) => {
|
|
89
|
+
// Internal structured-output tracking — runs before the user callback
|
|
90
|
+
// so user code observes the same state the hook does. Only active when
|
|
91
|
+
// a schema is supplied; otherwise the branches are no-ops.
|
|
92
|
+
if (optionsRef.current.outputSchema !== undefined) {
|
|
93
|
+
if (chunk.type === 'RUN_STARTED') {
|
|
94
|
+
// New run — reset both views.
|
|
95
|
+
rawJsonRef.current = ''
|
|
96
|
+
setPartial({} as Partial)
|
|
97
|
+
setFinal(null)
|
|
98
|
+
} else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {
|
|
99
|
+
rawJsonRef.current += chunk.delta
|
|
100
|
+
const progressive = parsePartialJSON(rawJsonRef.current)
|
|
101
|
+
if (progressive && typeof progressive === 'object') {
|
|
102
|
+
setPartial(progressive as Partial)
|
|
103
|
+
}
|
|
104
|
+
} else if (
|
|
105
|
+
chunk.type === 'CUSTOM' &&
|
|
106
|
+
chunk.name === 'structured-output.complete'
|
|
107
|
+
) {
|
|
108
|
+
const value = chunk.value as { object: unknown }
|
|
109
|
+
setFinal(value.object as Final)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
optionsRef.current.onChunk?.(chunk)
|
|
113
|
+
},
|
|
66
114
|
onFinish: (message: UIMessage<TTools>) => {
|
|
67
115
|
optionsRef.current.onFinish?.(message)
|
|
68
116
|
},
|
|
@@ -97,11 +145,17 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
|
97
145
|
})
|
|
98
146
|
}, [clientId])
|
|
99
147
|
|
|
100
|
-
// Sync body changes to the client
|
|
101
|
-
// This allows dynamic
|
|
148
|
+
// Sync body / forwardedProps changes to the client.
|
|
149
|
+
// This allows dynamic values (like model selection) to be updated
|
|
150
|
+
// without recreating the client. Both fields populate the same
|
|
151
|
+
// wire payload; `forwardedProps` is preferred and `body` is
|
|
152
|
+
// deprecated but still supported.
|
|
102
153
|
useEffect(() => {
|
|
103
|
-
client.updateOptions({
|
|
104
|
-
|
|
154
|
+
client.updateOptions({
|
|
155
|
+
body: options.body,
|
|
156
|
+
forwardedProps: options.forwardedProps,
|
|
157
|
+
})
|
|
158
|
+
}, [client, options.body, options.forwardedProps])
|
|
105
159
|
|
|
106
160
|
// Sync initial messages on mount only
|
|
107
161
|
// Note: initialMessages are passed to ChatClient constructor, but we also
|
|
@@ -195,6 +249,10 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
|
195
249
|
[client],
|
|
196
250
|
)
|
|
197
251
|
|
|
252
|
+
// partial / final are runtime-tracked unconditionally; the conditional
|
|
253
|
+
// return type (UseChatReturn<TTools, TSchema>) hides them from callers that
|
|
254
|
+
// didn't supply `outputSchema`. The `as` cast is the seam between the
|
|
255
|
+
// unconditional runtime shape and the schema-discriminated public shape.
|
|
198
256
|
return {
|
|
199
257
|
messages,
|
|
200
258
|
sendMessage,
|
|
@@ -211,5 +269,7 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
|
211
269
|
clear,
|
|
212
270
|
addToolResult,
|
|
213
271
|
addToolApprovalResponse,
|
|
214
|
-
|
|
272
|
+
partial,
|
|
273
|
+
final,
|
|
274
|
+
} as unknown as UseChatReturn<TTools, TSchema>
|
|
215
275
|
}
|