@tanstack/ai-react 0.16.4 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/realtime-types.d.ts +5 -5
- package/dist/esm/types.d.ts +16 -4
- package/dist/esm/use-chat.d.ts +1 -1
- package/dist/esm/use-chat.js +23 -2
- package/dist/esm/use-chat.js.map +1 -1
- package/dist/esm/use-realtime-chat.js +11 -12
- package/dist/esm/use-realtime-chat.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +6 -0
- package/src/realtime-types.ts +7 -5
- package/src/types.ts +36 -2
- package/src/use-chat.ts +31 -3
- package/src/use-realtime-chat.ts +12 -14
package/dist/esm/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { useChat } from './use-chat.js';
|
|
|
2
2
|
export { useRealtimeChat } from './use-realtime-chat.js';
|
|
3
3
|
export { useMcpAppBridge } from './use-mcp-app-bridge.js';
|
|
4
4
|
export type { UseMcpAppBridgeOptions } from './use-mcp-app-bridge.js';
|
|
5
|
-
export type { DeepPartial, UseChatOptions, UseChatReturn, UIMessage, ChatRequestBody, } from './types.js';
|
|
5
|
+
export type { DeepPartial, UseChatOptions, UseChatReturn, UIMessage, ChatRequestBody, QueuedMessage, SendMessageOptions, WhenBusy, QueueConfig, QueueStrategy, QueueOption, } from './types.js';
|
|
6
6
|
export type { UseRealtimeChatOptions, UseRealtimeChatReturn, } from './realtime-types.js';
|
|
7
7
|
export { useGeneration } from './use-generation.js';
|
|
8
8
|
export type { UseGenerationOptions, UseGenerationReturn, } from './use-generation.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnyClientTool, RealtimeMessage, RealtimeMode, RealtimeStatus, RealtimeToken } from '@tanstack/ai';
|
|
1
|
+
import { AnyClientTool, RealtimeMessage, RealtimeMode, RealtimeSessionConfig, RealtimeStatus, RealtimeToken, UsageInfo } from '@tanstack/ai';
|
|
2
2
|
import { RealtimeAdapter } from '@tanstack/ai-client';
|
|
3
3
|
/**
|
|
4
4
|
* Options for the useRealtimeChat hook.
|
|
@@ -59,6 +59,9 @@ export interface UseRealtimeChatOptions {
|
|
|
59
59
|
onMessage?: (message: RealtimeMessage) => void;
|
|
60
60
|
onModeChange?: (mode: RealtimeMode) => void;
|
|
61
61
|
onInterrupted?: () => void;
|
|
62
|
+
onUsage?: (usage: UsageInfo) => void;
|
|
63
|
+
onGoAway?: (timeLeft?: string) => void;
|
|
64
|
+
onStatusChange?: (status: RealtimeStatus) => void;
|
|
62
65
|
}
|
|
63
66
|
/**
|
|
64
67
|
* Return type for the useRealtimeChat hook.
|
|
@@ -102,8 +105,5 @@ export interface UseRealtimeChatReturn {
|
|
|
102
105
|
getInputTimeDomainData: () => Uint8Array;
|
|
103
106
|
/** Get time domain data for output waveform */
|
|
104
107
|
getOutputTimeDomainData: () => Uint8Array;
|
|
105
|
-
|
|
106
|
-
vadMode: 'server' | 'semantic' | 'manual';
|
|
107
|
-
/** Change VAD mode at runtime */
|
|
108
|
-
setVADMode: (mode: 'server' | 'semantic' | 'manual') => void;
|
|
108
|
+
updateSession: (config: RealtimeSessionConfig) => void;
|
|
109
109
|
}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AnyClientTool, InferSchemaType, ModelMessage, SchemaInput } from '@tanstack/ai/client';
|
|
2
|
-
import { AIDevtoolsDisplayOptions, ChatClientOptions, ChatClientState, ChatRequestBody, ClientContextOptionFromTools, ConnectionStatus, DistributedOmit, InferredClientContext, MultimodalContent, UIMessage } from '@tanstack/ai-client';
|
|
3
|
-
export type { ChatRequestBody, MultimodalContent, UIMessage };
|
|
2
|
+
import { AIDevtoolsDisplayOptions, ChatClientOptions, ChatClientState, ChatRequestBody, ClientContextOptionFromTools, ConnectionStatus, DistributedOmit, InferredClientContext, MultimodalContent, QueueConfig, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from '@tanstack/ai-client';
|
|
3
|
+
export type { ChatRequestBody, MultimodalContent, QueueConfig, QueuedMessage, QueueOption, QueueStrategy, SendMessageOptions, UIMessage, WhenBusy, };
|
|
4
4
|
/**
|
|
5
5
|
* Recursive partial — every property and every nested array element is optional.
|
|
6
6
|
* Used to type the in-flight `partial` value the hook exposes while a structured
|
|
@@ -35,7 +35,7 @@ export type DeepPartial<T> = T extends ReadonlyArray<infer U> ? Array<DeepPartia
|
|
|
35
35
|
* Note: Connection and body changes will recreate the ChatClient instance.
|
|
36
36
|
* To update these options, remount the component or use a key prop.
|
|
37
37
|
*/
|
|
38
|
-
export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>> = DistributedOmit<ChatClientOptions<TTools, TContext>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange' | 'context' | 'devtools'> & {
|
|
38
|
+
export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>> = DistributedOmit<ChatClientOptions<TTools, TContext>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange' | 'onQueueChange' | 'context' | 'devtools'> & {
|
|
39
39
|
/** Display options for TanStack AI Devtools. */
|
|
40
40
|
devtools?: AIDevtoolsDisplayOptions;
|
|
41
41
|
/**
|
|
@@ -81,8 +81,20 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, T
|
|
|
81
81
|
/**
|
|
82
82
|
* Send a message and get a response.
|
|
83
83
|
* Can be a simple string or multimodal content with images, audio, etc.
|
|
84
|
+
* By default, sends while busy are queued until the run settles successfully
|
|
85
|
+
* (`queue: 'drop'` restores the old drop-while-busy behavior).
|
|
86
|
+
* Pass `{ whenBusy }` to override the policy for a single send.
|
|
84
87
|
*/
|
|
85
|
-
sendMessage: (content: string | MultimodalContent) => Promise<void>;
|
|
88
|
+
sendMessage: (content: string | MultimodalContent, options?: SendMessageOptions) => Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Pending messages queued while the client is busy (streaming, claiming a
|
|
91
|
+
* send, or draining). Separate from `messages` until they drain.
|
|
92
|
+
*/
|
|
93
|
+
queue: Array<QueuedMessage>;
|
|
94
|
+
/**
|
|
95
|
+
* Cancel a queued message before it drains. No-op if already sent.
|
|
96
|
+
*/
|
|
97
|
+
cancelQueued: (id: string) => void;
|
|
86
98
|
/**
|
|
87
99
|
* Append a message to the conversation
|
|
88
100
|
*/
|
package/dist/esm/use-chat.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { AnyClientTool, SchemaInput } from '@tanstack/ai/client';
|
|
2
2
|
import { InferredClientContext } from '@tanstack/ai-client';
|
|
3
3
|
import { UseChatOptions, UseChatReturn } from './types.js';
|
|
4
|
-
export declare function useChat<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>>(options: UseChatOptions<TTools, TSchema, TContext>): UseChatReturn<TTools, TSchema>;
|
|
4
|
+
export declare function useChat<const TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>>(options: UseChatOptions<TTools, TSchema, TContext>): UseChatReturn<TTools, TSchema>;
|
package/dist/esm/use-chat.js
CHANGED
|
@@ -13,6 +13,7 @@ function useChat(options) {
|
|
|
13
13
|
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
14
14
|
const [connectionStatus, setConnectionStatus] = useState("disconnected");
|
|
15
15
|
const [sessionGenerating, setSessionGenerating] = useState(false);
|
|
16
|
+
const [queue, setQueue] = useState([]);
|
|
16
17
|
const messagesRef = useRef(
|
|
17
18
|
options.initialMessages || []
|
|
18
19
|
);
|
|
@@ -106,6 +107,13 @@ function useChat(options) {
|
|
|
106
107
|
onSessionGeneratingChange: (isGenerating) => {
|
|
107
108
|
if (activeClientRef.current !== instance) return;
|
|
108
109
|
setSessionGenerating(isGenerating);
|
|
110
|
+
},
|
|
111
|
+
...optionsRef.current.queue !== void 0 && {
|
|
112
|
+
queue: optionsRef.current.queue
|
|
113
|
+
},
|
|
114
|
+
onQueueChange: (nextQueue) => {
|
|
115
|
+
if (activeClientRef.current !== instance) return;
|
|
116
|
+
setQueue(nextQueue);
|
|
109
117
|
}
|
|
110
118
|
});
|
|
111
119
|
activeClientRef.current = instance;
|
|
@@ -133,6 +141,11 @@ function useChat(options) {
|
|
|
133
141
|
useEffect(() => {
|
|
134
142
|
client.updateOptions({ context: options.context });
|
|
135
143
|
}, [client, options.context]);
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (options.queue !== void 0) {
|
|
146
|
+
client.updateOptions({ queue: options.queue });
|
|
147
|
+
}
|
|
148
|
+
}, [client, options.queue]);
|
|
136
149
|
useEffect(() => {
|
|
137
150
|
if (options.live) {
|
|
138
151
|
client.subscribe();
|
|
@@ -163,8 +176,14 @@ function useChat(options) {
|
|
|
163
176
|
};
|
|
164
177
|
}, [client]);
|
|
165
178
|
const sendMessage = useCallback(
|
|
166
|
-
async (content) => {
|
|
167
|
-
await client.sendMessage(content);
|
|
179
|
+
async (content, sendOptions) => {
|
|
180
|
+
await client.sendMessage(content, void 0, sendOptions);
|
|
181
|
+
},
|
|
182
|
+
[client]
|
|
183
|
+
);
|
|
184
|
+
const cancelQueued = useCallback(
|
|
185
|
+
(id) => {
|
|
186
|
+
client.cancelQueued(id);
|
|
168
187
|
},
|
|
169
188
|
[client]
|
|
170
189
|
);
|
|
@@ -248,6 +267,8 @@ function useChat(options) {
|
|
|
248
267
|
clear,
|
|
249
268
|
addToolResult,
|
|
250
269
|
addToolApprovalResponse,
|
|
270
|
+
queue,
|
|
271
|
+
cancelQueued,
|
|
251
272
|
partial,
|
|
252
273
|
final
|
|
253
274
|
};
|
package/dist/esm/use-chat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type {\n AnyClientTool,\n InferSchemaType,\n ModelMessage,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type {\n ChatClientState,\n ConnectionStatus,\n InferredClientContext,\n StructuredOutputPart,\n} from '@tanstack/ai-client'\n\nimport type {\n DeepPartial,\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n>(\n options: UseChatOptions<TTools, TSchema, TContext>,\n): UseChatReturn<TTools, TSchema> {\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = useState(false)\n const [connectionStatus, setConnectionStatus] =\n useState<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = useState(false)\n\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n\n // Track current messages in a ref to preserve them when client is recreated\n const messagesRef = useRef<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const isFirstMountRef = useRef(true)\n const activeClientRef = useRef<ChatClient | null>(null)\n const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n\n // Update ref synchronously during render so it's always current when useMemo runs.\n messagesRef.current = messages\n\n // Track current options in a ref to avoid recreating client when options change\n const optionsRef = useRef<UseChatOptions<TTools, TSchema, TContext>>(options)\n optionsRef.current = options\n\n // Create ChatClient instance with callbacks to sync state\n const client = useMemo(() => {\n const messagesToUse = options.initialMessages || []\n isFirstMountRef.current = false\n\n // Build options with conditional spreads for fields whose source\n // type is `T | undefined` but the ChatClient target uses a strict\n // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects\n // assigning `undefined` to those, so we omit the key when absent.\n const initialOptions = optionsRef.current\n const transport = initialOptions.connection\n ? { connection: initialOptions.connection }\n : { fetcher: initialOptions.fetcher }\n\n const instance = new ChatClient<TTools, TContext>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n id: clientId,\n initialMessages: messagesToUse,\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.threadId !== undefined && {\n threadId: initialOptions.threadId,\n }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\n }),\n ...(initialOptions.persistence !== undefined && {\n persistence: initialOptions.persistence,\n }),\n ...(initialOptions.context !== undefined && {\n context: initialOptions.context,\n }),\n devtools: {\n ...initialOptions.devtools,\n framework: 'react',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n onResponse: (response) => {\n if (activeClientRef.current !== instance) return\n void optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk: StreamChunk) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onChunk?.(chunk)\n },\n onFinish: (message: UIMessage<TTools>) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onFinish?.(message)\n },\n onError: (error: Error) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onError?.(error)\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n onCustomEvent: (eventType, data, context) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n },\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n if (activeClientRef.current !== instance) return\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsLoading(newIsLoading)\n },\n onErrorChange: (newError: Error | undefined) => {\n if (activeClientRef.current !== instance) return\n setError(newError)\n },\n onStatusChange: (status: ChatClientState) => {\n if (activeClientRef.current !== instance) return\n setStatus(status)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n if (activeClientRef.current !== instance) return\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n if (activeClientRef.current !== instance) return\n setSessionGenerating(isGenerating)\n },\n })\n activeClientRef.current = instance\n return instance\n }, [clientId])\n\n useEffect(() => {\n const clientMessages = client.getMessages()\n if (clientMessages !== messagesRef.current) {\n setMessages(clientMessages)\n }\n }, [client])\n\n // Sync each wire-payload slot in its own effect so an unrelated option\n // changing doesn't re-run the others. `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT, so guard the optional\n // slots before passing them.\n useEffect(() => {\n client.updateOptions({ body: options.body })\n }, [client, options.body])\n\n useEffect(() => {\n if (options.forwardedProps !== undefined) {\n client.updateOptions({ forwardedProps: options.forwardedProps })\n }\n }, [client, options.forwardedProps])\n\n useEffect(() => {\n if (options.tools !== undefined) {\n client.updateOptions({ tools: options.tools })\n }\n }, [client, options.tools])\n\n useEffect(() => {\n client.updateOptions({ context: options.context })\n }, [client, options.context])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\n\n useEffect(() => {\n if (cleanupInvalidationRef.current) {\n clearTimeout(cleanupInvalidationRef.current)\n cleanupInvalidationRef.current = null\n }\n activeClientRef.current = client\n client.mountDevtools()\n\n return () => {\n cleanupInvalidationRef.current = setTimeout(() => {\n if (activeClientRef.current === client) {\n activeClientRef.current = null\n }\n cleanupInvalidationRef.current = null\n }, 0)\n // Subscribe/unsubscribe on `options.live` is owned by the dedicated\n // effect above. This cleanup only fires on unmount or client swap,\n // so read `live` through the ref to avoid disposing the client every\n // time `live` toggles.\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n client.dispose()\n }\n }, [client])\n\n const sendMessage = useCallback(\n async (content: string | MultimodalContent) => {\n await client.sendMessage(content)\n },\n [client],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n await client.append(message)\n },\n [client],\n )\n\n const reload = useCallback(async () => {\n await client.reload()\n }, [client])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n }, [client])\n\n const setMessagesManually = useCallback(\n (newMessages: Array<UIMessage<TTools>>) => {\n client.setMessagesManually(newMessages)\n },\n [client],\n )\n\n const addToolResult = useCallback(\n async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client.addToolResult(result)\n },\n [client],\n )\n\n const addToolApprovalResponse = useCallback(\n async (response: { id: string; approved: boolean }) => {\n await client.addToolApprovalResponse(response)\n },\n [client],\n )\n\n // The \"active\" structured-output part is the one on the assistant message\n // that follows the latest user message. No such message exists between\n // sendMessage() and the first chunk, so partial/final naturally read as\n // cleared. Historical parts on earlier assistant messages remain available\n // via `messages` directly.\n //\n // When there is NO user message yet (e.g. `initialMessages` contains only\n // a stale assistant turn or a system prompt) we deliberately return null\n // rather than scanning historical assistants — otherwise a `final` from a\n // previous session would leak into the hook value on first render.\n const renderedMessages = client.getMessages()\n\n const activeStructuredPart = useMemo<StructuredOutputPart | null>(() => {\n let lastUserIndex = -1\n for (let i = renderedMessages.length - 1; i >= 0; i--) {\n if (renderedMessages[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = renderedMessages.length - 1; i > lastUserIndex; i--) {\n const m = renderedMessages[i]\n if (m?.role !== 'assistant') continue\n const part = m.parts.find(\n (p): p is StructuredOutputPart => p.type === 'structured-output',\n )\n if (part) return part\n }\n return null\n }, [renderedMessages])\n\n const partial = useMemo<Partial>(() => {\n if (!activeStructuredPart) return {} as Partial\n const v = activeStructuredPart.partial ?? activeStructuredPart.data\n return (v ?? {}) as Partial\n }, [activeStructuredPart])\n\n const final = useMemo<Final | null>(() => {\n if (!activeStructuredPart || activeStructuredPart.status !== 'complete') {\n return null\n }\n return activeStructuredPart.data as Final\n }, [activeStructuredPart])\n\n // The runtime shape unconditionally exposes partial/final; the public\n // return type hides them when no outputSchema was supplied. TS can't\n // structurally narrow across that conditional, so the `as` is the seam.\n // eslint-disable-next-line no-restricted-syntax -- hook return shape diverges from generic UseChatReturn<TTools, TSchema> due to conditional type on TSchema; TS can't structurally narrow\n return {\n messages: renderedMessages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema>\n}\n"],"names":["error","status"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type {\n AnyClientTool,\n InferSchemaType,\n ModelMessage,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type {\n ChatClientState,\n ConnectionStatus,\n InferredClientContext,\n QueuedMessage,\n SendMessageOptions,\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 const TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n>(\n options: UseChatOptions<TTools, TSchema, TContext>,\n): UseChatReturn<TTools, TSchema> {\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = useState(false)\n const [connectionStatus, setConnectionStatus] =\n useState<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = useState(false)\n const [queue, setQueue] = useState<Array<QueuedMessage>>([])\n\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n\n // Track current messages in a ref to preserve them when client is recreated\n const messagesRef = useRef<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const isFirstMountRef = useRef(true)\n const activeClientRef = useRef<ChatClient | null>(null)\n const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n\n // Update ref synchronously during render so it's always current when useMemo runs.\n messagesRef.current = messages\n\n // Track current options in a ref to avoid recreating client when options change\n const optionsRef = useRef<UseChatOptions<TTools, TSchema, TContext>>(options)\n optionsRef.current = options\n\n // Create ChatClient instance with callbacks to sync state\n const client = useMemo(() => {\n const messagesToUse = options.initialMessages || []\n isFirstMountRef.current = false\n\n // Build options with conditional spreads for fields whose source\n // type is `T | undefined` but the ChatClient target uses a strict\n // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects\n // assigning `undefined` to those, so we omit the key when absent.\n const initialOptions = optionsRef.current\n const transport = initialOptions.connection\n ? { connection: initialOptions.connection }\n : { fetcher: initialOptions.fetcher }\n\n const instance = new ChatClient<TTools, TContext>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n id: clientId,\n initialMessages: messagesToUse,\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.threadId !== undefined && {\n threadId: initialOptions.threadId,\n }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\n }),\n ...(initialOptions.persistence !== undefined && {\n persistence: initialOptions.persistence,\n }),\n ...(initialOptions.context !== undefined && {\n context: initialOptions.context,\n }),\n devtools: {\n ...initialOptions.devtools,\n framework: 'react',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n onResponse: (response) => {\n if (activeClientRef.current !== instance) return\n void optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk: StreamChunk) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onChunk?.(chunk)\n },\n onFinish: (message: UIMessage<TTools>) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onFinish?.(message)\n },\n onError: (error: Error) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onError?.(error)\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n onCustomEvent: (eventType, data, context) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n },\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n if (activeClientRef.current !== instance) return\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsLoading(newIsLoading)\n },\n onErrorChange: (newError: Error | undefined) => {\n if (activeClientRef.current !== instance) return\n setError(newError)\n },\n onStatusChange: (status: ChatClientState) => {\n if (activeClientRef.current !== instance) return\n setStatus(status)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n if (activeClientRef.current !== instance) return\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n if (activeClientRef.current !== instance) return\n setSessionGenerating(isGenerating)\n },\n ...(optionsRef.current.queue !== undefined && {\n queue: optionsRef.current.queue,\n }),\n onQueueChange: (nextQueue: Array<QueuedMessage>) => {\n if (activeClientRef.current !== instance) return\n setQueue(nextQueue)\n },\n })\n activeClientRef.current = instance\n return instance\n }, [clientId])\n\n useEffect(() => {\n const clientMessages = client.getMessages()\n if (clientMessages !== messagesRef.current) {\n setMessages(clientMessages)\n }\n }, [client])\n\n // Sync each wire-payload slot in its own effect so an unrelated option\n // changing doesn't re-run the others. `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT, so guard the optional\n // slots before passing them.\n useEffect(() => {\n client.updateOptions({ body: options.body })\n }, [client, options.body])\n\n useEffect(() => {\n if (options.forwardedProps !== undefined) {\n client.updateOptions({ forwardedProps: options.forwardedProps })\n }\n }, [client, options.forwardedProps])\n\n useEffect(() => {\n if (options.tools !== undefined) {\n client.updateOptions({ tools: options.tools })\n }\n }, [client, options.tools])\n\n useEffect(() => {\n client.updateOptions({ context: options.context })\n }, [client, options.context])\n\n useEffect(() => {\n if (options.queue !== undefined) {\n client.updateOptions({ queue: options.queue })\n }\n }, [client, options.queue])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\n\n useEffect(() => {\n if (cleanupInvalidationRef.current) {\n clearTimeout(cleanupInvalidationRef.current)\n cleanupInvalidationRef.current = null\n }\n activeClientRef.current = client\n client.mountDevtools()\n\n return () => {\n cleanupInvalidationRef.current = setTimeout(() => {\n if (activeClientRef.current === client) {\n activeClientRef.current = null\n }\n cleanupInvalidationRef.current = null\n }, 0)\n // Subscribe/unsubscribe on `options.live` is owned by the dedicated\n // effect above. This cleanup only fires on unmount or client swap,\n // so read `live` through the ref to avoid disposing the client every\n // time `live` toggles.\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n client.dispose()\n }\n }, [client])\n\n const sendMessage = useCallback(\n async (\n content: string | MultimodalContent,\n sendOptions?: SendMessageOptions,\n ) => {\n await client.sendMessage(content, undefined, sendOptions)\n },\n [client],\n )\n\n const cancelQueued = useCallback(\n (id: string) => {\n client.cancelQueued(id)\n },\n [client],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n await client.append(message)\n },\n [client],\n )\n\n const reload = useCallback(async () => {\n await client.reload()\n }, [client])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n }, [client])\n\n const setMessagesManually = useCallback(\n (newMessages: Array<UIMessage<TTools>>) => {\n client.setMessagesManually(newMessages)\n },\n [client],\n )\n\n const addToolResult = useCallback(\n async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client.addToolResult(result)\n },\n [client],\n )\n\n const addToolApprovalResponse = useCallback(\n async (response: { id: string; approved: boolean }) => {\n await client.addToolApprovalResponse(response)\n },\n [client],\n )\n\n // The \"active\" structured-output part is the one on the assistant message\n // that follows the latest user message. No such message exists between\n // sendMessage() and the first chunk, so partial/final naturally read as\n // cleared. Historical parts on earlier assistant messages remain available\n // via `messages` directly.\n //\n // When there is NO user message yet (e.g. `initialMessages` contains only\n // a stale assistant turn or a system prompt) we deliberately return null\n // rather than scanning historical assistants — otherwise a `final` from a\n // previous session would leak into the hook value on first render.\n const renderedMessages = client.getMessages()\n\n const activeStructuredPart = useMemo<StructuredOutputPart | null>(() => {\n let lastUserIndex = -1\n for (let i = renderedMessages.length - 1; i >= 0; i--) {\n if (renderedMessages[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = renderedMessages.length - 1; i > lastUserIndex; i--) {\n const m = renderedMessages[i]\n if (m?.role !== 'assistant') continue\n const part = m.parts.find(\n (p): p is StructuredOutputPart => p.type === 'structured-output',\n )\n if (part) return part\n }\n return null\n }, [renderedMessages])\n\n const partial = useMemo<Partial>(() => {\n if (!activeStructuredPart) return {} as Partial\n const v = activeStructuredPart.partial ?? activeStructuredPart.data\n return (v ?? {}) as Partial\n }, [activeStructuredPart])\n\n const final = useMemo<Final | null>(() => {\n if (!activeStructuredPart || activeStructuredPart.status !== 'complete') {\n return null\n }\n return activeStructuredPart.data as Final\n }, [activeStructuredPart])\n\n // The runtime shape unconditionally exposes partial/final; the public\n // return type hides them when no outputSchema was supplied. TS can't\n // structurally narrow across that conditional, so the `as` is the seam.\n // eslint-disable-next-line no-restricted-syntax -- hook return shape diverges from generic UseChatReturn<TTools, TSchema> due to conditional type on TSchema; TS can't structurally narrow\n return {\n messages: renderedMessages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n queue,\n cancelQueued,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema>\n}\n"],"names":["error","status"],"mappings":"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"}
|
|
@@ -11,9 +11,6 @@ function useRealtimeChat(options) {
|
|
|
11
11
|
const [error, setError] = useState(null);
|
|
12
12
|
const [inputLevel, setInputLevel] = useState(0);
|
|
13
13
|
const [outputLevel, setOutputLevel] = useState(0);
|
|
14
|
-
const [vadMode, setVADModeState] = useState(
|
|
15
|
-
options.vadMode ?? "server"
|
|
16
|
-
);
|
|
17
14
|
const clientRef = useRef(null);
|
|
18
15
|
const optionsRef = useRef(options);
|
|
19
16
|
optionsRef.current = options;
|
|
@@ -58,6 +55,12 @@ function useRealtimeChat(options) {
|
|
|
58
55
|
setMessages((prev) => [...prev, message]);
|
|
59
56
|
optionsRef.current.onMessage?.(message);
|
|
60
57
|
},
|
|
58
|
+
onUsage(usage) {
|
|
59
|
+
optionsRef.current.onUsage?.(usage);
|
|
60
|
+
},
|
|
61
|
+
onGoAway(timeLeft) {
|
|
62
|
+
optionsRef.current.onGoAway?.(timeLeft);
|
|
63
|
+
},
|
|
61
64
|
onError: (err) => {
|
|
62
65
|
setError(err);
|
|
63
66
|
optionsRef.current.onError?.(err);
|
|
@@ -146,12 +149,9 @@ function useRealtimeChat(options) {
|
|
|
146
149
|
const getOutputTimeDomainData = useCallback(() => {
|
|
147
150
|
return clientRef.current?.audio?.getOutputTimeDomainData() ?? emptyTimeDomainData;
|
|
148
151
|
}, []);
|
|
149
|
-
const
|
|
150
|
-
(
|
|
151
|
-
|
|
152
|
-
},
|
|
153
|
-
[]
|
|
154
|
-
);
|
|
152
|
+
const updateSession = useCallback((config) => {
|
|
153
|
+
clientRef.current?.updateSession(config);
|
|
154
|
+
}, []);
|
|
155
155
|
return {
|
|
156
156
|
// Connection state
|
|
157
157
|
status,
|
|
@@ -178,9 +178,8 @@ function useRealtimeChat(options) {
|
|
|
178
178
|
getOutputFrequencyData,
|
|
179
179
|
getInputTimeDomainData,
|
|
180
180
|
getOutputTimeDomainData,
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
setVADMode
|
|
181
|
+
// Session control
|
|
182
|
+
updateSession
|
|
184
183
|
};
|
|
185
184
|
}
|
|
186
185
|
export {
|
|
@@ -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 // 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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"}
|
|
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 RealtimeSessionConfig,\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\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 onUsage(usage) {\n optionsRef.current.onUsage?.(usage)\n },\n onGoAway(timeLeft) {\n optionsRef.current.onGoAway?.(timeLeft)\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 const updateSession = useCallback((config: RealtimeSessionConfig) => {\n clientRef.current?.updateSession(config)\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 // Session control\n updateSession,\n }\n}\n"],"names":[],"mappings":";;AAcA,MAAM,qBAAqB,IAAI,WAAW,GAAG;AAC7C,MAAM,sBAAsB,IAAI,WAAW,GAAG,EAAE,KAAK,GAAG;AA0CjD,SAAS,gBACd,SACuB;AAEvB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAyB,MAAM;AAC3D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAuB,MAAM;AACrD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAiC,CAAA,CAAE;AACnE,QAAM,CAAC,uBAAuB,wBAAwB,IAAI,SAExD,IAAI;AACN,QAAM,CAAC,4BAA4B,6BAA6B,IAAI,SAElE,IAAI;AACN,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AACrD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,CAAC;AAC9C,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAGhD,QAAM,YAAY,OAA8B,IAAI;AACpD,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AACrB,QAAM,oBAAoB,OAAsB,IAAI;AAIpD,MAAI,CAAC,UAAU,SAAS;AAKtB,UAAM,UAAU,WAAW;AAC3B,cAAU,UAAU,IAAI,eAAe;AAAA,MACrC,UAAU,QAAQ;AAAA,MAClB,SAAS,QAAQ;AAAA,MACjB,GAAI,QAAQ,UAAU,UAAa,EAAE,OAAO,QAAQ,MAAA;AAAA,MACpD,GAAI,QAAQ,iBAAiB,UAAa;AAAA,QACxC,cAAc,QAAQ;AAAA,MAAA;AAAA,MAExB,GAAI,QAAQ,UAAU,UAAa,EAAE,OAAO,QAAQ,MAAA;AAAA,MACpD,GAAI,QAAQ,iBAAiB,UAAa;AAAA,QACxC,cAAc,QAAQ;AAAA,MAAA;AAAA,MAExB,GAAI,QAAQ,gBAAgB,UAAa;AAAA,QACvC,aAAa,QAAQ;AAAA,MAAA;AAAA,MAEvB,GAAI,QAAQ,YAAY,UAAa,EAAE,SAAS,QAAQ,QAAA;AAAA,MACxD,GAAI,QAAQ,qBAAqB,UAAa;AAAA,QAC5C,kBAAkB,QAAQ;AAAA,MAAA;AAAA,MAE5B,GAAI,QAAQ,gBAAgB,UAAa;AAAA,QACvC,aAAa,QAAQ;AAAA,MAAA;AAAA,MAEvB,GAAI,QAAQ,oBAAoB,UAAa;AAAA,QAC3C,iBAAiB,QAAQ;AAAA,MAAA;AAAA,MAE3B,GAAI,QAAQ,sBAAsB,UAAa;AAAA,QAC7C,mBAAmB,QAAQ;AAAA,MAAA;AAAA,MAE7B,gBAAgB,CAAC,cAAc;AAC7B,kBAAU,SAAS;AAAA,MACrB;AAAA,MACA,cAAc,CAAC,YAAY;AACzB,gBAAQ,OAAO;AACf,mBAAW,QAAQ,eAAe,OAAO;AAAA,MAC3C;AAAA,MACA,WAAW,CAAC,YAAY;AACtB,oBAAY,CAAC,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC;AACxC,mBAAW,QAAQ,YAAY,OAAO;AAAA,MACxC;AAAA,MACA,QAAQ,OAAO;AACb,mBAAW,QAAQ,UAAU,KAAK;AAAA,MACpC;AAAA,MACA,SAAS,UAAU;AACjB,mBAAW,QAAQ,WAAW,QAAQ;AAAA,MACxC;AAAA,MACA,SAAS,CAAC,QAAQ;AAChB,iBAAS,GAAG;AACZ,mBAAW,QAAQ,UAAU,GAAG;AAAA,MAClC;AAAA,MACA,WAAW,MAAM;AACf,iBAAS,IAAI;AACb,mBAAW,QAAQ,YAAA;AAAA,MACrB;AAAA,MACA,cAAc,MAAM;AAClB,mBAAW,QAAQ,eAAA;AAAA,MACrB;AAAA,MACA,eAAe,MAAM;AACnB,sCAA8B,IAAI;AAClC,mBAAW,QAAQ,gBAAA;AAAA,MACrB;AAAA,IAAA,CACD;AAGD,cAAU,QAAQ,cAAc,CAAC,UAAU;AACzC,+BAAyB,MAAM,qBAAqB;AACpD,oCAA8B,MAAM,0BAA0B;AAAA,IAChE,CAAC;AAAA,EACH;AAEA,QAAM,SAAS,UAAU;AAGzB,YAAU,MAAM;AACd,aAAS,eAAe;AACtB,UAAI,UAAU,SAAS,OAAO;AAC5B,sBAAc,UAAU,QAAQ,MAAM,UAAU;AAChD,uBAAe,UAAU,QAAQ,MAAM,WAAW;AAAA,MACpD;AACA,wBAAkB,UAAU,sBAAsB,YAAY;AAAA,IAChE;AAEA,QAAI,WAAW,aAAa;AAC1B,mBAAA;AAAA,IACF;AAEA,WAAO,MAAM;AACX,UAAI,kBAAkB,SAAS;AAC7B,6BAAqB,kBAAkB,OAAO;AAC9C,0BAAkB,UAAU;AAAA,MAC9B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAGX,YAAU,MAAM;AACd,WAAO,MAAM;AACX,gBAAU,SAAS,QAAA;AAAA,IACrB;AAAA,EACF,GAAG,CAAA,CAAE;AAGL,QAAM,UAAU,YAAY,YAAY;AACtC,aAAS,IAAI;AACb,gBAAY,CAAA,CAAE;AACd,6BAAyB,IAAI;AAC7B,kCAA8B,IAAI;AAClC,UAAM,OAAO,QAAA;AAAA,EACf,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,aAAa,YAAY,YAAY;AACzC,UAAM,OAAO,WAAA;AAAA,EACf,GAAG,CAAC,MAAM,CAAC;AAGX,QAAM,iBAAiB,YAAY,MAAM;AACvC,WAAO,eAAA;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,gBAAgB,YAAY,MAAM;AACtC,WAAO,cAAA;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,YAAY,YAAY,MAAM;AAClC,WAAO,UAAA;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAGX,QAAM,WAAW;AAAA,IACf,CAAC,SAAiB;AAChB,aAAO,SAAS,IAAI;AAAA,IACtB;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAIT,QAAM,YAAY;AAAA,IAChB,CAAC,WAAmB,aAAqB;AACvC,aAAO,UAAU,WAAW,QAAQ;AAAA,IACtC;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAIT,QAAM,wBAAwB,YAAY,MAAM;AAC9C,WACE,UAAU,SAAS,OAAO,sBAAA,KAA2B;AAAA,EAEzD,GAAG,CAAA,CAAE;AAEL,QAAM,yBAAyB,YAAY,MAAM;AAC/C,WACE,UAAU,SAAS,OAAO,uBAAA,KAA4B;AAAA,EAE1D,GAAG,CAAA,CAAE;AAEL,QAAM,yBAAyB,YAAY,MAAM;AAC/C,WACE,UAAU,SAAS,OAAO,uBAAA,KAA4B;AAAA,EAE1D,GAAG,CAAA,CAAE;AAEL,QAAM,0BAA0B,YAAY,MAAM;AAChD,WACE,UAAU,SAAS,OAAO,wBAAA,KAA6B;AAAA,EAE3D,GAAG,CAAA,CAAE;AAEL,QAAM,gBAAgB,YAAY,CAAC,WAAkC;AACnE,cAAU,SAAS,cAAc,MAAM;AAAA,EACzC,GAAG,CAAA,CAAE;AAEL,SAAO;AAAA;AAAA,IAEL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGA;AAAA;AAAA,IAGA;AAAA;AAAA,IAGA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGA;AAAA,EAAA;AAEJ;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "React hooks for TanStack AI streaming chat, realtime voice, structured outputs, and media generation.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
"media-generation"
|
|
50
50
|
],
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@tanstack/ai-client": "0.
|
|
52
|
+
"@tanstack/ai-client": "0.22.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@mcp-ui/client": "^7",
|
|
56
56
|
"@types/react": ">=18.0.0",
|
|
57
57
|
"react": ">=18.0.0",
|
|
58
|
-
"@tanstack/ai": "^0.
|
|
58
|
+
"@tanstack/ai": "^0.41.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependenciesMeta": {
|
|
61
61
|
"@mcp-ui/client": {
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"jsdom": "^27.2.0",
|
|
72
72
|
"react": "^19.2.3",
|
|
73
73
|
"vite": "^7.3.3",
|
|
74
|
-
"@tanstack/ai": "0.
|
|
74
|
+
"@tanstack/ai": "0.41.0"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
77
77
|
"clean": "premove ./build ./dist",
|
package/src/index.ts
CHANGED
package/src/realtime-types.ts
CHANGED
|
@@ -2,8 +2,10 @@ import type {
|
|
|
2
2
|
AnyClientTool,
|
|
3
3
|
RealtimeMessage,
|
|
4
4
|
RealtimeMode,
|
|
5
|
+
RealtimeSessionConfig,
|
|
5
6
|
RealtimeStatus,
|
|
6
7
|
RealtimeToken,
|
|
8
|
+
UsageInfo,
|
|
7
9
|
} from '@tanstack/ai'
|
|
8
10
|
import type { RealtimeAdapter } from '@tanstack/ai-client'
|
|
9
11
|
|
|
@@ -79,6 +81,9 @@ export interface UseRealtimeChatOptions {
|
|
|
79
81
|
onMessage?: (message: RealtimeMessage) => void
|
|
80
82
|
onModeChange?: (mode: RealtimeMode) => void
|
|
81
83
|
onInterrupted?: () => void
|
|
84
|
+
onUsage?: (usage: UsageInfo) => void
|
|
85
|
+
onGoAway?: (timeLeft?: string) => void
|
|
86
|
+
onStatusChange?: (status: RealtimeStatus) => void
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
/**
|
|
@@ -135,9 +140,6 @@ export interface UseRealtimeChatReturn {
|
|
|
135
140
|
/** Get time domain data for output waveform */
|
|
136
141
|
getOutputTimeDomainData: () => Uint8Array
|
|
137
142
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
vadMode: 'server' | 'semantic' | 'manual'
|
|
141
|
-
/** Change VAD mode at runtime */
|
|
142
|
-
setVADMode: (mode: 'server' | 'semantic' | 'manual') => void
|
|
143
|
+
// Session control
|
|
144
|
+
updateSession: (config: RealtimeSessionConfig) => void
|
|
143
145
|
}
|
package/src/types.ts
CHANGED
|
@@ -14,11 +14,27 @@ import type {
|
|
|
14
14
|
DistributedOmit,
|
|
15
15
|
InferredClientContext,
|
|
16
16
|
MultimodalContent,
|
|
17
|
+
QueueConfig,
|
|
18
|
+
QueueOption,
|
|
19
|
+
QueueStrategy,
|
|
20
|
+
QueuedMessage,
|
|
21
|
+
SendMessageOptions,
|
|
17
22
|
UIMessage,
|
|
23
|
+
WhenBusy,
|
|
18
24
|
} from '@tanstack/ai-client'
|
|
19
25
|
|
|
20
26
|
// Re-export types from ai-client
|
|
21
|
-
export type {
|
|
27
|
+
export type {
|
|
28
|
+
ChatRequestBody,
|
|
29
|
+
MultimodalContent,
|
|
30
|
+
QueueConfig,
|
|
31
|
+
QueuedMessage,
|
|
32
|
+
QueueOption,
|
|
33
|
+
QueueStrategy,
|
|
34
|
+
SendMessageOptions,
|
|
35
|
+
UIMessage,
|
|
36
|
+
WhenBusy,
|
|
37
|
+
}
|
|
22
38
|
|
|
23
39
|
/**
|
|
24
40
|
* Recursive partial — every property and every nested array element is optional.
|
|
@@ -71,6 +87,7 @@ export type UseChatOptions<
|
|
|
71
87
|
| 'onSubscriptionChange'
|
|
72
88
|
| 'onConnectionStatusChange'
|
|
73
89
|
| 'onSessionGeneratingChange'
|
|
90
|
+
| 'onQueueChange'
|
|
74
91
|
| 'context'
|
|
75
92
|
| 'devtools'
|
|
76
93
|
> & {
|
|
@@ -134,8 +151,25 @@ interface BaseUseChatReturn<
|
|
|
134
151
|
/**
|
|
135
152
|
* Send a message and get a response.
|
|
136
153
|
* Can be a simple string or multimodal content with images, audio, etc.
|
|
154
|
+
* By default, sends while busy are queued until the run settles successfully
|
|
155
|
+
* (`queue: 'drop'` restores the old drop-while-busy behavior).
|
|
156
|
+
* Pass `{ whenBusy }` to override the policy for a single send.
|
|
157
|
+
*/
|
|
158
|
+
sendMessage: (
|
|
159
|
+
content: string | MultimodalContent,
|
|
160
|
+
options?: SendMessageOptions,
|
|
161
|
+
) => Promise<void>
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Pending messages queued while the client is busy (streaming, claiming a
|
|
165
|
+
* send, or draining). Separate from `messages` until they drain.
|
|
166
|
+
*/
|
|
167
|
+
queue: Array<QueuedMessage>
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Cancel a queued message before it drains. No-op if already sent.
|
|
137
171
|
*/
|
|
138
|
-
|
|
172
|
+
cancelQueued: (id: string) => void
|
|
139
173
|
|
|
140
174
|
/**
|
|
141
175
|
* Append a message to the conversation
|
package/src/use-chat.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
ChatClientState,
|
|
13
13
|
ConnectionStatus,
|
|
14
14
|
InferredClientContext,
|
|
15
|
+
QueuedMessage,
|
|
16
|
+
SendMessageOptions,
|
|
15
17
|
StructuredOutputPart,
|
|
16
18
|
} from '@tanstack/ai-client'
|
|
17
19
|
|
|
@@ -24,7 +26,7 @@ import type {
|
|
|
24
26
|
} from './types'
|
|
25
27
|
|
|
26
28
|
export function useChat<
|
|
27
|
-
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
29
|
+
const TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
28
30
|
TSchema extends SchemaInput | undefined = undefined,
|
|
29
31
|
TContext = InferredClientContext<TTools>,
|
|
30
32
|
>(
|
|
@@ -43,6 +45,7 @@ export function useChat<
|
|
|
43
45
|
const [connectionStatus, setConnectionStatus] =
|
|
44
46
|
useState<ConnectionStatus>('disconnected')
|
|
45
47
|
const [sessionGenerating, setSessionGenerating] = useState(false)
|
|
48
|
+
const [queue, setQueue] = useState<Array<QueuedMessage>>([])
|
|
46
49
|
|
|
47
50
|
type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>
|
|
48
51
|
type Final = InferSchemaType<NonNullable<TSchema>>
|
|
@@ -156,6 +159,13 @@ export function useChat<
|
|
|
156
159
|
if (activeClientRef.current !== instance) return
|
|
157
160
|
setSessionGenerating(isGenerating)
|
|
158
161
|
},
|
|
162
|
+
...(optionsRef.current.queue !== undefined && {
|
|
163
|
+
queue: optionsRef.current.queue,
|
|
164
|
+
}),
|
|
165
|
+
onQueueChange: (nextQueue: Array<QueuedMessage>) => {
|
|
166
|
+
if (activeClientRef.current !== instance) return
|
|
167
|
+
setQueue(nextQueue)
|
|
168
|
+
},
|
|
159
169
|
})
|
|
160
170
|
activeClientRef.current = instance
|
|
161
171
|
return instance
|
|
@@ -192,6 +202,12 @@ export function useChat<
|
|
|
192
202
|
client.updateOptions({ context: options.context })
|
|
193
203
|
}, [client, options.context])
|
|
194
204
|
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (options.queue !== undefined) {
|
|
207
|
+
client.updateOptions({ queue: options.queue })
|
|
208
|
+
}
|
|
209
|
+
}, [client, options.queue])
|
|
210
|
+
|
|
195
211
|
useEffect(() => {
|
|
196
212
|
if (options.live) {
|
|
197
213
|
client.subscribe()
|
|
@@ -229,8 +245,18 @@ export function useChat<
|
|
|
229
245
|
}, [client])
|
|
230
246
|
|
|
231
247
|
const sendMessage = useCallback(
|
|
232
|
-
async (
|
|
233
|
-
|
|
248
|
+
async (
|
|
249
|
+
content: string | MultimodalContent,
|
|
250
|
+
sendOptions?: SendMessageOptions,
|
|
251
|
+
) => {
|
|
252
|
+
await client.sendMessage(content, undefined, sendOptions)
|
|
253
|
+
},
|
|
254
|
+
[client],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
const cancelQueued = useCallback(
|
|
258
|
+
(id: string) => {
|
|
259
|
+
client.cancelQueued(id)
|
|
234
260
|
},
|
|
235
261
|
[client],
|
|
236
262
|
)
|
|
@@ -346,6 +372,8 @@ export function useChat<
|
|
|
346
372
|
clear,
|
|
347
373
|
addToolResult,
|
|
348
374
|
addToolApprovalResponse,
|
|
375
|
+
queue,
|
|
376
|
+
cancelQueued,
|
|
349
377
|
partial,
|
|
350
378
|
final,
|
|
351
379
|
} as unknown as UseChatReturn<TTools, TSchema>
|
package/src/use-realtime-chat.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { RealtimeClient } from '@tanstack/ai-client'
|
|
|
3
3
|
import type {
|
|
4
4
|
RealtimeMessage,
|
|
5
5
|
RealtimeMode,
|
|
6
|
+
RealtimeSessionConfig,
|
|
6
7
|
RealtimeStatus,
|
|
7
8
|
} from '@tanstack/ai'
|
|
8
9
|
import type {
|
|
@@ -70,9 +71,6 @@ export function useRealtimeChat(
|
|
|
70
71
|
const [error, setError] = useState<Error | null>(null)
|
|
71
72
|
const [inputLevel, setInputLevel] = useState(0)
|
|
72
73
|
const [outputLevel, setOutputLevel] = useState(0)
|
|
73
|
-
const [vadMode, setVADModeState] = useState<'server' | 'semantic' | 'manual'>(
|
|
74
|
-
options.vadMode ?? 'server',
|
|
75
|
-
)
|
|
76
74
|
|
|
77
75
|
// Refs
|
|
78
76
|
const clientRef = useRef<RealtimeClient | null>(null)
|
|
@@ -126,6 +124,12 @@ export function useRealtimeChat(
|
|
|
126
124
|
setMessages((prev) => [...prev, message])
|
|
127
125
|
optionsRef.current.onMessage?.(message)
|
|
128
126
|
},
|
|
127
|
+
onUsage(usage) {
|
|
128
|
+
optionsRef.current.onUsage?.(usage)
|
|
129
|
+
},
|
|
130
|
+
onGoAway(timeLeft) {
|
|
131
|
+
optionsRef.current.onGoAway?.(timeLeft)
|
|
132
|
+
},
|
|
129
133
|
onError: (err) => {
|
|
130
134
|
setError(err)
|
|
131
135
|
optionsRef.current.onError?.(err)
|
|
@@ -248,14 +252,9 @@ export function useRealtimeChat(
|
|
|
248
252
|
)
|
|
249
253
|
}, [])
|
|
250
254
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
setVADModeState(newMode)
|
|
255
|
-
// TODO: Update session config if connected
|
|
256
|
-
},
|
|
257
|
-
[],
|
|
258
|
-
)
|
|
255
|
+
const updateSession = useCallback((config: RealtimeSessionConfig) => {
|
|
256
|
+
clientRef.current?.updateSession(config)
|
|
257
|
+
}, [])
|
|
259
258
|
|
|
260
259
|
return {
|
|
261
260
|
// Connection state
|
|
@@ -289,8 +288,7 @@ export function useRealtimeChat(
|
|
|
289
288
|
getInputTimeDomainData,
|
|
290
289
|
getOutputTimeDomainData,
|
|
291
290
|
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
setVADMode,
|
|
291
|
+
// Session control
|
|
292
|
+
updateSession,
|
|
295
293
|
}
|
|
296
294
|
}
|