@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.
- package/README.md +15 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +2 -23
- package/dist/esm/mcp-app-resource.js +45 -45
- package/dist/esm/mcp-app-resource.js.map +1 -1
- package/dist/esm/mcp-apps.js +1 -4
- package/dist/esm/types.d.ts +29 -3
- package/dist/esm/use-audio-recorder.js +41 -46
- package/dist/esm/use-audio-recorder.js.map +1 -1
- package/dist/esm/use-chat.js +330 -275
- package/dist/esm/use-chat.js.map +1 -1
- package/dist/esm/use-generate-audio.d.ts +50 -4
- package/dist/esm/use-generate-audio.js +47 -23
- package/dist/esm/use-generate-audio.js.map +1 -1
- package/dist/esm/use-generate-image.d.ts +50 -4
- package/dist/esm/use-generate-image.js +49 -23
- package/dist/esm/use-generate-image.js.map +1 -1
- package/dist/esm/use-generate-speech.d.ts +50 -4
- package/dist/esm/use-generate-speech.js +43 -23
- package/dist/esm/use-generate-speech.js.map +1 -1
- package/dist/esm/use-generate-video.d.ts +50 -4
- package/dist/esm/use-generate-video.js +141 -103
- package/dist/esm/use-generate-video.js.map +1 -1
- package/dist/esm/use-generation.d.ts +59 -6
- package/dist/esm/use-generation.js +113 -90
- package/dist/esm/use-generation.js.map +1 -1
- package/dist/esm/use-mcp-app-bridge.js +46 -23
- package/dist/esm/use-mcp-app-bridge.js.map +1 -1
- package/dist/esm/use-realtime-chat.js +185 -185
- package/dist/esm/use-realtime-chat.js.map +1 -1
- package/dist/esm/use-summarize.d.ts +50 -4
- package/dist/esm/use-summarize.js +46 -23
- package/dist/esm/use-summarize.js.map +1 -1
- package/dist/esm/use-transcription.d.ts +50 -4
- package/dist/esm/use-transcription.js +51 -20
- package/dist/esm/use-transcription.js.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +10 -0
- package/src/types.ts +44 -0
- package/src/use-chat.ts +221 -35
- package/src/use-generate-audio.ts +64 -17
- package/src/use-generate-image.ts +64 -17
- package/src/use-generate-speech.ts +64 -17
- package/src/use-generate-video.ts +99 -18
- package/src/use-generation.ts +112 -18
- package/src/use-summarize.ts +64 -17
- package/src/use-transcription.ts +60 -18
- package/dist/esm/index.js.map +0 -1
- 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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
|
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"}
|