@tanstack/ai-react 0.18.1 → 0.19.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.
Files changed (49) hide show
  1. package/README.md +15 -1
  2. package/dist/esm/index.d.ts +1 -1
  3. package/dist/esm/index.js +2 -23
  4. package/dist/esm/mcp-app-resource.js +45 -45
  5. package/dist/esm/mcp-app-resource.js.map +1 -1
  6. package/dist/esm/mcp-apps.js +1 -4
  7. package/dist/esm/types.d.ts +29 -3
  8. package/dist/esm/use-audio-recorder.js +41 -46
  9. package/dist/esm/use-audio-recorder.js.map +1 -1
  10. package/dist/esm/use-chat.js +330 -275
  11. package/dist/esm/use-chat.js.map +1 -1
  12. package/dist/esm/use-generate-audio.d.ts +50 -4
  13. package/dist/esm/use-generate-audio.js +47 -23
  14. package/dist/esm/use-generate-audio.js.map +1 -1
  15. package/dist/esm/use-generate-image.d.ts +50 -4
  16. package/dist/esm/use-generate-image.js +49 -23
  17. package/dist/esm/use-generate-image.js.map +1 -1
  18. package/dist/esm/use-generate-speech.d.ts +50 -4
  19. package/dist/esm/use-generate-speech.js +43 -23
  20. package/dist/esm/use-generate-speech.js.map +1 -1
  21. package/dist/esm/use-generate-video.d.ts +50 -4
  22. package/dist/esm/use-generate-video.js +141 -103
  23. package/dist/esm/use-generate-video.js.map +1 -1
  24. package/dist/esm/use-generation.d.ts +59 -6
  25. package/dist/esm/use-generation.js +113 -90
  26. package/dist/esm/use-generation.js.map +1 -1
  27. package/dist/esm/use-mcp-app-bridge.js +46 -23
  28. package/dist/esm/use-mcp-app-bridge.js.map +1 -1
  29. package/dist/esm/use-realtime-chat.js +185 -185
  30. package/dist/esm/use-realtime-chat.js.map +1 -1
  31. package/dist/esm/use-summarize.d.ts +50 -4
  32. package/dist/esm/use-summarize.js +46 -23
  33. package/dist/esm/use-summarize.js.map +1 -1
  34. package/dist/esm/use-transcription.d.ts +50 -4
  35. package/dist/esm/use-transcription.js +51 -20
  36. package/dist/esm/use-transcription.js.map +1 -1
  37. package/package.json +7 -7
  38. package/src/index.ts +10 -0
  39. package/src/types.ts +44 -0
  40. package/src/use-chat.ts +221 -35
  41. package/src/use-generate-audio.ts +64 -17
  42. package/src/use-generate-image.ts +64 -17
  43. package/src/use-generate-speech.ts +64 -17
  44. package/src/use-generate-video.ts +99 -18
  45. package/src/use-generation.ts +112 -18
  46. package/src/use-summarize.ts +64 -17
  47. package/src/use-transcription.ts +60 -18
  48. package/dist/esm/index.js.map +0 -1
  49. package/dist/esm/mcp-apps.js.map +0 -1
@@ -1,188 +1,188 @@
1
- import { useState, useRef, useEffect, useCallback } from "react";
2
1
  import { RealtimeClient } from "@tanstack/ai-client";
3
- const emptyFrequencyData = new Uint8Array(128);
4
- const emptyTimeDomainData = new Uint8Array(128).fill(128);
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ //#region src/use-realtime-chat.ts
4
+ var emptyFrequencyData = /* @__PURE__ */ new Uint8Array(128);
5
+ var emptyTimeDomainData = (/* @__PURE__ */ new Uint8Array(128)).fill(128);
6
+ /**
7
+ * React hook for realtime voice conversations.
8
+ *
9
+ * Provides a simple interface for voice-to-voice AI interactions
10
+ * with support for multiple providers (OpenAI, ElevenLabs, etc.).
11
+ *
12
+ * @param options - Configuration options including adapter and callbacks
13
+ * @returns Hook return value with state and control methods
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { useRealtimeChat } from '@tanstack/ai-react'
18
+ * import { openaiRealtime } from '@tanstack/ai-openai'
19
+ *
20
+ * function VoiceChat() {
21
+ * const {
22
+ * status,
23
+ * mode,
24
+ * messages,
25
+ * connect,
26
+ * disconnect,
27
+ * inputLevel,
28
+ * outputLevel,
29
+ * } = useRealtimeChat({
30
+ * getToken: () => fetch('/api/realtime-token').then(r => r.json()),
31
+ * adapter: openaiRealtime(),
32
+ * })
33
+ *
34
+ * return (
35
+ * <div>
36
+ * <p>Status: {status}</p>
37
+ * <p>Mode: {mode}</p>
38
+ * <button onClick={status === 'idle' ? connect : disconnect}>
39
+ * {status === 'idle' ? 'Start' : 'Stop'}
40
+ * </button>
41
+ * </div>
42
+ * )
43
+ * }
44
+ * ```
45
+ */
5
46
  function useRealtimeChat(options) {
6
- const [status, setStatus] = useState("idle");
7
- const [mode, setMode] = useState("idle");
8
- const [messages, setMessages] = useState([]);
9
- const [pendingUserTranscript, setPendingUserTranscript] = useState(null);
10
- const [pendingAssistantTranscript, setPendingAssistantTranscript] = useState(null);
11
- const [error, setError] = useState(null);
12
- const [inputLevel, setInputLevel] = useState(0);
13
- const [outputLevel, setOutputLevel] = useState(0);
14
- const clientRef = useRef(null);
15
- const optionsRef = useRef(options);
16
- optionsRef.current = options;
17
- const animationFrameRef = useRef(null);
18
- if (!clientRef.current) {
19
- const initial = optionsRef.current;
20
- clientRef.current = new RealtimeClient({
21
- getToken: initial.getToken,
22
- adapter: initial.adapter,
23
- ...initial.tools !== void 0 && { tools: initial.tools },
24
- ...initial.instructions !== void 0 && {
25
- instructions: initial.instructions
26
- },
27
- ...initial.voice !== void 0 && { voice: initial.voice },
28
- ...initial.autoPlayback !== void 0 && {
29
- autoPlayback: initial.autoPlayback
30
- },
31
- ...initial.autoCapture !== void 0 && {
32
- autoCapture: initial.autoCapture
33
- },
34
- ...initial.vadMode !== void 0 && { vadMode: initial.vadMode },
35
- ...initial.outputModalities !== void 0 && {
36
- outputModalities: initial.outputModalities
37
- },
38
- ...initial.temperature !== void 0 && {
39
- temperature: initial.temperature
40
- },
41
- ...initial.maxOutputTokens !== void 0 && {
42
- maxOutputTokens: initial.maxOutputTokens
43
- },
44
- ...initial.semanticEagerness !== void 0 && {
45
- semanticEagerness: initial.semanticEagerness
46
- },
47
- onStatusChange: (newStatus) => {
48
- setStatus(newStatus);
49
- },
50
- onModeChange: (newMode) => {
51
- setMode(newMode);
52
- optionsRef.current.onModeChange?.(newMode);
53
- },
54
- onMessage: (message) => {
55
- setMessages((prev) => [...prev, message]);
56
- optionsRef.current.onMessage?.(message);
57
- },
58
- onUsage(usage) {
59
- optionsRef.current.onUsage?.(usage);
60
- },
61
- onGoAway(timeLeft) {
62
- optionsRef.current.onGoAway?.(timeLeft);
63
- },
64
- onError: (err) => {
65
- setError(err);
66
- optionsRef.current.onError?.(err);
67
- },
68
- onConnect: () => {
69
- setError(null);
70
- optionsRef.current.onConnect?.();
71
- },
72
- onDisconnect: () => {
73
- optionsRef.current.onDisconnect?.();
74
- },
75
- onInterrupted: () => {
76
- setPendingAssistantTranscript(null);
77
- optionsRef.current.onInterrupted?.();
78
- }
79
- });
80
- clientRef.current.onStateChange((state) => {
81
- setPendingUserTranscript(state.pendingUserTranscript);
82
- setPendingAssistantTranscript(state.pendingAssistantTranscript);
83
- });
84
- }
85
- const client = clientRef.current;
86
- useEffect(() => {
87
- function updateLevels() {
88
- if (clientRef.current?.audio) {
89
- setInputLevel(clientRef.current.audio.inputLevel);
90
- setOutputLevel(clientRef.current.audio.outputLevel);
91
- }
92
- animationFrameRef.current = requestAnimationFrame(updateLevels);
93
- }
94
- if (status === "connected") {
95
- updateLevels();
96
- }
97
- return () => {
98
- if (animationFrameRef.current) {
99
- cancelAnimationFrame(animationFrameRef.current);
100
- animationFrameRef.current = null;
101
- }
102
- };
103
- }, [status]);
104
- useEffect(() => {
105
- return () => {
106
- clientRef.current?.destroy();
107
- };
108
- }, []);
109
- const connect = useCallback(async () => {
110
- setError(null);
111
- setMessages([]);
112
- setPendingUserTranscript(null);
113
- setPendingAssistantTranscript(null);
114
- await client.connect();
115
- }, [client]);
116
- const disconnect = useCallback(async () => {
117
- await client.disconnect();
118
- }, [client]);
119
- const startListening = useCallback(() => {
120
- client.startListening();
121
- }, [client]);
122
- const stopListening = useCallback(() => {
123
- client.stopListening();
124
- }, [client]);
125
- const interrupt = useCallback(() => {
126
- client.interrupt();
127
- }, [client]);
128
- const sendText = useCallback(
129
- (text) => {
130
- client.sendText(text);
131
- },
132
- [client]
133
- );
134
- const sendImage = useCallback(
135
- (imageData, mimeType) => {
136
- client.sendImage(imageData, mimeType);
137
- },
138
- [client]
139
- );
140
- const getInputFrequencyData = useCallback(() => {
141
- return clientRef.current?.audio?.getInputFrequencyData() ?? emptyFrequencyData;
142
- }, []);
143
- const getOutputFrequencyData = useCallback(() => {
144
- return clientRef.current?.audio?.getOutputFrequencyData() ?? emptyFrequencyData;
145
- }, []);
146
- const getInputTimeDomainData = useCallback(() => {
147
- return clientRef.current?.audio?.getInputTimeDomainData() ?? emptyTimeDomainData;
148
- }, []);
149
- const getOutputTimeDomainData = useCallback(() => {
150
- return clientRef.current?.audio?.getOutputTimeDomainData() ?? emptyTimeDomainData;
151
- }, []);
152
- const updateSession = useCallback((config) => {
153
- clientRef.current?.updateSession(config);
154
- }, []);
155
- return {
156
- // Connection state
157
- status,
158
- error,
159
- connect,
160
- disconnect,
161
- // Conversation state
162
- mode,
163
- messages,
164
- pendingUserTranscript,
165
- pendingAssistantTranscript,
166
- // Voice control
167
- startListening,
168
- stopListening,
169
- interrupt,
170
- // Text input
171
- sendText,
172
- // Image input
173
- sendImage,
174
- // Audio visualization
175
- inputLevel,
176
- outputLevel,
177
- getInputFrequencyData,
178
- getOutputFrequencyData,
179
- getInputTimeDomainData,
180
- getOutputTimeDomainData,
181
- // Session control
182
- updateSession
183
- };
47
+ const [status, setStatus] = useState("idle");
48
+ const [mode, setMode] = useState("idle");
49
+ const [messages, setMessages] = useState([]);
50
+ const [pendingUserTranscript, setPendingUserTranscript] = useState(null);
51
+ const [pendingAssistantTranscript, setPendingAssistantTranscript] = useState(null);
52
+ const [error, setError] = useState(null);
53
+ const [inputLevel, setInputLevel] = useState(0);
54
+ const [outputLevel, setOutputLevel] = useState(0);
55
+ const clientRef = useRef(null);
56
+ const optionsRef = useRef(options);
57
+ optionsRef.current = options;
58
+ const animationFrameRef = useRef(null);
59
+ if (!clientRef.current) {
60
+ const initial = optionsRef.current;
61
+ clientRef.current = new RealtimeClient({
62
+ getToken: initial.getToken,
63
+ adapter: initial.adapter,
64
+ ...initial.tools !== void 0 && { tools: initial.tools },
65
+ ...initial.instructions !== void 0 && { instructions: initial.instructions },
66
+ ...initial.voice !== void 0 && { voice: initial.voice },
67
+ ...initial.autoPlayback !== void 0 && { autoPlayback: initial.autoPlayback },
68
+ ...initial.autoCapture !== void 0 && { autoCapture: initial.autoCapture },
69
+ ...initial.vadMode !== void 0 && { vadMode: initial.vadMode },
70
+ ...initial.outputModalities !== void 0 && { outputModalities: initial.outputModalities },
71
+ ...initial.temperature !== void 0 && { temperature: initial.temperature },
72
+ ...initial.maxOutputTokens !== void 0 && { maxOutputTokens: initial.maxOutputTokens },
73
+ ...initial.semanticEagerness !== void 0 && { semanticEagerness: initial.semanticEagerness },
74
+ onStatusChange: (newStatus) => {
75
+ setStatus(newStatus);
76
+ },
77
+ onModeChange: (newMode) => {
78
+ setMode(newMode);
79
+ optionsRef.current.onModeChange?.(newMode);
80
+ },
81
+ onMessage: (message) => {
82
+ setMessages((prev) => [...prev, message]);
83
+ optionsRef.current.onMessage?.(message);
84
+ },
85
+ onUsage(usage) {
86
+ optionsRef.current.onUsage?.(usage);
87
+ },
88
+ onGoAway(timeLeft) {
89
+ optionsRef.current.onGoAway?.(timeLeft);
90
+ },
91
+ onError: (err) => {
92
+ setError(err);
93
+ optionsRef.current.onError?.(err);
94
+ },
95
+ onConnect: () => {
96
+ setError(null);
97
+ optionsRef.current.onConnect?.();
98
+ },
99
+ onDisconnect: () => {
100
+ optionsRef.current.onDisconnect?.();
101
+ },
102
+ onInterrupted: () => {
103
+ setPendingAssistantTranscript(null);
104
+ optionsRef.current.onInterrupted?.();
105
+ }
106
+ });
107
+ clientRef.current.onStateChange((state) => {
108
+ setPendingUserTranscript(state.pendingUserTranscript);
109
+ setPendingAssistantTranscript(state.pendingAssistantTranscript);
110
+ });
111
+ }
112
+ const client = clientRef.current;
113
+ useEffect(() => {
114
+ function updateLevels() {
115
+ if (clientRef.current?.audio) {
116
+ setInputLevel(clientRef.current.audio.inputLevel);
117
+ setOutputLevel(clientRef.current.audio.outputLevel);
118
+ }
119
+ animationFrameRef.current = requestAnimationFrame(updateLevels);
120
+ }
121
+ if (status === "connected") updateLevels();
122
+ return () => {
123
+ if (animationFrameRef.current) {
124
+ cancelAnimationFrame(animationFrameRef.current);
125
+ animationFrameRef.current = null;
126
+ }
127
+ };
128
+ }, [status]);
129
+ useEffect(() => {
130
+ return () => {
131
+ clientRef.current?.destroy();
132
+ };
133
+ }, []);
134
+ return {
135
+ status,
136
+ error,
137
+ connect: useCallback(async () => {
138
+ setError(null);
139
+ setMessages([]);
140
+ setPendingUserTranscript(null);
141
+ setPendingAssistantTranscript(null);
142
+ await client.connect();
143
+ }, [client]),
144
+ disconnect: useCallback(async () => {
145
+ await client.disconnect();
146
+ }, [client]),
147
+ mode,
148
+ messages,
149
+ pendingUserTranscript,
150
+ pendingAssistantTranscript,
151
+ startListening: useCallback(() => {
152
+ client.startListening();
153
+ }, [client]),
154
+ stopListening: useCallback(() => {
155
+ client.stopListening();
156
+ }, [client]),
157
+ interrupt: useCallback(() => {
158
+ client.interrupt();
159
+ }, [client]),
160
+ sendText: useCallback((text) => {
161
+ client.sendText(text);
162
+ }, [client]),
163
+ sendImage: useCallback((imageData, mimeType) => {
164
+ client.sendImage(imageData, mimeType);
165
+ }, [client]),
166
+ inputLevel,
167
+ outputLevel,
168
+ getInputFrequencyData: useCallback(() => {
169
+ return clientRef.current?.audio?.getInputFrequencyData() ?? emptyFrequencyData;
170
+ }, []),
171
+ getOutputFrequencyData: useCallback(() => {
172
+ return clientRef.current?.audio?.getOutputFrequencyData() ?? emptyFrequencyData;
173
+ }, []),
174
+ getInputTimeDomainData: useCallback(() => {
175
+ return clientRef.current?.audio?.getInputTimeDomainData() ?? emptyTimeDomainData;
176
+ }, []),
177
+ getOutputTimeDomainData: useCallback(() => {
178
+ return clientRef.current?.audio?.getOutputTimeDomainData() ?? emptyTimeDomainData;
179
+ }, []),
180
+ updateSession: useCallback((config) => {
181
+ clientRef.current?.updateSession(config);
182
+ }, [])
183
+ };
184
184
  }
185
- export {
186
- useRealtimeChat
187
- };
188
- //# sourceMappingURL=use-realtime-chat.js.map
185
+ //#endregion
186
+ export { useRealtimeChat };
187
+
188
+ //# sourceMappingURL=use-realtime-chat.js.map
@@ -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 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":"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"}
1
+ {"version":3,"file":"use-realtime-chat.js","names":[],"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"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { StreamChunk, SummarizationResult } from '@tanstack/ai';
2
- import { AIDevtoolsDisplayOptions, ConnectConnectionAdapter, GenerationClientState, GenerationFetcher, InferGenerationOutputFromReturn, SummarizeGenerateInput } from '@tanstack/ai-client';
2
+ import { AIDevtoolsDisplayOptions, ConnectConnectionAdapter, GenerationClientState, GenerationFetcher, GenerationPersistenceOptions, InferGenerationOutputFromReturn, SummarizeGenerateInput } from '@tanstack/ai-client';
3
3
  /**
4
4
  * Options for the useSummarize hook.
5
5
  *
@@ -10,12 +10,51 @@ export interface UseSummarizeOptions<TOutput = SummarizationResult> {
10
10
  connection?: ConnectConnectionAdapter;
11
11
  /** Direct async function for summarization */
12
12
  fetcher?: GenerationFetcher<SummarizeGenerateInput, SummarizationResult>;
13
- /** Unique identifier for this generation instance */
13
+ /**
14
+ * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
15
+ */
14
16
  id?: string;
15
17
  /** Additional body parameters to send with connect-based adapter requests */
16
18
  body?: Record<string, any>;
17
19
  /** Display options for TanStack AI Devtools. */
18
20
  devtools?: AIDevtoolsDisplayOptions;
21
+ /**
22
+ * How this generation persists across reloads.
23
+ * - Omit / `false`: ephemeral, in-memory only.
24
+ * - `true`: server-driven — on mount the client hydrates the last generation
25
+ * for its `threadId` from the server (needs a connection with a
26
+ * `hydrateGeneration` handler) and repaints it; it never auto-starts a run.
27
+ */
28
+ persistence?: boolean;
29
+ /**
30
+ * The **scope** this generation belongs to: a stable, app-chosen name for the
31
+ * slot successive runs fill — not a link to a chat conversation.
32
+ *
33
+ * The hook starts empty and produces many runs over its life; each gets its
34
+ * own `runId`, but all belong to one scope. Persistence keys on this, so
35
+ * derive it from your own domain and keep it identical across reloads (e.g.
36
+ * `` `video-${videoId}-start-frame` ``). It is also sent as the AG-UI thread
37
+ * id on the wire, which the protocol requires.
38
+ *
39
+ * **Required whenever `persistence` is set** — an app that cannot name the
40
+ * scope has nothing to restore to. Optional for ephemeral generations, where
41
+ * it falls back to `id` purely to satisfy the wire.
42
+ */
43
+ threadId?: string;
44
+ /**
45
+ * Server-driven hydration handler for `persistence: true` when the
46
+ * connection doesn't carry one (e.g. alongside `fetcher`, or a `stream()` /
47
+ * `rpcStream()` adapter built without handlers) — typically a one-line
48
+ * server-function call. The connection's own handler takes precedence.
49
+ */
50
+ hydrateGeneration?: ConnectConnectionAdapter['hydrateGeneration'];
51
+ /**
52
+ * Re-attach handler that replays a run still generating to completion on
53
+ * mount, when the connection doesn't carry one. Without it, a restored
54
+ * `running` snapshot surfaces as an (interrupted) error. The connection's
55
+ * own handler takes precedence.
56
+ */
57
+ joinRun?: ConnectConnectionAdapter['joinRun'];
19
58
  /**
20
59
  * Callback when summarization is complete. Can optionally return a transformed value.
21
60
  *
@@ -51,6 +90,13 @@ export interface UseSummarizeReturn<TOutput = SummarizationResult> {
51
90
  stop: () => void;
52
91
  /** Clear result, error, and return to idle */
53
92
  reset: () => void;
93
+ /**
94
+ * The id of the generation job currently running, or `null` when nothing is in
95
+ * flight. Each call to `generate` is one job with its own id. Pass it to your
96
+ * own endpoint to cancel or poll the provider job — `stop()` only aborts the
97
+ * local stream, it does not stop work already running on the provider.
98
+ */
99
+ runId: string | null;
54
100
  }
55
101
  /**
56
102
  * React hook for summarizing text using AI models.
@@ -81,6 +127,6 @@ export interface UseSummarizeReturn<TOutput = SummarizationResult> {
81
127
  * }
82
128
  * ```
83
129
  */
84
- export declare function useSummarize<TTransformed = void>(options: Omit<UseSummarizeOptions, 'onResult'> & {
130
+ export declare function useSummarize<TTransformed = void>(options: Omit<UseSummarizeOptions, 'onResult' | 'persistence' | 'threadId' | 'id'> & {
85
131
  onResult?: (result: SummarizationResult) => TTransformed;
86
- }): UseSummarizeReturn<InferGenerationOutputFromReturn<SummarizationResult, TTransformed>>;
132
+ } & GenerationPersistenceOptions): UseSummarizeReturn<InferGenerationOutputFromReturn<SummarizationResult, TTransformed>>;
@@ -1,26 +1,49 @@
1
1
  import { useGeneration } from "./use-generation.js";
2
+ import { reconstructSummarizeResult } from "@tanstack/ai-client";
3
+ //#region src/use-summarize.ts
4
+ /**
5
+ * React hook for summarizing text using AI models.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { useSummarize } from '@tanstack/ai-react'
10
+ * import { fetchServerSentEvents } from '@tanstack/ai-client'
11
+ *
12
+ * function Summarizer() {
13
+ * const { generate, result, isLoading } = useSummarize({
14
+ * connection: fetchServerSentEvents('/api/summarize'),
15
+ * })
16
+ *
17
+ * return (
18
+ * <div>
19
+ * <button onClick={() => generate({
20
+ * text: 'Long article text...',
21
+ * style: 'bullet-points',
22
+ * maxLength: 200,
23
+ * })}>
24
+ * Summarize
25
+ * </button>
26
+ * {isLoading && <p>Summarizing...</p>}
27
+ * {result && <p>{result.summary}</p>}
28
+ * </div>
29
+ * )
30
+ * }
31
+ * ```
32
+ */
2
33
  function useSummarize(options) {
3
- const devtools = {
4
- ...options.devtools,
5
- framework: "react",
6
- hookName: "useSummarize",
7
- outputKind: "text"
8
- };
9
- const { generate, result, isLoading, error, status, stop, reset } = useGeneration({
10
- ...options,
11
- devtools
12
- });
13
- return {
14
- generate,
15
- result,
16
- isLoading,
17
- error,
18
- status,
19
- stop,
20
- reset
21
- };
34
+ const devtools = {
35
+ ...options.devtools,
36
+ framework: "react",
37
+ hookName: "useSummarize",
38
+ outputKind: "text"
39
+ };
40
+ return useGeneration({
41
+ ...options,
42
+ devtools,
43
+ reconstructResult: reconstructSummarizeResult
44
+ });
22
45
  }
23
- export {
24
- useSummarize
25
- };
26
- //# sourceMappingURL=use-summarize.js.map
46
+ //#endregion
47
+ export { useSummarize };
48
+
49
+ //# sourceMappingURL=use-summarize.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-summarize.js","sources":["../../src/use-summarize.ts"],"sourcesContent":["import { useGeneration } from './use-generation'\nimport type { StreamChunk, SummarizationResult } from '@tanstack/ai'\nimport type {\n AIDevtoolsDisplayOptions,\n ConnectConnectionAdapter,\n GenerationClientState,\n GenerationFetcher,\n InferGenerationOutputFromReturn,\n SummarizeGenerateInput,\n} from '@tanstack/ai-client'\n\n/**\n * Options for the useSummarize hook.\n *\n * @template TOutput - The output type after optional transform (defaults to SummarizationResult)\n */\nexport interface UseSummarizeOptions<TOutput = SummarizationResult> {\n /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */\n connection?: ConnectConnectionAdapter\n /** Direct async function for summarization */\n fetcher?: GenerationFetcher<SummarizeGenerateInput, SummarizationResult>\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 /** Display options for TanStack AI Devtools. */\n devtools?: AIDevtoolsDisplayOptions\n /**\n * Callback when summarization is complete. 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: SummarizationResult) => 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 useSummarize hook.\n *\n * @template TOutput - The output type (after optional transform)\n */\nexport interface UseSummarizeReturn<TOutput = SummarizationResult> {\n /** Trigger summarization */\n generate: (input: SummarizeGenerateInput) => Promise<void>\n /** The summarization result, or null */\n result: TOutput | null\n /** Whether summarization 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 summarization */\n stop: () => void\n /** Clear result, error, and return to idle */\n reset: () => void\n}\n\n/**\n * React hook for summarizing text using AI models.\n *\n * @example\n * ```tsx\n * import { useSummarize } from '@tanstack/ai-react'\n * import { fetchServerSentEvents } from '@tanstack/ai-client'\n *\n * function Summarizer() {\n * const { generate, result, isLoading } = useSummarize({\n * connection: fetchServerSentEvents('/api/summarize'),\n * })\n *\n * return (\n * <div>\n * <button onClick={() => generate({\n * text: 'Long article text...',\n * style: 'bullet-points',\n * maxLength: 200,\n * })}>\n * Summarize\n * </button>\n * {isLoading && <p>Summarizing...</p>}\n * {result && <p>{result.summary}</p>}\n * </div>\n * )\n * }\n * ```\n */\nexport function useSummarize<TTransformed = void>(\n options: Omit<UseSummarizeOptions, 'onResult'> & {\n onResult?: (result: SummarizationResult) => TTransformed\n },\n): UseSummarizeReturn<\n InferGenerationOutputFromReturn<SummarizationResult, TTransformed>\n> {\n const devtools = {\n ...options.devtools,\n framework: 'react',\n hookName: 'useSummarize',\n outputKind: 'text' as const,\n }\n const { generate, result, isLoading, error, status, stop, reset } =\n useGeneration<SummarizeGenerateInput, SummarizationResult, TTransformed>({\n ...options,\n devtools,\n })\n\n return {\n generate: generate as (input: SummarizeGenerateInput) => Promise<void>,\n result,\n isLoading,\n error,\n status,\n stop,\n reset,\n }\n}\n"],"names":[],"mappings":";AA8FO,SAAS,aACd,SAKA;AACA,QAAM,WAAW;AAAA,IACf,GAAG,QAAQ;AAAA,IACX,WAAW;AAAA,IACX,UAAU;AAAA,IACV,YAAY;AAAA,EAAA;AAEd,QAAM,EAAE,UAAU,QAAQ,WAAW,OAAO,QAAQ,MAAM,MAAA,IACxD,cAAyE;AAAA,IACvE,GAAG;AAAA,IACH;AAAA,EAAA,CACD;AAEH,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"use-summarize.js","names":[],"sources":["../../src/use-summarize.ts"],"sourcesContent":["import { useGeneration } from './use-generation'\nimport { reconstructSummarizeResult } from '@tanstack/ai-client'\nimport type { StreamChunk, SummarizationResult } from '@tanstack/ai'\nimport type {\n AIDevtoolsDisplayOptions,\n ConnectConnectionAdapter,\n GenerationClientState,\n GenerationFetcher,\n GenerationPersistenceOptions,\n InferGenerationOutputFromReturn,\n SummarizeGenerateInput,\n} from '@tanstack/ai-client'\n\n/**\n * Options for the useSummarize hook.\n *\n * @template TOutput - The output type after optional transform (defaults to SummarizationResult)\n */\nexport interface UseSummarizeOptions<TOutput = SummarizationResult> {\n /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */\n connection?: ConnectConnectionAdapter\n /** Direct async function for summarization */\n fetcher?: GenerationFetcher<SummarizeGenerateInput, SummarizationResult>\n /**\n * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).\n */\n id?: string\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n /** Display options for TanStack AI Devtools. */\n devtools?: AIDevtoolsDisplayOptions\n /**\n * How this generation persists across reloads.\n * - Omit / `false`: ephemeral, in-memory only.\n * - `true`: server-driven — on mount the client hydrates the last generation\n * for its `threadId` from the server (needs a connection with a\n * `hydrateGeneration` handler) and repaints it; it never auto-starts a run.\n */\n persistence?: boolean\n /**\n * The **scope** this generation belongs to: a stable, app-chosen name for the\n * slot successive runs fill — not a link to a chat conversation.\n *\n * The hook starts empty and produces many runs over its life; each gets its\n * own `runId`, but all belong to one scope. Persistence keys on this, so\n * derive it from your own domain and keep it identical across reloads (e.g.\n * `` `video-${videoId}-start-frame` ``). It is also sent as the AG-UI thread\n * id on the wire, which the protocol requires.\n *\n * **Required whenever `persistence` is set** — an app that cannot name the\n * scope has nothing to restore to. Optional for ephemeral generations, where\n * it falls back to `id` purely to satisfy the wire.\n */\n threadId?: string\n /**\n * Server-driven hydration handler for `persistence: true` when the\n * connection doesn't carry one (e.g. alongside `fetcher`, or a `stream()` /\n * `rpcStream()` adapter built without handlers) — typically a one-line\n * server-function call. The connection's own handler takes precedence.\n */\n hydrateGeneration?: ConnectConnectionAdapter['hydrateGeneration']\n /**\n * Re-attach handler that replays a run still generating to completion on\n * mount, when the connection doesn't carry one. Without it, a restored\n * `running` snapshot surfaces as an (interrupted) error. The connection's\n * own handler takes precedence.\n */\n joinRun?: ConnectConnectionAdapter['joinRun']\n /**\n * Callback when summarization is complete. 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: SummarizationResult) => 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 useSummarize hook.\n *\n * @template TOutput - The output type (after optional transform)\n */\nexport interface UseSummarizeReturn<TOutput = SummarizationResult> {\n /** Trigger summarization */\n generate: (input: SummarizeGenerateInput) => Promise<void>\n /** The summarization result, or null */\n result: TOutput | null\n /** Whether summarization 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 summarization */\n stop: () => void\n /** Clear result, error, and return to idle */\n reset: () => void\n /**\n * The id of the generation job currently running, or `null` when nothing is in\n * flight. Each call to `generate` is one job with its own id. Pass it to your\n * own endpoint to cancel or poll the provider job — `stop()` only aborts the\n * local stream, it does not stop work already running on the provider.\n */\n runId: string | null\n}\n\n/**\n * React hook for summarizing text using AI models.\n *\n * @example\n * ```tsx\n * import { useSummarize } from '@tanstack/ai-react'\n * import { fetchServerSentEvents } from '@tanstack/ai-client'\n *\n * function Summarizer() {\n * const { generate, result, isLoading } = useSummarize({\n * connection: fetchServerSentEvents('/api/summarize'),\n * })\n *\n * return (\n * <div>\n * <button onClick={() => generate({\n * text: 'Long article text...',\n * style: 'bullet-points',\n * maxLength: 200,\n * })}>\n * Summarize\n * </button>\n * {isLoading && <p>Summarizing...</p>}\n * {result && <p>{result.summary}</p>}\n * </div>\n * )\n * }\n * ```\n */\nexport function useSummarize<TTransformed = void>(\n options: Omit<\n UseSummarizeOptions,\n 'onResult' | 'persistence' | 'threadId' | 'id'\n > & {\n onResult?: (result: SummarizationResult) => TTransformed\n } & GenerationPersistenceOptions,\n): UseSummarizeReturn<\n InferGenerationOutputFromReturn<SummarizationResult, TTransformed>\n> {\n const devtools = {\n ...options.devtools,\n framework: 'react',\n hookName: 'useSummarize',\n outputKind: 'text' as const,\n }\n const generation = useGeneration<\n SummarizeGenerateInput,\n SummarizationResult,\n TTransformed\n >({\n ...options,\n devtools,\n reconstructResult: reconstructSummarizeResult,\n })\n\n return generation\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8IA,SAAgB,aACd,SAQA;CACA,MAAM,WAAW;EACf,GAAG,QAAQ;EACX,WAAW;EACX,UAAU;EACV,YAAY;CACd;CAWA,OAVmB,cAIjB;EACA,GAAG;EACH;EACA,mBAAmB;CACrB,CAEO;AACT"}