@tanstack/ai-react 0.11.2 → 0.11.4
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/index.js +2 -1
- package/dist/esm/use-chat.js +23 -10
- package/dist/esm/use-chat.js.map +1 -1
- package/dist/esm/use-generate-video.js +18 -6
- package/dist/esm/use-generate-video.js.map +1 -1
- package/dist/esm/use-generation.js +12 -4
- package/dist/esm/use-generation.js.map +1 -1
- package/dist/esm/use-realtime-chat.js +27 -12
- package/dist/esm/use-realtime-chat.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +4 -0
- package/src/use-chat.ts +34 -12
- package/src/use-generate-video.ts +24 -8
- package/src/use-generation.ts +17 -5
- package/src/use-realtime-chat.ts +31 -12
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, createChatClientOptions, type ConnectionAdapter, 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 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/index.js
CHANGED
|
@@ -7,11 +7,12 @@ import { useGenerateSpeech } from "./use-generate-speech.js";
|
|
|
7
7
|
import { useTranscription } from "./use-transcription.js";
|
|
8
8
|
import { useSummarize } from "./use-summarize.js";
|
|
9
9
|
import { useGenerateVideo } from "./use-generate-video.js";
|
|
10
|
-
import { createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream } from "@tanstack/ai-client";
|
|
10
|
+
import { createChatClientOptions, fetchHttpStream, fetchServerSentEvents, rpcStream, stream } from "@tanstack/ai-client";
|
|
11
11
|
export {
|
|
12
12
|
createChatClientOptions,
|
|
13
13
|
fetchHttpStream,
|
|
14
14
|
fetchServerSentEvents,
|
|
15
|
+
rpcStream,
|
|
15
16
|
stream,
|
|
16
17
|
useChat,
|
|
17
18
|
useGenerateAudio,
|
package/dist/esm/use-chat.js
CHANGED
|
@@ -22,13 +22,18 @@ function useChat(options) {
|
|
|
22
22
|
const client = useMemo(() => {
|
|
23
23
|
const messagesToUse = isFirstMountRef.current ? options.initialMessages || [] : messagesRef.current;
|
|
24
24
|
isFirstMountRef.current = false;
|
|
25
|
+
const initialOptions = optionsRef.current;
|
|
25
26
|
return new ChatClient({
|
|
26
|
-
connection:
|
|
27
|
+
connection: initialOptions.connection,
|
|
27
28
|
id: clientId,
|
|
28
29
|
initialMessages: messagesToUse,
|
|
29
|
-
body:
|
|
30
|
-
forwardedProps
|
|
31
|
-
|
|
30
|
+
...initialOptions.body !== void 0 && { body: initialOptions.body },
|
|
31
|
+
...initialOptions.forwardedProps !== void 0 && {
|
|
32
|
+
forwardedProps: initialOptions.forwardedProps
|
|
33
|
+
},
|
|
34
|
+
onResponse: (response) => {
|
|
35
|
+
void optionsRef.current.onResponse?.(response);
|
|
36
|
+
},
|
|
32
37
|
onChunk: (chunk) => {
|
|
33
38
|
optionsRef.current.onChunk?.(chunk);
|
|
34
39
|
},
|
|
@@ -38,9 +43,15 @@ function useChat(options) {
|
|
|
38
43
|
onError: (error2) => {
|
|
39
44
|
optionsRef.current.onError?.(error2);
|
|
40
45
|
},
|
|
41
|
-
tools
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
...initialOptions.tools !== void 0 && {
|
|
47
|
+
tools: initialOptions.tools
|
|
48
|
+
},
|
|
49
|
+
onCustomEvent: (eventType, data, context) => {
|
|
50
|
+
optionsRef.current.onCustomEvent?.(eventType, data, context);
|
|
51
|
+
},
|
|
52
|
+
...options.streamProcessor !== void 0 && {
|
|
53
|
+
streamProcessor: options.streamProcessor
|
|
54
|
+
},
|
|
44
55
|
onMessagesChange: (newMessages) => {
|
|
45
56
|
setMessages(newMessages);
|
|
46
57
|
},
|
|
@@ -67,7 +78,9 @@ function useChat(options) {
|
|
|
67
78
|
useEffect(() => {
|
|
68
79
|
client.updateOptions({
|
|
69
80
|
body: options.body,
|
|
70
|
-
|
|
81
|
+
...options.forwardedProps !== void 0 && {
|
|
82
|
+
forwardedProps: options.forwardedProps
|
|
83
|
+
}
|
|
71
84
|
});
|
|
72
85
|
}, [client, options.body, options.forwardedProps]);
|
|
73
86
|
useEffect(() => {
|
|
@@ -135,7 +148,7 @@ function useChat(options) {
|
|
|
135
148
|
const activeStructuredPart = useMemo(() => {
|
|
136
149
|
let lastUserIndex = -1;
|
|
137
150
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
138
|
-
if (messages[i]
|
|
151
|
+
if (messages[i]?.role === "user") {
|
|
139
152
|
lastUserIndex = i;
|
|
140
153
|
break;
|
|
141
154
|
}
|
|
@@ -143,7 +156,7 @@ function useChat(options) {
|
|
|
143
156
|
if (lastUserIndex === -1) return null;
|
|
144
157
|
for (let i = messages.length - 1; i > lastUserIndex; i--) {
|
|
145
158
|
const m = messages[i];
|
|
146
|
-
if (m
|
|
159
|
+
if (m?.role !== "assistant") continue;
|
|
147
160
|
const part = m.parts.find(
|
|
148
161
|
(p) => p.type === "structured-output"
|
|
149
162
|
);
|
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 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 onResponse: (response) => optionsRef.current.onResponse?.(response),\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 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 useEffect(() => {\n client.updateOptions({\n body: options.body,\n forwardedProps: options.forwardedProps,\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 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 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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"}
|
|
@@ -17,11 +17,21 @@ function useGenerateVideo(options) {
|
|
|
17
17
|
id: clientId,
|
|
18
18
|
body: opts.body,
|
|
19
19
|
onResult: (r) => optionsRef.current.onResult?.(r),
|
|
20
|
-
onError: (e) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
onError: (e) => {
|
|
21
|
+
optionsRef.current.onError?.(e);
|
|
22
|
+
},
|
|
23
|
+
onProgress: (p, m) => {
|
|
24
|
+
optionsRef.current.onProgress?.(p, m);
|
|
25
|
+
},
|
|
26
|
+
onChunk: (c) => {
|
|
27
|
+
optionsRef.current.onChunk?.(c);
|
|
28
|
+
},
|
|
29
|
+
onJobCreated: (id) => {
|
|
30
|
+
optionsRef.current.onJobCreated?.(id);
|
|
31
|
+
},
|
|
32
|
+
onStatusUpdate: (s) => {
|
|
33
|
+
optionsRef.current.onStatusUpdate?.(s);
|
|
34
|
+
},
|
|
25
35
|
onResultChange: setResult,
|
|
26
36
|
onLoadingChange: setIsLoading,
|
|
27
37
|
onErrorChange: setError,
|
|
@@ -46,7 +56,9 @@ function useGenerateVideo(options) {
|
|
|
46
56
|
);
|
|
47
57
|
}, [clientId]);
|
|
48
58
|
useEffect(() => {
|
|
49
|
-
client.updateOptions({
|
|
59
|
+
client.updateOptions({
|
|
60
|
+
...options.body !== void 0 && { body: options.body }
|
|
61
|
+
});
|
|
50
62
|
}, [client, options.body]);
|
|
51
63
|
useEffect(() => {
|
|
52
64
|
return () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-generate-video.js","sources":["../../src/use-generate-video.ts"],"sourcesContent":["import { VideoGenerationClient } from '@tanstack/ai-client'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type { StreamChunk } from '@tanstack/ai'\nimport type {\n ConnectConnectionAdapter,\n GenerationClientState,\n GenerationFetcher,\n InferGenerationOutput,\n VideoGenerateInput,\n VideoGenerateResult,\n VideoStatusInfo,\n} from '@tanstack/ai-client'\n\n/**\n * Options for the useGenerateVideo hook.\n */\nexport interface UseGenerateVideoOptions<TOutput = VideoGenerateResult> {\n /** Connect-based adapter for streaming transport (server handles polling) */\n connection?: ConnectConnectionAdapter\n /** Direct async function that returns a completed video result */\n fetcher?: GenerationFetcher<VideoGenerateInput, VideoGenerateResult>\n /** Unique identifier for this generation instance */\n id?: string\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n /**\n * Callback when video generation completes. Can optionally return a transformed value.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: VideoGenerateResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback when a video job is created */\n onJobCreated?: (jobId: string) => void\n /** Callback on each status update */\n onStatusUpdate?: (status: VideoStatusInfo) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n}\n\n/**\n * Return type for the useGenerateVideo hook.\n *\n * @template TOutput - The output type (after optional transform)\n */\nexport interface UseGenerateVideoReturn<TOutput = VideoGenerateResult> {\n /** Trigger video generation */\n generate: (input: VideoGenerateInput) => Promise<void>\n /** The final video result (with URL), or null */\n result: TOutput | null\n /** The current job ID, or null */\n jobId: string | null\n /** Current video generation status info, or null */\n videoStatus: VideoStatusInfo | null\n /** Whether generation/polling is in progress */\n isLoading: boolean\n /** Current error, if any */\n error: Error | undefined\n /** Current state of the generation */\n status: GenerationClientState\n /** Abort the current generation/polling */\n stop: () => void\n /** Clear all state and return to idle */\n reset: () => void\n}\n\n/**\n * React hook for generating videos using AI models.\n *\n * Video generation is asynchronous: a job is created, then polled for status\n * until completion. This hook handles the full lifecycle.\n *\n * @example\n * ```tsx\n * import { useGenerateVideo } from '@tanstack/ai-react'\n * import { fetchServerSentEvents } from '@tanstack/ai-client'\n *\n * function VideoGenerator() {\n * const { generate, result, videoStatus, isLoading } = useGenerateVideo({\n * connection: fetchServerSentEvents('/api/generate/video'),\n * onStatusUpdate: (status) => console.log(`Progress: ${status.progress}%`),\n * })\n *\n * return (\n * <div>\n * <button onClick={() => generate({ prompt: 'A flying car over a city' })}>\n * Generate Video\n * </button>\n * {isLoading && videoStatus && (\n * <p>Status: {videoStatus.status} ({videoStatus.progress}%)</p>\n * )}\n * {result && <video src={result.url} controls />}\n * </div>\n * )\n * }\n * ```\n */\nexport function useGenerateVideo<\n TOnResult extends ((result: VideoGenerateResult) => any) | undefined =\n undefined,\n>(\n options: Omit<UseGenerateVideoOptions, 'onResult'> & {\n onResult?: TOnResult\n },\n): UseGenerateVideoReturn<\n InferGenerationOutput<VideoGenerateResult, TOnResult>\n> {\n type TOutput = InferGenerationOutput<VideoGenerateResult, TOnResult>\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [result, setResult] = useState<TOutput | null>(null)\n const [jobId, setJobId] = useState<string | null>(null)\n const [videoStatus, setVideoStatus] = useState<VideoStatusInfo | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<GenerationClientState>('idle')\n\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n const client = useMemo(() => {\n const opts = optionsRef.current\n\n const baseOptions = {\n id: clientId,\n body: opts.body,\n onResult: (r: VideoGenerateResult) => optionsRef.current.onResult?.(r),\n onError: (e: Error) => optionsRef.current.onError?.(e),\n onProgress: (p: number, m?: string)
|
|
1
|
+
{"version":3,"file":"use-generate-video.js","sources":["../../src/use-generate-video.ts"],"sourcesContent":["import { VideoGenerationClient } from '@tanstack/ai-client'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type { StreamChunk } from '@tanstack/ai'\nimport type {\n ConnectConnectionAdapter,\n GenerationClientState,\n GenerationFetcher,\n InferGenerationOutput,\n VideoGenerateInput,\n VideoGenerateResult,\n VideoStatusInfo,\n} from '@tanstack/ai-client'\n\n/**\n * Options for the useGenerateVideo hook.\n */\nexport interface UseGenerateVideoOptions<TOutput = VideoGenerateResult> {\n /** Connect-based adapter for streaming transport (server handles polling) */\n connection?: ConnectConnectionAdapter\n /** Direct async function that returns a completed video result */\n fetcher?: GenerationFetcher<VideoGenerateInput, VideoGenerateResult>\n /** Unique identifier for this generation instance */\n id?: string\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n /**\n * Callback when video generation completes. Can optionally return a transformed value.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: VideoGenerateResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback when a video job is created */\n onJobCreated?: (jobId: string) => void\n /** Callback on each status update */\n onStatusUpdate?: (status: VideoStatusInfo) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n}\n\n/**\n * Return type for the useGenerateVideo hook.\n *\n * @template TOutput - The output type (after optional transform)\n */\nexport interface UseGenerateVideoReturn<TOutput = VideoGenerateResult> {\n /** Trigger video generation */\n generate: (input: VideoGenerateInput) => Promise<void>\n /** The final video result (with URL), or null */\n result: TOutput | null\n /** The current job ID, or null */\n jobId: string | null\n /** Current video generation status info, or null */\n videoStatus: VideoStatusInfo | null\n /** Whether generation/polling is in progress */\n isLoading: boolean\n /** Current error, if any */\n error: Error | undefined\n /** Current state of the generation */\n status: GenerationClientState\n /** Abort the current generation/polling */\n stop: () => void\n /** Clear all state and return to idle */\n reset: () => void\n}\n\n/**\n * React hook for generating videos using AI models.\n *\n * Video generation is asynchronous: a job is created, then polled for status\n * until completion. This hook handles the full lifecycle.\n *\n * @example\n * ```tsx\n * import { useGenerateVideo } from '@tanstack/ai-react'\n * import { fetchServerSentEvents } from '@tanstack/ai-client'\n *\n * function VideoGenerator() {\n * const { generate, result, videoStatus, isLoading } = useGenerateVideo({\n * connection: fetchServerSentEvents('/api/generate/video'),\n * onStatusUpdate: (status) => console.log(`Progress: ${status.progress}%`),\n * })\n *\n * return (\n * <div>\n * <button onClick={() => generate({ prompt: 'A flying car over a city' })}>\n * Generate Video\n * </button>\n * {isLoading && videoStatus && (\n * <p>Status: {videoStatus.status} ({videoStatus.progress}%)</p>\n * )}\n * {result && <video src={result.url} controls />}\n * </div>\n * )\n * }\n * ```\n */\nexport function useGenerateVideo<\n TOnResult extends ((result: VideoGenerateResult) => any) | undefined =\n undefined,\n>(\n options: Omit<UseGenerateVideoOptions, 'onResult'> & {\n onResult?: TOnResult\n },\n): UseGenerateVideoReturn<\n InferGenerationOutput<VideoGenerateResult, TOnResult>\n> {\n type TOutput = InferGenerationOutput<VideoGenerateResult, TOnResult>\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [result, setResult] = useState<TOutput | null>(null)\n const [jobId, setJobId] = useState<string | null>(null)\n const [videoStatus, setVideoStatus] = useState<VideoStatusInfo | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<GenerationClientState>('idle')\n\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n const client = useMemo(() => {\n const opts = optionsRef.current\n\n // Conditional spread for `body` (strict-optional in target).\n // Optional callbacks are wrapped in non-returning bodies so\n // `?.()`'s implicit `undefined` doesn't widen the function\n // return type (which `exactOptionalPropertyTypes` rejects\n // against the strict-optional target).\n const baseOptions = {\n id: clientId,\n body: opts.body,\n onResult: (r: VideoGenerateResult) => optionsRef.current.onResult?.(r),\n onError: (e: Error) => {\n optionsRef.current.onError?.(e)\n },\n onProgress: (p: number, m?: string) => {\n optionsRef.current.onProgress?.(p, m)\n },\n onChunk: (c: StreamChunk) => {\n optionsRef.current.onChunk?.(c)\n },\n onJobCreated: (id: string) => {\n optionsRef.current.onJobCreated?.(id)\n },\n onStatusUpdate: (s: VideoStatusInfo) => {\n optionsRef.current.onStatusUpdate?.(s)\n },\n onResultChange: setResult,\n onLoadingChange: setIsLoading,\n onErrorChange: setError,\n onStatusChange: setStatus,\n onJobIdChange: setJobId,\n onVideoStatusChange: setVideoStatus,\n }\n\n if (opts.connection) {\n return new VideoGenerationClient<TOutput>({\n ...baseOptions,\n connection: opts.connection,\n })\n }\n\n if (opts.fetcher) {\n return new VideoGenerationClient<TOutput>({\n ...baseOptions,\n fetcher: opts.fetcher,\n })\n }\n\n throw new Error(\n 'useGenerateVideo requires either a connection or fetcher option',\n )\n }, [clientId])\n\n // Sync body changes without recreating client\n useEffect(() => {\n // Conditional spread: target uses strict-optional `body?: T`.\n client.updateOptions({\n ...(options.body !== undefined && { body: options.body }),\n })\n }, [client, options.body])\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n client.stop()\n }\n }, [client])\n\n const generate = useCallback(\n async (input: VideoGenerateInput) => {\n await client.generate(input)\n },\n [client],\n )\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const reset = useCallback(() => {\n client.reset()\n }, [client])\n\n return {\n generate,\n result,\n jobId,\n videoStatus,\n isLoading,\n error,\n status,\n stop,\n reset,\n }\n}\n"],"names":[],"mappings":";;AAsGO,SAAS,iBAId,SAKA;AAEA,QAAM,SAAS,MAAA;AACf,QAAM,WAAW,QAAQ,MAAM;AAE/B,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAyB,IAAI;AACzD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAiC,IAAI;AAC3E,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA4B,MAAS;AAC/D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAgC,MAAM;AAElE,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AAErB,QAAM,SAAS,QAAQ,MAAM;AAC3B,UAAM,OAAO,WAAW;AAOxB,UAAM,cAAc;AAAA,MAClB,IAAI;AAAA,MACJ,MAAM,KAAK;AAAA,MACX,UAAU,CAAC,MAA2B,WAAW,QAAQ,WAAW,CAAC;AAAA,MACrE,SAAS,CAAC,MAAa;AACrB,mBAAW,QAAQ,UAAU,CAAC;AAAA,MAChC;AAAA,MACA,YAAY,CAAC,GAAW,MAAe;AACrC,mBAAW,QAAQ,aAAa,GAAG,CAAC;AAAA,MACtC;AAAA,MACA,SAAS,CAAC,MAAmB;AAC3B,mBAAW,QAAQ,UAAU,CAAC;AAAA,MAChC;AAAA,MACA,cAAc,CAAC,OAAe;AAC5B,mBAAW,QAAQ,eAAe,EAAE;AAAA,MACtC;AAAA,MACA,gBAAgB,CAAC,MAAuB;AACtC,mBAAW,QAAQ,iBAAiB,CAAC;AAAA,MACvC;AAAA,MACA,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,MACjB,eAAe;AAAA,MACf,gBAAgB;AAAA,MAChB,eAAe;AAAA,MACf,qBAAqB;AAAA,IAAA;AAGvB,QAAI,KAAK,YAAY;AACnB,aAAO,IAAI,sBAA+B;AAAA,QACxC,GAAG;AAAA,QACH,YAAY,KAAK;AAAA,MAAA,CAClB;AAAA,IACH;AAEA,QAAI,KAAK,SAAS;AAChB,aAAO,IAAI,sBAA+B;AAAA,QACxC,GAAG;AAAA,QACH,SAAS,KAAK;AAAA,MAAA,CACf;AAAA,IACH;AAEA,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ,GAAG,CAAC,QAAQ,CAAC;AAGb,YAAU,MAAM;AAEd,WAAO,cAAc;AAAA,MACnB,GAAI,QAAQ,SAAS,UAAa,EAAE,MAAM,QAAQ,KAAA;AAAA,IAAK,CACxD;AAAA,EACH,GAAG,CAAC,QAAQ,QAAQ,IAAI,CAAC;AAGzB,YAAU,MAAM;AACd,WAAO,MAAM;AACX,aAAO,KAAA;AAAA,IACT;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,WAAW;AAAA,IACf,OAAO,UAA8B;AACnC,YAAM,OAAO,SAAS,KAAK;AAAA,IAC7B;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,QAAM,OAAO,YAAY,MAAM;AAC7B,WAAO,KAAA;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,QAAQ,YAAY,MAAM;AAC9B,WAAO,MAAA;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAEX,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
|
|
@@ -15,9 +15,15 @@ function useGeneration(options) {
|
|
|
15
15
|
id: clientId,
|
|
16
16
|
body: opts.body,
|
|
17
17
|
onResult: (r) => optionsRef.current.onResult?.(r),
|
|
18
|
-
onError: (e) =>
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
onError: (e) => {
|
|
19
|
+
optionsRef.current.onError?.(e);
|
|
20
|
+
},
|
|
21
|
+
onProgress: (p, m) => {
|
|
22
|
+
optionsRef.current.onProgress?.(p, m);
|
|
23
|
+
},
|
|
24
|
+
onChunk: (c) => {
|
|
25
|
+
optionsRef.current.onChunk?.(c);
|
|
26
|
+
},
|
|
21
27
|
onResultChange: setResult,
|
|
22
28
|
onLoadingChange: setIsLoading,
|
|
23
29
|
onErrorChange: setError,
|
|
@@ -40,7 +46,9 @@ function useGeneration(options) {
|
|
|
40
46
|
);
|
|
41
47
|
}, [clientId]);
|
|
42
48
|
useEffect(() => {
|
|
43
|
-
client.updateOptions({
|
|
49
|
+
client.updateOptions({
|
|
50
|
+
...options.body !== void 0 && { body: options.body }
|
|
51
|
+
});
|
|
44
52
|
}, [client, options.body]);
|
|
45
53
|
useEffect(() => {
|
|
46
54
|
return () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-generation.js","sources":["../../src/use-generation.ts"],"sourcesContent":["import { GenerationClient } from '@tanstack/ai-client'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type { StreamChunk } from '@tanstack/ai'\nimport type {\n ConnectConnectionAdapter,\n GenerationClientOptions,\n GenerationClientState,\n GenerationFetcher,\n InferGenerationOutput,\n} from '@tanstack/ai-client'\n\n/**\n * Options for the useGeneration hook.\n *\n * Accepts either a `connection` (streaming transport) or a `fetcher` (direct async call).\n *\n * @template TInput - The input type for the generation request\n * @template TResult - The result type returned by the generation\n * @template TOutput - The output type after optional transform (defaults to TResult)\n */\nexport interface UseGenerationOptions<TInput, TResult, TOutput = TResult> {\n /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */\n connection?: ConnectConnectionAdapter\n /** Direct async function for one-shot generation (no streaming protocol needed) */\n fetcher?: GenerationFetcher<TInput, TResult>\n /** Unique identifier for this generation instance */\n id?: string\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n /**\n * Callback when a result is received. Can optionally return a transformed value.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: TResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n}\n\n/**\n * Return type for the useGeneration hook.\n *\n * @template TOutput - The output type (after optional transform)\n */\nexport interface UseGenerationReturn<TOutput> {\n /** Trigger a generation request */\n generate: (input: Record<string, any>) => Promise<void>\n /** The generation result, or null if not yet generated */\n result: TOutput | null\n /** Whether a generation is currently in progress */\n isLoading: boolean\n /** Current error, if any */\n error: Error | undefined\n /** Current state of the generation client */\n status: GenerationClientState\n /** Abort the current generation */\n stop: () => void\n /** Clear result, error, and return to idle */\n reset: () => void\n}\n\n/**\n * Generic React hook for one-shot generation tasks.\n *\n * This is the base hook used by `useGenerateImage`, `useGenerateSpeech`,\n * `useTranscription`, and `useSummarize`. You can also use it directly\n * for custom generation types.\n *\n * @template TInput - The input type for the generation request\n * @template TResult - The result type returned by the generation\n *\n * @example\n * ```tsx\n * const { generate, result, isLoading } = useGeneration<MyInput, MyResult>({\n * connection: fetchServerSentEvents('/api/generate/custom'),\n * })\n *\n * await generate({ prompt: 'Hello' })\n * ```\n */\nexport function useGeneration<\n TInput extends Record<string, any>,\n TResult,\n TOnResult extends ((result: TResult) => any) | undefined = undefined,\n>(\n options: Omit<UseGenerationOptions<TInput, TResult>, 'onResult'> & {\n onResult?: TOnResult\n },\n): UseGenerationReturn<InferGenerationOutput<TResult, TOnResult>> {\n type TOutput = InferGenerationOutput<TResult, TOnResult>\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [result, setResult] = useState<TOutput | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<GenerationClientState>('idle')\n\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n const client = useMemo(() => {\n const opts = optionsRef.current\n\n const clientOptions: GenerationClientOptions<TInput, TResult, TOutput> = {\n id: clientId,\n body: opts.body,\n onResult: (r: TResult) => optionsRef.current.onResult?.(r),\n onError: (e: Error) => optionsRef.current.onError?.(e),\n onProgress: (p: number, m?: string)
|
|
1
|
+
{"version":3,"file":"use-generation.js","sources":["../../src/use-generation.ts"],"sourcesContent":["import { GenerationClient } from '@tanstack/ai-client'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type { StreamChunk } from '@tanstack/ai'\nimport type {\n ConnectConnectionAdapter,\n GenerationClientOptions,\n GenerationClientState,\n GenerationFetcher,\n InferGenerationOutput,\n} from '@tanstack/ai-client'\n\n/**\n * Options for the useGeneration hook.\n *\n * Accepts either a `connection` (streaming transport) or a `fetcher` (direct async call).\n *\n * @template TInput - The input type for the generation request\n * @template TResult - The result type returned by the generation\n * @template TOutput - The output type after optional transform (defaults to TResult)\n */\nexport interface UseGenerationOptions<TInput, TResult, TOutput = TResult> {\n /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */\n connection?: ConnectConnectionAdapter\n /** Direct async function for one-shot generation (no streaming protocol needed) */\n fetcher?: GenerationFetcher<TInput, TResult>\n /** Unique identifier for this generation instance */\n id?: string\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n /**\n * Callback when a result is received. Can optionally return a transformed value.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: TResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n}\n\n/**\n * Return type for the useGeneration hook.\n *\n * @template TOutput - The output type (after optional transform)\n */\nexport interface UseGenerationReturn<TOutput> {\n /** Trigger a generation request */\n generate: (input: Record<string, any>) => Promise<void>\n /** The generation result, or null if not yet generated */\n result: TOutput | null\n /** Whether a generation is currently in progress */\n isLoading: boolean\n /** Current error, if any */\n error: Error | undefined\n /** Current state of the generation client */\n status: GenerationClientState\n /** Abort the current generation */\n stop: () => void\n /** Clear result, error, and return to idle */\n reset: () => void\n}\n\n/**\n * Generic React hook for one-shot generation tasks.\n *\n * This is the base hook used by `useGenerateImage`, `useGenerateSpeech`,\n * `useTranscription`, and `useSummarize`. You can also use it directly\n * for custom generation types.\n *\n * @template TInput - The input type for the generation request\n * @template TResult - The result type returned by the generation\n *\n * @example\n * ```tsx\n * const { generate, result, isLoading } = useGeneration<MyInput, MyResult>({\n * connection: fetchServerSentEvents('/api/generate/custom'),\n * })\n *\n * await generate({ prompt: 'Hello' })\n * ```\n */\nexport function useGeneration<\n TInput extends Record<string, any>,\n TResult,\n TOnResult extends ((result: TResult) => any) | undefined = undefined,\n>(\n options: Omit<UseGenerationOptions<TInput, TResult>, 'onResult'> & {\n onResult?: TOnResult\n },\n): UseGenerationReturn<InferGenerationOutput<TResult, TOnResult>> {\n type TOutput = InferGenerationOutput<TResult, TOnResult>\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [result, setResult] = useState<TOutput | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<GenerationClientState>('idle')\n\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n const client = useMemo(() => {\n const opts = optionsRef.current\n\n // Conditional spread for `body` (strict-optional in target;\n // local source is `Record<string, any> | undefined`). Callbacks\n // wrap optional ones in non-returning bodies so `?.()`'s\n // implicit `undefined` doesn't pollute the function return type.\n const clientOptions: GenerationClientOptions<TInput, TResult, TOutput> = {\n id: clientId,\n body: opts.body,\n onResult: (r: TResult) => optionsRef.current.onResult?.(r),\n onError: (e: Error) => {\n optionsRef.current.onError?.(e)\n },\n onProgress: (p: number, m?: string) => {\n optionsRef.current.onProgress?.(p, m)\n },\n onChunk: (c: StreamChunk) => {\n optionsRef.current.onChunk?.(c)\n },\n onResultChange: setResult,\n onLoadingChange: setIsLoading,\n onErrorChange: setError,\n onStatusChange: setStatus,\n }\n\n if (opts.connection) {\n return new GenerationClient<TInput, TResult, TOutput>({\n ...clientOptions,\n connection: opts.connection,\n })\n }\n\n if (opts.fetcher) {\n return new GenerationClient<TInput, TResult, TOutput>({\n ...clientOptions,\n fetcher: opts.fetcher,\n })\n }\n\n throw new Error(\n 'useGeneration requires either a connection or fetcher option',\n )\n }, [clientId])\n\n // Sync body changes without recreating client\n useEffect(() => {\n // Conditional spread: target uses strict-optional `body?: T`.\n client.updateOptions({\n ...(options.body !== undefined && { body: options.body }),\n })\n }, [client, options.body])\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n client.stop()\n }\n }, [client])\n\n const generate = useCallback(\n async (input: TInput) => {\n await client.generate(input)\n },\n [client],\n )\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const reset = useCallback(() => {\n client.reset()\n }, [client])\n\n return {\n generate: generate as (input: Record<string, any>) => Promise<void>,\n result,\n isLoading,\n error,\n status,\n stop,\n reset,\n }\n}\n"],"names":[],"mappings":";;AAsFO,SAAS,cAKd,SAGgE;AAEhE,QAAM,SAAS,MAAA;AACf,QAAM,WAAW,QAAQ,MAAM;AAE/B,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAyB,IAAI;AACzD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA4B,MAAS;AAC/D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAgC,MAAM;AAElE,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AAErB,QAAM,SAAS,QAAQ,MAAM;AAC3B,UAAM,OAAO,WAAW;AAMxB,UAAM,gBAAmE;AAAA,MACvE,IAAI;AAAA,MACJ,MAAM,KAAK;AAAA,MACX,UAAU,CAAC,MAAe,WAAW,QAAQ,WAAW,CAAC;AAAA,MACzD,SAAS,CAAC,MAAa;AACrB,mBAAW,QAAQ,UAAU,CAAC;AAAA,MAChC;AAAA,MACA,YAAY,CAAC,GAAW,MAAe;AACrC,mBAAW,QAAQ,aAAa,GAAG,CAAC;AAAA,MACtC;AAAA,MACA,SAAS,CAAC,MAAmB;AAC3B,mBAAW,QAAQ,UAAU,CAAC;AAAA,MAChC;AAAA,MACA,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,MACjB,eAAe;AAAA,MACf,gBAAgB;AAAA,IAAA;AAGlB,QAAI,KAAK,YAAY;AACnB,aAAO,IAAI,iBAA2C;AAAA,QACpD,GAAG;AAAA,QACH,YAAY,KAAK;AAAA,MAAA,CAClB;AAAA,IACH;AAEA,QAAI,KAAK,SAAS;AAChB,aAAO,IAAI,iBAA2C;AAAA,QACpD,GAAG;AAAA,QACH,SAAS,KAAK;AAAA,MAAA,CACf;AAAA,IACH;AAEA,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ,GAAG,CAAC,QAAQ,CAAC;AAGb,YAAU,MAAM;AAEd,WAAO,cAAc;AAAA,MACnB,GAAI,QAAQ,SAAS,UAAa,EAAE,MAAM,QAAQ,KAAA;AAAA,IAAK,CACxD;AAAA,EACH,GAAG,CAAC,QAAQ,QAAQ,IAAI,CAAC;AAGzB,YAAU,MAAM;AACd,WAAO,MAAM;AACX,aAAO,KAAA;AAAA,IACT;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,WAAW;AAAA,IACf,OAAO,UAAkB;AACvB,YAAM,OAAO,SAAS,KAAK;AAAA,IAC7B;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,QAAM,OAAO,YAAY,MAAM;AAC7B,WAAO,KAAA;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,QAAQ,YAAY,MAAM;AAC9B,WAAO,MAAA;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAEX,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
|
|
@@ -19,19 +19,34 @@ function useRealtimeChat(options) {
|
|
|
19
19
|
optionsRef.current = options;
|
|
20
20
|
const animationFrameRef = useRef(null);
|
|
21
21
|
if (!clientRef.current) {
|
|
22
|
+
const initial = optionsRef.current;
|
|
22
23
|
clientRef.current = new RealtimeClient({
|
|
23
|
-
getToken:
|
|
24
|
-
adapter:
|
|
25
|
-
tools:
|
|
26
|
-
instructions
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
getToken: initial.getToken,
|
|
25
|
+
adapter: initial.adapter,
|
|
26
|
+
...initial.tools !== void 0 && { tools: initial.tools },
|
|
27
|
+
...initial.instructions !== void 0 && {
|
|
28
|
+
instructions: initial.instructions
|
|
29
|
+
},
|
|
30
|
+
...initial.voice !== void 0 && { voice: initial.voice },
|
|
31
|
+
...initial.autoPlayback !== void 0 && {
|
|
32
|
+
autoPlayback: initial.autoPlayback
|
|
33
|
+
},
|
|
34
|
+
...initial.autoCapture !== void 0 && {
|
|
35
|
+
autoCapture: initial.autoCapture
|
|
36
|
+
},
|
|
37
|
+
...initial.vadMode !== void 0 && { vadMode: initial.vadMode },
|
|
38
|
+
...initial.outputModalities !== void 0 && {
|
|
39
|
+
outputModalities: initial.outputModalities
|
|
40
|
+
},
|
|
41
|
+
...initial.temperature !== void 0 && {
|
|
42
|
+
temperature: initial.temperature
|
|
43
|
+
},
|
|
44
|
+
...initial.maxOutputTokens !== void 0 && {
|
|
45
|
+
maxOutputTokens: initial.maxOutputTokens
|
|
46
|
+
},
|
|
47
|
+
...initial.semanticEagerness !== void 0 && {
|
|
48
|
+
semanticEagerness: initial.semanticEagerness
|
|
49
|
+
},
|
|
35
50
|
onStatusChange: (newStatus) => {
|
|
36
51
|
setStatus(newStatus);
|
|
37
52
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-realtime-chat.js","sources":["../../src/use-realtime-chat.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { RealtimeClient } from '@tanstack/ai-client'\nimport type {\n RealtimeMessage,\n RealtimeMode,\n RealtimeStatus,\n} from '@tanstack/ai'\nimport type {\n UseRealtimeChatOptions,\n UseRealtimeChatReturn,\n} from './realtime-types'\n\n// Empty frequency data for when client is not connected\nconst emptyFrequencyData = new Uint8Array(128)\nconst emptyTimeDomainData = new Uint8Array(128).fill(128)\n\n/**\n * React hook for realtime voice conversations.\n *\n * Provides a simple interface for voice-to-voice AI interactions\n * with support for multiple providers (OpenAI, ElevenLabs, etc.).\n *\n * @param options - Configuration options including adapter and callbacks\n * @returns Hook return value with state and control methods\n *\n * @example\n * ```typescript\n * import { useRealtimeChat } from '@tanstack/ai-react'\n * import { openaiRealtime } from '@tanstack/ai-openai'\n *\n * function VoiceChat() {\n * const {\n * status,\n * mode,\n * messages,\n * connect,\n * disconnect,\n * inputLevel,\n * outputLevel,\n * } = useRealtimeChat({\n * getToken: () => fetch('/api/realtime-token').then(r => r.json()),\n * adapter: openaiRealtime(),\n * })\n *\n * return (\n * <div>\n * <p>Status: {status}</p>\n * <p>Mode: {mode}</p>\n * <button onClick={status === 'idle' ? connect : disconnect}>\n * {status === 'idle' ? 'Start' : 'Stop'}\n * </button>\n * </div>\n * )\n * }\n * ```\n */\nexport function useRealtimeChat(\n options: UseRealtimeChatOptions,\n): UseRealtimeChatReturn {\n // State\n const [status, setStatus] = useState<RealtimeStatus>('idle')\n const [mode, setMode] = useState<RealtimeMode>('idle')\n const [messages, setMessages] = useState<Array<RealtimeMessage>>([])\n const [pendingUserTranscript, setPendingUserTranscript] = useState<\n string | null\n >(null)\n const [pendingAssistantTranscript, setPendingAssistantTranscript] = useState<\n string | null\n >(null)\n const [error, setError] = useState<Error | null>(null)\n const [inputLevel, setInputLevel] = useState(0)\n const [outputLevel, setOutputLevel] = useState(0)\n const [vadMode, setVADModeState] = useState<'server' | 'semantic' | 'manual'>(\n options.vadMode ?? 'server',\n )\n\n // Refs\n const clientRef = useRef<RealtimeClient | null>(null)\n const optionsRef = useRef(options)\n optionsRef.current = options\n const animationFrameRef = useRef<number | null>(null)\n\n // Create client instance - use ref to ensure we reuse the same instance\n // This handles React StrictMode double-rendering\n if (!clientRef.current) {\n clientRef.current = new RealtimeClient({\n getToken: optionsRef.current.getToken,\n adapter: optionsRef.current.adapter,\n tools: optionsRef.current.tools,\n instructions: optionsRef.current.instructions,\n voice: optionsRef.current.voice,\n autoPlayback: optionsRef.current.autoPlayback,\n autoCapture: optionsRef.current.autoCapture,\n vadMode: optionsRef.current.vadMode,\n outputModalities: optionsRef.current.outputModalities,\n temperature: optionsRef.current.temperature,\n maxOutputTokens: optionsRef.current.maxOutputTokens,\n semanticEagerness: optionsRef.current.semanticEagerness,\n onStatusChange: (newStatus) => {\n setStatus(newStatus)\n },\n onModeChange: (newMode) => {\n setMode(newMode)\n optionsRef.current.onModeChange?.(newMode)\n },\n onMessage: (message) => {\n setMessages((prev) => [...prev, message])\n optionsRef.current.onMessage?.(message)\n },\n onError: (err) => {\n setError(err)\n optionsRef.current.onError?.(err)\n },\n onConnect: () => {\n setError(null)\n optionsRef.current.onConnect?.()\n },\n onDisconnect: () => {\n optionsRef.current.onDisconnect?.()\n },\n onInterrupted: () => {\n setPendingAssistantTranscript(null)\n optionsRef.current.onInterrupted?.()\n },\n })\n\n // Subscribe to state changes for transcripts\n clientRef.current.onStateChange((state) => {\n setPendingUserTranscript(state.pendingUserTranscript)\n setPendingAssistantTranscript(state.pendingAssistantTranscript)\n })\n }\n\n const client = clientRef.current\n\n // Audio level animation loop\n useEffect(() => {\n function updateLevels() {\n if (clientRef.current?.audio) {\n setInputLevel(clientRef.current.audio.inputLevel)\n setOutputLevel(clientRef.current.audio.outputLevel)\n }\n animationFrameRef.current = requestAnimationFrame(updateLevels)\n }\n\n if (status === 'connected') {\n updateLevels()\n }\n\n return () => {\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current)\n animationFrameRef.current = null\n }\n }\n }, [status])\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n clientRef.current?.destroy()\n }\n }, [])\n\n // Connection methods\n const connect = useCallback(async () => {\n setError(null)\n setMessages([])\n setPendingUserTranscript(null)\n setPendingAssistantTranscript(null)\n await client.connect()\n }, [client])\n\n const disconnect = useCallback(async () => {\n await client.disconnect()\n }, [client])\n\n // Voice control methods\n const startListening = useCallback(() => {\n client.startListening()\n }, [client])\n\n const stopListening = useCallback(() => {\n client.stopListening()\n }, [client])\n\n const interrupt = useCallback(() => {\n client.interrupt()\n }, [client])\n\n // Text input\n const sendText = useCallback(\n (text: string) => {\n client.sendText(text)\n },\n [client],\n )\n\n // Image input\n const sendImage = useCallback(\n (imageData: string, mimeType: string) => {\n client.sendImage(imageData, mimeType)\n },\n [client],\n )\n\n // Audio visualization\n const getInputFrequencyData = useCallback(() => {\n return (\n clientRef.current?.audio?.getInputFrequencyData() ?? emptyFrequencyData\n )\n }, [])\n\n const getOutputFrequencyData = useCallback(() => {\n return (\n clientRef.current?.audio?.getOutputFrequencyData() ?? emptyFrequencyData\n )\n }, [])\n\n const getInputTimeDomainData = useCallback(() => {\n return (\n clientRef.current?.audio?.getInputTimeDomainData() ?? emptyTimeDomainData\n )\n }, [])\n\n const getOutputTimeDomainData = useCallback(() => {\n return (\n clientRef.current?.audio?.getOutputTimeDomainData() ?? emptyTimeDomainData\n )\n }, [])\n\n // VAD mode control\n const setVADMode = useCallback(\n (newMode: 'server' | 'semantic' | 'manual') => {\n setVADModeState(newMode)\n // TODO: Update session config if connected\n },\n [],\n )\n\n return {\n // Connection state\n status,\n error,\n connect,\n disconnect,\n\n // Conversation state\n mode,\n messages,\n pendingUserTranscript,\n pendingAssistantTranscript,\n\n // Voice control\n startListening,\n stopListening,\n interrupt,\n\n // Text input\n sendText,\n\n // Image input\n sendImage,\n\n // Audio visualization\n inputLevel,\n outputLevel,\n getInputFrequencyData,\n getOutputFrequencyData,\n getInputTimeDomainData,\n getOutputTimeDomainData,\n\n // VAD control\n vadMode,\n setVADMode,\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-realtime-chat.js","sources":["../../src/use-realtime-chat.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { RealtimeClient } from '@tanstack/ai-client'\nimport type {\n RealtimeMessage,\n RealtimeMode,\n RealtimeStatus,\n} from '@tanstack/ai'\nimport type {\n UseRealtimeChatOptions,\n UseRealtimeChatReturn,\n} from './realtime-types'\n\n// Empty frequency data for when client is not connected\nconst emptyFrequencyData = new Uint8Array(128)\nconst emptyTimeDomainData = new Uint8Array(128).fill(128)\n\n/**\n * React hook for realtime voice conversations.\n *\n * Provides a simple interface for voice-to-voice AI interactions\n * with support for multiple providers (OpenAI, ElevenLabs, etc.).\n *\n * @param options - Configuration options including adapter and callbacks\n * @returns Hook return value with state and control methods\n *\n * @example\n * ```typescript\n * import { useRealtimeChat } from '@tanstack/ai-react'\n * import { openaiRealtime } from '@tanstack/ai-openai'\n *\n * function VoiceChat() {\n * const {\n * status,\n * mode,\n * messages,\n * connect,\n * disconnect,\n * inputLevel,\n * outputLevel,\n * } = useRealtimeChat({\n * getToken: () => fetch('/api/realtime-token').then(r => r.json()),\n * adapter: openaiRealtime(),\n * })\n *\n * return (\n * <div>\n * <p>Status: {status}</p>\n * <p>Mode: {mode}</p>\n * <button onClick={status === 'idle' ? connect : disconnect}>\n * {status === 'idle' ? 'Start' : 'Stop'}\n * </button>\n * </div>\n * )\n * }\n * ```\n */\nexport function useRealtimeChat(\n options: UseRealtimeChatOptions,\n): UseRealtimeChatReturn {\n // State\n const [status, setStatus] = useState<RealtimeStatus>('idle')\n const [mode, setMode] = useState<RealtimeMode>('idle')\n const [messages, setMessages] = useState<Array<RealtimeMessage>>([])\n const [pendingUserTranscript, setPendingUserTranscript] = useState<\n string | null\n >(null)\n const [pendingAssistantTranscript, setPendingAssistantTranscript] = useState<\n string | null\n >(null)\n const [error, setError] = useState<Error | null>(null)\n const [inputLevel, setInputLevel] = useState(0)\n const [outputLevel, setOutputLevel] = useState(0)\n const [vadMode, setVADModeState] = useState<'server' | 'semantic' | 'manual'>(\n options.vadMode ?? 'server',\n )\n\n // Refs\n const clientRef = useRef<RealtimeClient | null>(null)\n const optionsRef = useRef(options)\n optionsRef.current = options\n const animationFrameRef = useRef<number | null>(null)\n\n // Create client instance - use ref to ensure we reuse the same instance\n // This handles React StrictMode double-rendering\n if (!clientRef.current) {\n // Each optional source field is spread conditionally because the\n // `RealtimeClientOptions` target declares strict optionals\n // (`field?: T`) and `exactOptionalPropertyTypes` rejects passing\n // `undefined` for absent values.\n const initial = optionsRef.current\n clientRef.current = new RealtimeClient({\n getToken: initial.getToken,\n adapter: initial.adapter,\n ...(initial.tools !== undefined && { tools: initial.tools }),\n ...(initial.instructions !== undefined && {\n instructions: initial.instructions,\n }),\n ...(initial.voice !== undefined && { voice: initial.voice }),\n ...(initial.autoPlayback !== undefined && {\n autoPlayback: initial.autoPlayback,\n }),\n ...(initial.autoCapture !== undefined && {\n autoCapture: initial.autoCapture,\n }),\n ...(initial.vadMode !== undefined && { vadMode: initial.vadMode }),\n ...(initial.outputModalities !== undefined && {\n outputModalities: initial.outputModalities,\n }),\n ...(initial.temperature !== undefined && {\n temperature: initial.temperature,\n }),\n ...(initial.maxOutputTokens !== undefined && {\n maxOutputTokens: initial.maxOutputTokens,\n }),\n ...(initial.semanticEagerness !== undefined && {\n semanticEagerness: initial.semanticEagerness,\n }),\n onStatusChange: (newStatus) => {\n setStatus(newStatus)\n },\n onModeChange: (newMode) => {\n setMode(newMode)\n optionsRef.current.onModeChange?.(newMode)\n },\n onMessage: (message) => {\n setMessages((prev) => [...prev, message])\n optionsRef.current.onMessage?.(message)\n },\n onError: (err) => {\n setError(err)\n optionsRef.current.onError?.(err)\n },\n onConnect: () => {\n setError(null)\n optionsRef.current.onConnect?.()\n },\n onDisconnect: () => {\n optionsRef.current.onDisconnect?.()\n },\n onInterrupted: () => {\n setPendingAssistantTranscript(null)\n optionsRef.current.onInterrupted?.()\n },\n })\n\n // Subscribe to state changes for transcripts\n clientRef.current.onStateChange((state) => {\n setPendingUserTranscript(state.pendingUserTranscript)\n setPendingAssistantTranscript(state.pendingAssistantTranscript)\n })\n }\n\n const client = clientRef.current\n\n // Audio level animation loop\n useEffect(() => {\n function updateLevels() {\n if (clientRef.current?.audio) {\n setInputLevel(clientRef.current.audio.inputLevel)\n setOutputLevel(clientRef.current.audio.outputLevel)\n }\n animationFrameRef.current = requestAnimationFrame(updateLevels)\n }\n\n if (status === 'connected') {\n updateLevels()\n }\n\n return () => {\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current)\n animationFrameRef.current = null\n }\n }\n }, [status])\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n clientRef.current?.destroy()\n }\n }, [])\n\n // Connection methods\n const connect = useCallback(async () => {\n setError(null)\n setMessages([])\n setPendingUserTranscript(null)\n setPendingAssistantTranscript(null)\n await client.connect()\n }, [client])\n\n const disconnect = useCallback(async () => {\n await client.disconnect()\n }, [client])\n\n // Voice control methods\n const startListening = useCallback(() => {\n client.startListening()\n }, [client])\n\n const stopListening = useCallback(() => {\n client.stopListening()\n }, [client])\n\n const interrupt = useCallback(() => {\n client.interrupt()\n }, [client])\n\n // Text input\n const sendText = useCallback(\n (text: string) => {\n client.sendText(text)\n },\n [client],\n )\n\n // Image input\n const sendImage = useCallback(\n (imageData: string, mimeType: string) => {\n client.sendImage(imageData, mimeType)\n },\n [client],\n )\n\n // Audio visualization\n const getInputFrequencyData = useCallback(() => {\n return (\n clientRef.current?.audio?.getInputFrequencyData() ?? emptyFrequencyData\n )\n }, [])\n\n const getOutputFrequencyData = useCallback(() => {\n return (\n clientRef.current?.audio?.getOutputFrequencyData() ?? emptyFrequencyData\n )\n }, [])\n\n const getInputTimeDomainData = useCallback(() => {\n return (\n clientRef.current?.audio?.getInputTimeDomainData() ?? emptyTimeDomainData\n )\n }, [])\n\n const getOutputTimeDomainData = useCallback(() => {\n return (\n clientRef.current?.audio?.getOutputTimeDomainData() ?? emptyTimeDomainData\n )\n }, [])\n\n // VAD mode control\n const setVADMode = useCallback(\n (newMode: 'server' | 'semantic' | 'manual') => {\n setVADModeState(newMode)\n // TODO: Update session config if connected\n },\n [],\n )\n\n return {\n // Connection state\n status,\n error,\n connect,\n disconnect,\n\n // Conversation state\n mode,\n messages,\n pendingUserTranscript,\n pendingAssistantTranscript,\n\n // Voice control\n startListening,\n stopListening,\n interrupt,\n\n // Text input\n sendText,\n\n // Image input\n sendImage,\n\n // Audio visualization\n inputLevel,\n outputLevel,\n getInputFrequencyData,\n getOutputFrequencyData,\n getInputTimeDomainData,\n getOutputTimeDomainData,\n\n // VAD control\n vadMode,\n setVADMode,\n }\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-react",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"description": "React hooks for TanStack AI",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"streaming"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@tanstack/ai-client": "0.11.
|
|
34
|
+
"@tanstack/ai-client": "0.11.4"
|
|
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.21.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@standard-schema/spec": "^1.1.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"jsdom": "^27.2.0",
|
|
47
47
|
"react": "^19.2.3",
|
|
48
48
|
"vite": "^7.3.3",
|
|
49
|
-
"@tanstack/ai": "0.
|
|
49
|
+
"@tanstack/ai": "0.21.0"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"clean": "premove ./build ./dist",
|
package/src/index.ts
CHANGED
|
@@ -57,8 +57,12 @@ export {
|
|
|
57
57
|
fetchServerSentEvents,
|
|
58
58
|
fetchHttpStream,
|
|
59
59
|
stream,
|
|
60
|
+
rpcStream,
|
|
60
61
|
createChatClientOptions,
|
|
61
62
|
type ConnectionAdapter,
|
|
63
|
+
type ConnectConnectionAdapter,
|
|
64
|
+
type SubscribeConnectionAdapter,
|
|
65
|
+
type RunAgentInputContext,
|
|
62
66
|
type FetchConnectionOptions,
|
|
63
67
|
type InferChatMessages,
|
|
64
68
|
type GenerationClientState,
|
package/src/use-chat.ts
CHANGED
|
@@ -63,13 +63,22 @@ export function useChat<
|
|
|
63
63
|
|
|
64
64
|
isFirstMountRef.current = false
|
|
65
65
|
|
|
66
|
+
// Build options with conditional spreads for fields whose source
|
|
67
|
+
// type is `T | undefined` but the ChatClient target uses a strict
|
|
68
|
+
// optional (`field?: T`) — `exactOptionalPropertyTypes` rejects
|
|
69
|
+
// assigning `undefined` to those, so we omit the key when absent.
|
|
70
|
+
const initialOptions = optionsRef.current
|
|
66
71
|
return new ChatClient({
|
|
67
|
-
connection:
|
|
72
|
+
connection: initialOptions.connection,
|
|
68
73
|
id: clientId,
|
|
69
74
|
initialMessages: messagesToUse,
|
|
70
|
-
body:
|
|
71
|
-
forwardedProps
|
|
72
|
-
|
|
75
|
+
...(initialOptions.body !== undefined && { body: initialOptions.body }),
|
|
76
|
+
...(initialOptions.forwardedProps !== undefined && {
|
|
77
|
+
forwardedProps: initialOptions.forwardedProps,
|
|
78
|
+
}),
|
|
79
|
+
onResponse: (response) => {
|
|
80
|
+
void optionsRef.current.onResponse?.(response)
|
|
81
|
+
},
|
|
73
82
|
onChunk: (chunk: StreamChunk) => {
|
|
74
83
|
optionsRef.current.onChunk?.(chunk)
|
|
75
84
|
},
|
|
@@ -79,10 +88,15 @@ export function useChat<
|
|
|
79
88
|
onError: (error: Error) => {
|
|
80
89
|
optionsRef.current.onError?.(error)
|
|
81
90
|
},
|
|
82
|
-
tools
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
...(initialOptions.tools !== undefined && {
|
|
92
|
+
tools: initialOptions.tools,
|
|
93
|
+
}),
|
|
94
|
+
onCustomEvent: (eventType, data, context) => {
|
|
95
|
+
optionsRef.current.onCustomEvent?.(eventType, data, context)
|
|
96
|
+
},
|
|
97
|
+
...(options.streamProcessor !== undefined && {
|
|
98
|
+
streamProcessor: options.streamProcessor,
|
|
99
|
+
}),
|
|
86
100
|
onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
|
|
87
101
|
setMessages(newMessages)
|
|
88
102
|
},
|
|
@@ -108,9 +122,13 @@ export function useChat<
|
|
|
108
122
|
}, [clientId])
|
|
109
123
|
|
|
110
124
|
useEffect(() => {
|
|
125
|
+
// Conditional spread: `updateOptions` declares strict-optional
|
|
126
|
+
// fields and rejects explicit `undefined` under EOPT.
|
|
111
127
|
client.updateOptions({
|
|
112
128
|
body: options.body,
|
|
113
|
-
|
|
129
|
+
...(options.forwardedProps !== undefined && {
|
|
130
|
+
forwardedProps: options.forwardedProps,
|
|
131
|
+
}),
|
|
114
132
|
})
|
|
115
133
|
}, [client, options.body, options.forwardedProps])
|
|
116
134
|
|
|
@@ -206,15 +224,15 @@ export function useChat<
|
|
|
206
224
|
const activeStructuredPart = useMemo<StructuredOutputPart | null>(() => {
|
|
207
225
|
let lastUserIndex = -1
|
|
208
226
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
209
|
-
if (messages[i]
|
|
227
|
+
if (messages[i]?.role === 'user') {
|
|
210
228
|
lastUserIndex = i
|
|
211
229
|
break
|
|
212
230
|
}
|
|
213
231
|
}
|
|
214
232
|
if (lastUserIndex === -1) return null
|
|
215
233
|
for (let i = messages.length - 1; i > lastUserIndex; i--) {
|
|
216
|
-
const m = messages[i]
|
|
217
|
-
if (m
|
|
234
|
+
const m = messages[i]
|
|
235
|
+
if (m?.role !== 'assistant') continue
|
|
218
236
|
const part = m.parts.find(
|
|
219
237
|
(p): p is StructuredOutputPart => p.type === 'structured-output',
|
|
220
238
|
)
|
|
@@ -236,6 +254,10 @@ export function useChat<
|
|
|
236
254
|
return activeStructuredPart.data as Final
|
|
237
255
|
}, [activeStructuredPart])
|
|
238
256
|
|
|
257
|
+
// The runtime shape unconditionally exposes partial/final; the public
|
|
258
|
+
// return type hides them when no outputSchema was supplied. TS can't
|
|
259
|
+
// structurally narrow across that conditional, so the `as` is the seam.
|
|
260
|
+
// 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
|
|
239
261
|
return {
|
|
240
262
|
messages,
|
|
241
263
|
sendMessage,
|
|
@@ -127,17 +127,30 @@ export function useGenerateVideo<
|
|
|
127
127
|
const client = useMemo(() => {
|
|
128
128
|
const opts = optionsRef.current
|
|
129
129
|
|
|
130
|
+
// Conditional spread for `body` (strict-optional in target).
|
|
131
|
+
// Optional callbacks are wrapped in non-returning bodies so
|
|
132
|
+
// `?.()`'s implicit `undefined` doesn't widen the function
|
|
133
|
+
// return type (which `exactOptionalPropertyTypes` rejects
|
|
134
|
+
// against the strict-optional target).
|
|
130
135
|
const baseOptions = {
|
|
131
136
|
id: clientId,
|
|
132
137
|
body: opts.body,
|
|
133
138
|
onResult: (r: VideoGenerateResult) => optionsRef.current.onResult?.(r),
|
|
134
|
-
onError: (e: Error) =>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
onError: (e: Error) => {
|
|
140
|
+
optionsRef.current.onError?.(e)
|
|
141
|
+
},
|
|
142
|
+
onProgress: (p: number, m?: string) => {
|
|
143
|
+
optionsRef.current.onProgress?.(p, m)
|
|
144
|
+
},
|
|
145
|
+
onChunk: (c: StreamChunk) => {
|
|
146
|
+
optionsRef.current.onChunk?.(c)
|
|
147
|
+
},
|
|
148
|
+
onJobCreated: (id: string) => {
|
|
149
|
+
optionsRef.current.onJobCreated?.(id)
|
|
150
|
+
},
|
|
151
|
+
onStatusUpdate: (s: VideoStatusInfo) => {
|
|
152
|
+
optionsRef.current.onStatusUpdate?.(s)
|
|
153
|
+
},
|
|
141
154
|
onResultChange: setResult,
|
|
142
155
|
onLoadingChange: setIsLoading,
|
|
143
156
|
onErrorChange: setError,
|
|
@@ -167,7 +180,10 @@ export function useGenerateVideo<
|
|
|
167
180
|
|
|
168
181
|
// Sync body changes without recreating client
|
|
169
182
|
useEffect(() => {
|
|
170
|
-
|
|
183
|
+
// Conditional spread: target uses strict-optional `body?: T`.
|
|
184
|
+
client.updateOptions({
|
|
185
|
+
...(options.body !== undefined && { body: options.body }),
|
|
186
|
+
})
|
|
171
187
|
}, [client, options.body])
|
|
172
188
|
|
|
173
189
|
// Cleanup on unmount
|
package/src/use-generation.ts
CHANGED
|
@@ -108,14 +108,23 @@ export function useGeneration<
|
|
|
108
108
|
const client = useMemo(() => {
|
|
109
109
|
const opts = optionsRef.current
|
|
110
110
|
|
|
111
|
+
// Conditional spread for `body` (strict-optional in target;
|
|
112
|
+
// local source is `Record<string, any> | undefined`). Callbacks
|
|
113
|
+
// wrap optional ones in non-returning bodies so `?.()`'s
|
|
114
|
+
// implicit `undefined` doesn't pollute the function return type.
|
|
111
115
|
const clientOptions: GenerationClientOptions<TInput, TResult, TOutput> = {
|
|
112
116
|
id: clientId,
|
|
113
117
|
body: opts.body,
|
|
114
118
|
onResult: (r: TResult) => optionsRef.current.onResult?.(r),
|
|
115
|
-
onError: (e: Error) =>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
onError: (e: Error) => {
|
|
120
|
+
optionsRef.current.onError?.(e)
|
|
121
|
+
},
|
|
122
|
+
onProgress: (p: number, m?: string) => {
|
|
123
|
+
optionsRef.current.onProgress?.(p, m)
|
|
124
|
+
},
|
|
125
|
+
onChunk: (c: StreamChunk) => {
|
|
126
|
+
optionsRef.current.onChunk?.(c)
|
|
127
|
+
},
|
|
119
128
|
onResultChange: setResult,
|
|
120
129
|
onLoadingChange: setIsLoading,
|
|
121
130
|
onErrorChange: setError,
|
|
@@ -143,7 +152,10 @@ export function useGeneration<
|
|
|
143
152
|
|
|
144
153
|
// Sync body changes without recreating client
|
|
145
154
|
useEffect(() => {
|
|
146
|
-
|
|
155
|
+
// Conditional spread: target uses strict-optional `body?: T`.
|
|
156
|
+
client.updateOptions({
|
|
157
|
+
...(options.body !== undefined && { body: options.body }),
|
|
158
|
+
})
|
|
147
159
|
}, [client, options.body])
|
|
148
160
|
|
|
149
161
|
// Cleanup on unmount
|
package/src/use-realtime-chat.ts
CHANGED
|
@@ -83,19 +83,38 @@ export function useRealtimeChat(
|
|
|
83
83
|
// Create client instance - use ref to ensure we reuse the same instance
|
|
84
84
|
// This handles React StrictMode double-rendering
|
|
85
85
|
if (!clientRef.current) {
|
|
86
|
+
// Each optional source field is spread conditionally because the
|
|
87
|
+
// `RealtimeClientOptions` target declares strict optionals
|
|
88
|
+
// (`field?: T`) and `exactOptionalPropertyTypes` rejects passing
|
|
89
|
+
// `undefined` for absent values.
|
|
90
|
+
const initial = optionsRef.current
|
|
86
91
|
clientRef.current = new RealtimeClient({
|
|
87
|
-
getToken:
|
|
88
|
-
adapter:
|
|
89
|
-
tools:
|
|
90
|
-
instructions
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
getToken: initial.getToken,
|
|
93
|
+
adapter: initial.adapter,
|
|
94
|
+
...(initial.tools !== undefined && { tools: initial.tools }),
|
|
95
|
+
...(initial.instructions !== undefined && {
|
|
96
|
+
instructions: initial.instructions,
|
|
97
|
+
}),
|
|
98
|
+
...(initial.voice !== undefined && { voice: initial.voice }),
|
|
99
|
+
...(initial.autoPlayback !== undefined && {
|
|
100
|
+
autoPlayback: initial.autoPlayback,
|
|
101
|
+
}),
|
|
102
|
+
...(initial.autoCapture !== undefined && {
|
|
103
|
+
autoCapture: initial.autoCapture,
|
|
104
|
+
}),
|
|
105
|
+
...(initial.vadMode !== undefined && { vadMode: initial.vadMode }),
|
|
106
|
+
...(initial.outputModalities !== undefined && {
|
|
107
|
+
outputModalities: initial.outputModalities,
|
|
108
|
+
}),
|
|
109
|
+
...(initial.temperature !== undefined && {
|
|
110
|
+
temperature: initial.temperature,
|
|
111
|
+
}),
|
|
112
|
+
...(initial.maxOutputTokens !== undefined && {
|
|
113
|
+
maxOutputTokens: initial.maxOutputTokens,
|
|
114
|
+
}),
|
|
115
|
+
...(initial.semanticEagerness !== undefined && {
|
|
116
|
+
semanticEagerness: initial.semanticEagerness,
|
|
117
|
+
}),
|
|
99
118
|
onStatusChange: (newStatus) => {
|
|
100
119
|
setStatus(newStatus)
|
|
101
120
|
},
|