@tanstack/ai-react 0.11.2 → 0.11.3

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.
@@ -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: optionsRef.current.connection,
27
+ connection: initialOptions.connection,
27
28
  id: clientId,
28
29
  initialMessages: messagesToUse,
29
- body: optionsRef.current.body,
30
- forwardedProps: optionsRef.current.forwardedProps,
31
- onResponse: (response) => optionsRef.current.onResponse?.(response),
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: optionsRef.current.tools,
42
- onCustomEvent: (eventType, data, context) => optionsRef.current.onCustomEvent?.(eventType, data, context),
43
- streamProcessor: options.streamProcessor,
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
- forwardedProps: options.forwardedProps
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].role === "user") {
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.role !== "assistant") continue;
159
+ if (m?.role !== "assistant") continue;
147
160
  const part = m.parts.find(
148
161
  (p) => p.type === "structured-output"
149
162
  );
@@ -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) => optionsRef.current.onError?.(e),
21
- onProgress: (p, m) => optionsRef.current.onProgress?.(p, m),
22
- onChunk: (c) => optionsRef.current.onChunk?.(c),
23
- onJobCreated: (id) => optionsRef.current.onJobCreated?.(id),
24
- onStatusUpdate: (s) => optionsRef.current.onStatusUpdate?.(s),
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({ body: options.body });
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) =>\n optionsRef.current.onProgress?.(p, m),\n onChunk: (c: StreamChunk) => optionsRef.current.onChunk?.(c),\n onJobCreated: (id: string) => optionsRef.current.onJobCreated?.(id),\n onStatusUpdate: (s: VideoStatusInfo) =>\n optionsRef.current.onStatusUpdate?.(s),\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 client.updateOptions({ body: options.body })\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;AAExB,UAAM,cAAc;AAAA,MAClB,IAAI;AAAA,MACJ,MAAM,KAAK;AAAA,MACX,UAAU,CAAC,MAA2B,WAAW,QAAQ,WAAW,CAAC;AAAA,MACrE,SAAS,CAAC,MAAa,WAAW,QAAQ,UAAU,CAAC;AAAA,MACrD,YAAY,CAAC,GAAW,MACtB,WAAW,QAAQ,aAAa,GAAG,CAAC;AAAA,MACtC,SAAS,CAAC,MAAmB,WAAW,QAAQ,UAAU,CAAC;AAAA,MAC3D,cAAc,CAAC,OAAe,WAAW,QAAQ,eAAe,EAAE;AAAA,MAClE,gBAAgB,CAAC,MACf,WAAW,QAAQ,iBAAiB,CAAC;AAAA,MACvC,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;AACd,WAAO,cAAc,EAAE,MAAM,QAAQ,MAAM;AAAA,EAC7C,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;"}
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) => optionsRef.current.onError?.(e),
19
- onProgress: (p, m) => optionsRef.current.onProgress?.(p, m),
20
- onChunk: (c) => optionsRef.current.onChunk?.(c),
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({ body: options.body });
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) =>\n optionsRef.current.onProgress?.(p, m),\n onChunk: (c: StreamChunk) => optionsRef.current.onChunk?.(c),\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 client.updateOptions({ body: options.body })\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;AAExB,UAAM,gBAAmE;AAAA,MACvE,IAAI;AAAA,MACJ,MAAM,KAAK;AAAA,MACX,UAAU,CAAC,MAAe,WAAW,QAAQ,WAAW,CAAC;AAAA,MACzD,SAAS,CAAC,MAAa,WAAW,QAAQ,UAAU,CAAC;AAAA,MACrD,YAAY,CAAC,GAAW,MACtB,WAAW,QAAQ,aAAa,GAAG,CAAC;AAAA,MACtC,SAAS,CAAC,MAAmB,WAAW,QAAQ,UAAU,CAAC;AAAA,MAC3D,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;AACd,WAAO,cAAc,EAAE,MAAM,QAAQ,MAAM;AAAA,EAC7C,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;"}
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: optionsRef.current.getToken,
24
- adapter: optionsRef.current.adapter,
25
- tools: optionsRef.current.tools,
26
- instructions: optionsRef.current.instructions,
27
- voice: optionsRef.current.voice,
28
- autoPlayback: optionsRef.current.autoPlayback,
29
- autoCapture: optionsRef.current.autoCapture,
30
- vadMode: optionsRef.current.vadMode,
31
- outputModalities: optionsRef.current.outputModalities,
32
- temperature: optionsRef.current.temperature,
33
- maxOutputTokens: optionsRef.current.maxOutputTokens,
34
- semanticEagerness: optionsRef.current.semanticEagerness,
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.2",
3
+ "version": "0.11.3",
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.2"
34
+ "@tanstack/ai-client": "0.11.3"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@types/react": ">=18.0.0",
38
38
  "react": ">=18.0.0",
39
- "@tanstack/ai": "^0.20.0"
39
+ "@tanstack/ai": "^0.20.1"
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.20.0"
49
+ "@tanstack/ai": "0.20.1"
50
50
  },
51
51
  "scripts": {
52
52
  "clean": "premove ./build ./dist",
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: optionsRef.current.connection,
72
+ connection: initialOptions.connection,
68
73
  id: clientId,
69
74
  initialMessages: messagesToUse,
70
- body: optionsRef.current.body,
71
- forwardedProps: optionsRef.current.forwardedProps,
72
- onResponse: (response) => optionsRef.current.onResponse?.(response),
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: optionsRef.current.tools,
83
- onCustomEvent: (eventType, data, context) =>
84
- optionsRef.current.onCustomEvent?.(eventType, data, context),
85
- streamProcessor: options.streamProcessor,
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
- forwardedProps: options.forwardedProps,
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]!.role === 'user') {
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.role !== 'assistant') continue
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) => optionsRef.current.onError?.(e),
135
- onProgress: (p: number, m?: string) =>
136
- optionsRef.current.onProgress?.(p, m),
137
- onChunk: (c: StreamChunk) => optionsRef.current.onChunk?.(c),
138
- onJobCreated: (id: string) => optionsRef.current.onJobCreated?.(id),
139
- onStatusUpdate: (s: VideoStatusInfo) =>
140
- optionsRef.current.onStatusUpdate?.(s),
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
- client.updateOptions({ body: options.body })
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
@@ -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) => optionsRef.current.onError?.(e),
116
- onProgress: (p: number, m?: string) =>
117
- optionsRef.current.onProgress?.(p, m),
118
- onChunk: (c: StreamChunk) => optionsRef.current.onChunk?.(c),
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
- client.updateOptions({ body: options.body })
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
@@ -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: optionsRef.current.getToken,
88
- adapter: optionsRef.current.adapter,
89
- tools: optionsRef.current.tools,
90
- instructions: optionsRef.current.instructions,
91
- voice: optionsRef.current.voice,
92
- autoPlayback: optionsRef.current.autoPlayback,
93
- autoCapture: optionsRef.current.autoCapture,
94
- vadMode: optionsRef.current.vadMode,
95
- outputModalities: optionsRef.current.outputModalities,
96
- temperature: optionsRef.current.temperature,
97
- maxOutputTokens: optionsRef.current.maxOutputTokens,
98
- semanticEagerness: optionsRef.current.semanticEagerness,
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
  },