@tanstack/ai-react 0.11.8 → 0.12.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 +5 -2
- package/dist/esm/use-chat.js +2 -1
- package/dist/esm/use-chat.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +3 -0
- package/src/types.ts +5 -1
- package/src/use-chat.ts +5 -1
package/dist/esm/index.d.ts
CHANGED
|
@@ -16,4 +16,4 @@ export { useSummarize } from './use-summarize.js';
|
|
|
16
16
|
export type { UseSummarizeOptions, UseSummarizeReturn } from './use-summarize.js';
|
|
17
17
|
export { useGenerateVideo } from './use-generate-video.js';
|
|
18
18
|
export type { UseGenerateVideoOptions, UseGenerateVideoReturn, } from './use-generate-video.js';
|
|
19
|
-
export { fetchServerSentEvents, fetchHttpStream, stream, rpcStream, createChatClientOptions, type ConnectionAdapter, type ConnectConnectionAdapter, type SubscribeConnectionAdapter, type RunAgentInputContext, type FetchConnectionOptions, type InferChatMessages, type GenerationClientState, type ImageGenerateInput, type AudioGenerateInput, type SpeechGenerateInput, type TranscriptionGenerateInput, type SummarizeGenerateInput, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, } from '@tanstack/ai-client';
|
|
19
|
+
export { fetchServerSentEvents, fetchHttpStream, stream, rpcStream, createChatClientOptions, type ChatFetcher, type ChatFetcherInput, type ChatFetcherOptions, type ConnectionAdapter, type ConnectConnectionAdapter, type SubscribeConnectionAdapter, type RunAgentInputContext, type FetchConnectionOptions, type InferChatMessages, type GenerationClientState, type ImageGenerateInput, type AudioGenerateInput, type SpeechGenerateInput, type TranscriptionGenerateInput, type SummarizeGenerateInput, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, } from '@tanstack/ai-client';
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AnyClientTool, InferSchemaType, ModelMessage, SchemaInput } from '@tanstack/ai';
|
|
2
|
-
import { ChatClientOptions, ChatClientState, ChatRequestBody, ConnectionStatus, MultimodalContent, UIMessage } from '@tanstack/ai-client';
|
|
2
|
+
import { ChatClientOptions, ChatClientState, ChatRequestBody, ConnectionStatus, DistributedOmit, MultimodalContent, UIMessage } from '@tanstack/ai-client';
|
|
3
3
|
export type { ChatRequestBody, MultimodalContent, UIMessage };
|
|
4
4
|
/**
|
|
5
5
|
* Recursive partial — every property and every nested array element is optional.
|
|
@@ -12,6 +12,9 @@ export type DeepPartial<T> = T extends ReadonlyArray<infer U> ? Array<DeepPartia
|
|
|
12
12
|
/**
|
|
13
13
|
* Options for the useChat hook.
|
|
14
14
|
*
|
|
15
|
+
* Pass either `connection` or `fetcher` — the XOR is enforced at the type
|
|
16
|
+
* level via `ChatTransport`.
|
|
17
|
+
*
|
|
15
18
|
* This extends ChatClientOptions but omits the state change callbacks that are
|
|
16
19
|
* managed internally by React state:
|
|
17
20
|
* - `onMessagesChange` - Managed by React state (exposed as `messages`)
|
|
@@ -32,7 +35,7 @@ export type DeepPartial<T> = T extends ReadonlyArray<infer U> ? Array<DeepPartia
|
|
|
32
35
|
* Note: Connection and body changes will recreate the ChatClient instance.
|
|
33
36
|
* To update these options, remount the component or use a key prop.
|
|
34
37
|
*/
|
|
35
|
-
export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> =
|
|
38
|
+
export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = DistributedOmit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange'> & {
|
|
36
39
|
/**
|
|
37
40
|
* Opt into mount-time live subscription behavior.
|
|
38
41
|
* When enabled, the hook subscribes on mount and unsubscribes on unmount.
|
package/dist/esm/use-chat.js
CHANGED
|
@@ -23,8 +23,9 @@ function useChat(options) {
|
|
|
23
23
|
const messagesToUse = isFirstMountRef.current ? options.initialMessages || [] : messagesRef.current;
|
|
24
24
|
isFirstMountRef.current = false;
|
|
25
25
|
const initialOptions = optionsRef.current;
|
|
26
|
+
const transport = initialOptions.connection ? { connection: initialOptions.connection } : { fetcher: initialOptions.fetcher };
|
|
26
27
|
return new ChatClient({
|
|
27
|
-
|
|
28
|
+
...transport,
|
|
28
29
|
id: clientId,
|
|
29
30
|
initialMessages: messagesToUse,
|
|
30
31
|
...initialOptions.body !== void 0 && { body: initialOptions.body },
|
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 {\n AnyClientTool,\n InferSchemaType,\n ModelMessage,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai'\nimport type {\n ChatClientState,\n ConnectionStatus,\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>(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 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>>(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 return new ChatClient({\n connection: initialOptions.connection,\n id: clientId,\n initialMessages: messagesToUse,\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\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 })\n }, [client, options.body, options.forwardedProps])\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 return () => {\n if (options.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n }\n }, [client, options.live])\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 { 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 {\n ChatClientState,\n ConnectionStatus,\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>(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 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>>(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({\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 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 })\n }, [client, options.body, options.forwardedProps])\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 return () => {\n if (options.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n }\n }, [client, options.live])\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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"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "React hooks for TanStack AI streaming chat, realtime voice, structured outputs, and media generation.",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/TanStack/ai.git",
|
|
10
|
-
"directory": "packages/
|
|
10
|
+
"directory": "packages/ai-react"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
13
13
|
"module": "./dist/esm/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"media-generation"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@tanstack/ai-client": "0.
|
|
40
|
+
"@tanstack/ai-client": "0.12.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@types/react": ">=18.0.0",
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
ChatClientState,
|
|
10
10
|
ChatRequestBody,
|
|
11
11
|
ConnectionStatus,
|
|
12
|
+
DistributedOmit,
|
|
12
13
|
MultimodalContent,
|
|
13
14
|
UIMessage,
|
|
14
15
|
} from '@tanstack/ai-client'
|
|
@@ -31,6 +32,9 @@ export type DeepPartial<T> =
|
|
|
31
32
|
/**
|
|
32
33
|
* Options for the useChat hook.
|
|
33
34
|
*
|
|
35
|
+
* Pass either `connection` or `fetcher` — the XOR is enforced at the type
|
|
36
|
+
* level via `ChatTransport`.
|
|
37
|
+
*
|
|
34
38
|
* This extends ChatClientOptions but omits the state change callbacks that are
|
|
35
39
|
* managed internally by React state:
|
|
36
40
|
* - `onMessagesChange` - Managed by React state (exposed as `messages`)
|
|
@@ -54,7 +58,7 @@ export type DeepPartial<T> =
|
|
|
54
58
|
export type UseChatOptions<
|
|
55
59
|
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
56
60
|
TSchema extends SchemaInput | undefined = undefined,
|
|
57
|
-
> =
|
|
61
|
+
> = DistributedOmit<
|
|
58
62
|
ChatClientOptions<TTools>,
|
|
59
63
|
| 'onMessagesChange'
|
|
60
64
|
| 'onLoadingChange'
|
package/src/use-chat.ts
CHANGED
|
@@ -68,8 +68,12 @@ export function useChat<
|
|
|
68
68
|
// optional (`field?: T`) — `exactOptionalPropertyTypes` rejects
|
|
69
69
|
// assigning `undefined` to those, so we omit the key when absent.
|
|
70
70
|
const initialOptions = optionsRef.current
|
|
71
|
+
const transport = initialOptions.connection
|
|
72
|
+
? { connection: initialOptions.connection }
|
|
73
|
+
: { fetcher: initialOptions.fetcher }
|
|
74
|
+
|
|
71
75
|
return new ChatClient({
|
|
72
|
-
|
|
76
|
+
...transport,
|
|
73
77
|
id: clientId,
|
|
74
78
|
initialMessages: messagesToUse,
|
|
75
79
|
...(initialOptions.body !== undefined && { body: initialOptions.body }),
|