@octanejs/tanstack-ai 0.0.1

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.
@@ -0,0 +1,245 @@
1
+ import { VideoGenerationClient } from '@tanstack/ai-client'
2
+ import { createVideoDevtoolsBridge } from '@tanstack/ai-client/devtools'
3
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'octane'
4
+ import type { StreamChunk } from '@tanstack/ai'
5
+ import type {
6
+ AIDevtoolsDisplayOptions,
7
+ ConnectConnectionAdapter,
8
+ GenerationClientState,
9
+ GenerationFetcher,
10
+ InferGenerationOutputFromReturn,
11
+ VideoGenerateInput,
12
+ VideoGenerateResult,
13
+ VideoStatusInfo,
14
+ } from '@tanstack/ai-client'
15
+
16
+ /**
17
+ * Options for the useGenerateVideo hook.
18
+ */
19
+ export interface UseGenerateVideoOptions<TOutput = VideoGenerateResult> {
20
+ /** Connect-based adapter for streaming transport (server handles polling) */
21
+ connection?: ConnectConnectionAdapter
22
+ /** Direct async function that returns a completed video result */
23
+ fetcher?: GenerationFetcher<VideoGenerateInput, VideoGenerateResult>
24
+ /** Unique identifier for this generation instance */
25
+ id?: string
26
+ /** Additional body parameters to send with connect-based adapter requests */
27
+ body?: Record<string, any>
28
+ /** Display options for TanStack AI Devtools. */
29
+ devtools?: AIDevtoolsDisplayOptions
30
+ /**
31
+ * Callback when video generation completes. Can optionally return a transformed value.
32
+ *
33
+ * - Return a non-null value to transform and store it as the result
34
+ * - Return `null` to keep the previous result unchanged
35
+ * - Return nothing (`void`) to store the raw result as-is
36
+ */
37
+ onResult?: (result: VideoGenerateResult) => TOutput | null | void
38
+ /** Callback when an error occurs */
39
+ onError?: (error: Error) => void
40
+ /** Callback when progress is reported (0-100) */
41
+ onProgress?: (progress: number, message?: string) => void
42
+ /** Callback when a video job is created */
43
+ onJobCreated?: (jobId: string) => void
44
+ /** Callback on each status update */
45
+ onStatusUpdate?: (status: VideoStatusInfo) => void
46
+ /** Callback for each stream chunk (connect-based adapter mode only) */
47
+ onChunk?: (chunk: StreamChunk) => void
48
+ }
49
+
50
+ /**
51
+ * Return type for the useGenerateVideo hook.
52
+ *
53
+ * @template TOutput - The output type (after optional transform)
54
+ */
55
+ export interface UseGenerateVideoReturn<TOutput = VideoGenerateResult> {
56
+ /** Trigger video generation */
57
+ generate: (input: VideoGenerateInput) => Promise<void>
58
+ /** The final video result (with URL), or null */
59
+ result: TOutput | null
60
+ /** The current job ID, or null */
61
+ jobId: string | null
62
+ /** Current video generation status info, or null */
63
+ videoStatus: VideoStatusInfo | null
64
+ /** Whether generation/polling is in progress */
65
+ isLoading: boolean
66
+ /** Current error, if any */
67
+ error: Error | undefined
68
+ /** Current state of the generation */
69
+ status: GenerationClientState
70
+ /** Abort the current generation/polling */
71
+ stop: () => void
72
+ /** Clear all state and return to idle */
73
+ reset: () => void
74
+ }
75
+
76
+ /**
77
+ * Octane hook for generating videos using AI models.
78
+ *
79
+ * Video generation is asynchronous: a job is created, then polled for status
80
+ * until completion. This hook handles the full lifecycle.
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * import { useGenerateVideo } from '@octanejs/tanstack-ai'
85
+ * import { fetchServerSentEvents } from '@tanstack/ai-client'
86
+ *
87
+ * function VideoGenerator() {
88
+ * const { generate, result, videoStatus, isLoading } = useGenerateVideo({
89
+ * connection: fetchServerSentEvents('/api/generate/video'),
90
+ * onStatusUpdate: (status) => console.log(`Progress: ${status.progress}%`),
91
+ * })
92
+ *
93
+ * return (
94
+ * <div>
95
+ * <button onClick={() => generate({ prompt: 'A flying car over a city' })}>
96
+ * Generate Video
97
+ * </button>
98
+ * {isLoading && videoStatus && (
99
+ * <p>Status: {videoStatus.status} ({videoStatus.progress}%)</p>
100
+ * )}
101
+ * {result && <video src={result.url} controls />}
102
+ * </div>
103
+ * )
104
+ * }
105
+ * ```
106
+ */
107
+ // `TTransformed` infers from the `onResult` return position so the callback
108
+ // parameter is typed as `VideoGenerateResult` and `result` narrows to the
109
+ // transform's return. See issue #848.
110
+ export function useGenerateVideo<TTransformed = void>(
111
+ options: Omit<UseGenerateVideoOptions, 'onResult'> & {
112
+ onResult?: (result: VideoGenerateResult) => TTransformed
113
+ },
114
+ ): UseGenerateVideoReturn<
115
+ InferGenerationOutputFromReturn<VideoGenerateResult, TTransformed>
116
+ > {
117
+ type TOutput = InferGenerationOutputFromReturn<
118
+ VideoGenerateResult,
119
+ TTransformed
120
+ >
121
+ const hookId = useId()
122
+ const clientId = options.id || hookId
123
+
124
+ const [result, setResult] = useState<TOutput | null>(null)
125
+ const [jobId, setJobId] = useState<string | null>(null)
126
+ const [videoStatus, setVideoStatus] = useState<VideoStatusInfo | null>(null)
127
+ const [isLoading, setIsLoading] = useState(false)
128
+ const [error, setError] = useState<Error | undefined>(undefined)
129
+ const [status, setStatus] = useState<GenerationClientState>('idle')
130
+
131
+ const optionsRef = useRef(options)
132
+ optionsRef.current = options
133
+
134
+ const client = useMemo(() => {
135
+ const opts = optionsRef.current
136
+
137
+ // Conditional spread for `body` (strict-optional in target).
138
+ // Optional callbacks are wrapped in non-returning bodies so
139
+ // `?.()`'s implicit `undefined` doesn't widen the function
140
+ // return type (which `exactOptionalPropertyTypes` rejects
141
+ // against the strict-optional target).
142
+ const baseOptions = {
143
+ id: clientId,
144
+ body: opts.body,
145
+ devtoolsBridgeFactory: createVideoDevtoolsBridge,
146
+ devtools: {
147
+ ...opts.devtools,
148
+ // OCTANE DIVERGENCE: identify this binding as Octane (not 'react') in TanStack AI Devtools.
149
+ framework: 'octane',
150
+ hookName: 'useGenerateVideo',
151
+ outputKind: 'video' as const,
152
+ },
153
+ // The transform's raw return type (`TTransformed`) and the stored output
154
+ // (`TOutput`, with null/void/undefined stripped) are identical at runtime;
155
+ // the cast bridges the relationship that the conditional type hides.
156
+ onResult: ((r: VideoGenerateResult) =>
157
+ optionsRef.current.onResult?.(r)) as (
158
+ result: VideoGenerateResult,
159
+ ) => TOutput | null | void,
160
+ onError: (e: Error) => {
161
+ optionsRef.current.onError?.(e)
162
+ },
163
+ onProgress: (p: number, m?: string) => {
164
+ optionsRef.current.onProgress?.(p, m)
165
+ },
166
+ onChunk: (c: StreamChunk) => {
167
+ optionsRef.current.onChunk?.(c)
168
+ },
169
+ onJobCreated: (id: string) => {
170
+ optionsRef.current.onJobCreated?.(id)
171
+ },
172
+ onStatusUpdate: (s: VideoStatusInfo) => {
173
+ optionsRef.current.onStatusUpdate?.(s)
174
+ },
175
+ onResultChange: setResult,
176
+ onLoadingChange: setIsLoading,
177
+ onErrorChange: setError,
178
+ onStatusChange: setStatus,
179
+ onJobIdChange: setJobId,
180
+ onVideoStatusChange: setVideoStatus,
181
+ }
182
+
183
+ if (opts.connection) {
184
+ return new VideoGenerationClient<TOutput>({
185
+ ...baseOptions,
186
+ connection: opts.connection,
187
+ })
188
+ }
189
+
190
+ if (opts.fetcher) {
191
+ return new VideoGenerationClient<TOutput>({
192
+ ...baseOptions,
193
+ fetcher: opts.fetcher,
194
+ })
195
+ }
196
+
197
+ throw new Error(
198
+ 'useGenerateVideo requires either a connection or fetcher option',
199
+ )
200
+ }, [clientId])
201
+
202
+ // Sync body changes without recreating client
203
+ useEffect(() => {
204
+ // Conditional spread: target uses strict-optional `body?: T`.
205
+ client.updateOptions({
206
+ ...(options.body !== undefined && { body: options.body }),
207
+ })
208
+ }, [client, options.body])
209
+
210
+ // Cleanup on unmount
211
+ useEffect(() => {
212
+ client.mountDevtools()
213
+
214
+ return () => {
215
+ client.dispose()
216
+ }
217
+ }, [client])
218
+
219
+ const generate = useCallback(
220
+ async (input: VideoGenerateInput) => {
221
+ await client.generate(input)
222
+ },
223
+ [client],
224
+ )
225
+
226
+ const stop = useCallback(() => {
227
+ client.stop()
228
+ }, [client])
229
+
230
+ const reset = useCallback(() => {
231
+ client.reset()
232
+ }, [client])
233
+
234
+ return {
235
+ generate,
236
+ result,
237
+ jobId,
238
+ videoStatus,
239
+ isLoading,
240
+ error,
241
+ status,
242
+ stop,
243
+ reset,
244
+ }
245
+ }
@@ -0,0 +1,95 @@
1
+ // Declaration companion generated from use-generate-video.tsrx.
2
+ import type { StreamChunk } from '@tanstack/ai';
3
+ import type { AIDevtoolsDisplayOptions, ConnectConnectionAdapter, GenerationClientState, GenerationFetcher, InferGenerationOutputFromReturn, VideoGenerateInput, VideoGenerateResult, VideoStatusInfo } from '@tanstack/ai-client';
4
+ /**
5
+ * Options for the useGenerateVideo hook.
6
+ */
7
+ export interface UseGenerateVideoOptions<TOutput = VideoGenerateResult> {
8
+ /** Connect-based adapter for streaming transport (server handles polling) */
9
+ connection?: ConnectConnectionAdapter;
10
+ /** Direct async function that returns a completed video result */
11
+ fetcher?: GenerationFetcher<VideoGenerateInput, VideoGenerateResult>;
12
+ /** Unique identifier for this generation instance */
13
+ id?: string;
14
+ /** Additional body parameters to send with connect-based adapter requests */
15
+ body?: Record<string, any>;
16
+ /** Display options for TanStack AI Devtools. */
17
+ devtools?: AIDevtoolsDisplayOptions;
18
+ /**
19
+ * Callback when video generation completes. Can optionally return a transformed value.
20
+ *
21
+ * - Return a non-null value to transform and store it as the result
22
+ * - Return `null` to keep the previous result unchanged
23
+ * - Return nothing (`void`) to store the raw result as-is
24
+ */
25
+ onResult?: (result: VideoGenerateResult) => TOutput | null | void;
26
+ /** Callback when an error occurs */
27
+ onError?: (error: Error) => void;
28
+ /** Callback when progress is reported (0-100) */
29
+ onProgress?: (progress: number, message?: string) => void;
30
+ /** Callback when a video job is created */
31
+ onJobCreated?: (jobId: string) => void;
32
+ /** Callback on each status update */
33
+ onStatusUpdate?: (status: VideoStatusInfo) => void;
34
+ /** Callback for each stream chunk (connect-based adapter mode only) */
35
+ onChunk?: (chunk: StreamChunk) => void;
36
+ }
37
+ /**
38
+ * Return type for the useGenerateVideo hook.
39
+ *
40
+ * @template TOutput - The output type (after optional transform)
41
+ */
42
+ export interface UseGenerateVideoReturn<TOutput = VideoGenerateResult> {
43
+ /** Trigger video generation */
44
+ generate: (input: VideoGenerateInput) => Promise<void>;
45
+ /** The final video result (with URL), or null */
46
+ result: TOutput | null;
47
+ /** The current job ID, or null */
48
+ jobId: string | null;
49
+ /** Current video generation status info, or null */
50
+ videoStatus: VideoStatusInfo | null;
51
+ /** Whether generation/polling is in progress */
52
+ isLoading: boolean;
53
+ /** Current error, if any */
54
+ error: Error | undefined;
55
+ /** Current state of the generation */
56
+ status: GenerationClientState;
57
+ /** Abort the current generation/polling */
58
+ stop: () => void;
59
+ /** Clear all state and return to idle */
60
+ reset: () => void;
61
+ }
62
+ /**
63
+ * Octane hook for generating videos using AI models.
64
+ *
65
+ * Video generation is asynchronous: a job is created, then polled for status
66
+ * until completion. This hook handles the full lifecycle.
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * import { useGenerateVideo } from '@octanejs/tanstack-ai'
71
+ * import { fetchServerSentEvents } from '@tanstack/ai-client'
72
+ *
73
+ * function VideoGenerator() {
74
+ * const { generate, result, videoStatus, isLoading } = useGenerateVideo({
75
+ * connection: fetchServerSentEvents('/api/generate/video'),
76
+ * onStatusUpdate: (status) => console.log(`Progress: ${status.progress}%`),
77
+ * })
78
+ *
79
+ * return (
80
+ * <div>
81
+ * <button onClick={() => generate({ prompt: 'A flying car over a city' })}>
82
+ * Generate Video
83
+ * </button>
84
+ * {isLoading && videoStatus && (
85
+ * <p>Status: {videoStatus.status} ({videoStatus.progress}%)</p>
86
+ * )}
87
+ * {result && <video src={result.url} controls />}
88
+ * </div>
89
+ * )
90
+ * }
91
+ * ```
92
+ */
93
+ export declare function useGenerateVideo<TTransformed = void>(options: Omit<UseGenerateVideoOptions, 'onResult'> & {
94
+ onResult?: (result: VideoGenerateResult) => TTransformed;
95
+ }): UseGenerateVideoReturn<InferGenerationOutputFromReturn<VideoGenerateResult, TTransformed>>;
@@ -0,0 +1,216 @@
1
+ import { GenerationClient } from '@tanstack/ai-client'
2
+ import { createGenerationDevtoolsBridge } from '@tanstack/ai-client/devtools'
3
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'octane'
4
+ import type { StreamChunk } from '@tanstack/ai'
5
+ import type {
6
+ AIDevtoolsDisplayOptions,
7
+ ConnectConnectionAdapter,
8
+ GenerationClientOptions,
9
+ GenerationClientState,
10
+ GenerationFetcher,
11
+ InferGenerationOutputFromReturn,
12
+ } from '@tanstack/ai-client'
13
+
14
+ /**
15
+ * Options for the useGeneration hook.
16
+ *
17
+ * Accepts either a `connection` (streaming transport) or a `fetcher` (direct async call).
18
+ *
19
+ * @template TInput - The input type for the generation request
20
+ * @template TResult - The result type returned by the generation
21
+ * @template TOutput - The output type after optional transform (defaults to TResult)
22
+ */
23
+ export interface UseGenerationOptions<TInput, TResult, TOutput = TResult> {
24
+ /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */
25
+ connection?: ConnectConnectionAdapter
26
+ /** Direct async function for one-shot generation (no streaming protocol needed) */
27
+ fetcher?: GenerationFetcher<TInput, TResult>
28
+ /** Unique identifier for this generation instance */
29
+ id?: string
30
+ /** Additional body parameters to send with connect-based adapter requests */
31
+ body?: Record<string, any>
32
+ /** Display options for TanStack AI Devtools. */
33
+ devtools?: AIDevtoolsDisplayOptions
34
+ /**
35
+ * Callback when a result is received. Can optionally return a transformed value.
36
+ *
37
+ * - Return a non-null value to transform and store it as the result
38
+ * - Return `null` to keep the previous result unchanged
39
+ * - Return nothing (`void`) to store the raw result as-is
40
+ */
41
+ onResult?: (result: TResult) => TOutput | null | void
42
+ /** Callback when an error occurs */
43
+ onError?: (error: Error) => void
44
+ /** Callback when progress is reported (0-100) */
45
+ onProgress?: (progress: number, message?: string) => void
46
+ /** Callback for each stream chunk (connect-based adapter mode only) */
47
+ onChunk?: (chunk: StreamChunk) => void
48
+ }
49
+
50
+ /**
51
+ * Return type for the useGeneration hook.
52
+ *
53
+ * @template TOutput - The output type (after optional transform)
54
+ */
55
+ export interface UseGenerationReturn<TOutput> {
56
+ /** Trigger a generation request */
57
+ generate: (input: Record<string, any>) => Promise<void>
58
+ /** The generation result, or null if not yet generated */
59
+ result: TOutput | null
60
+ /** Whether a generation is currently in progress */
61
+ isLoading: boolean
62
+ /** Current error, if any */
63
+ error: Error | undefined
64
+ /** Current state of the generation client */
65
+ status: GenerationClientState
66
+ /** Abort the current generation */
67
+ stop: () => void
68
+ /** Clear result, error, and return to idle */
69
+ reset: () => void
70
+ }
71
+
72
+ /**
73
+ * Generic Octane hook for one-shot generation tasks.
74
+ *
75
+ * This is the base hook used by `useGenerateImage`, `useGenerateSpeech`,
76
+ * `useTranscription`, and `useSummarize`. You can also use it directly
77
+ * for custom generation types.
78
+ *
79
+ * @template TInput - The input type for the generation request
80
+ * @template TResult - The result type returned by the generation
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * const { generate, result, isLoading } = useGeneration<MyInput, MyResult>({
85
+ * connection: fetchServerSentEvents('/api/generate/custom'),
86
+ * })
87
+ *
88
+ * await generate({ prompt: 'Hello' })
89
+ * ```
90
+ */
91
+ // `TTransformed` infers from the `onResult` return position (a covariant
92
+ // inference site that works even for an optional nested property), which types
93
+ // the callback parameter as `TResult` and narrows `result`. Inferring the
94
+ // whole callback as a defaulted type parameter instead collapses to the
95
+ // default, leaving the parameter `any` — a hard error under `strict`. See
96
+ // issue #848.
97
+ export function useGeneration<
98
+ TInput extends Record<string, any>,
99
+ TResult,
100
+ TTransformed = void,
101
+ >(
102
+ options: Omit<UseGenerationOptions<TInput, TResult>, 'onResult'> & {
103
+ onResult?: (result: TResult) => TTransformed
104
+ },
105
+ ): UseGenerationReturn<InferGenerationOutputFromReturn<TResult, TTransformed>> {
106
+ type TOutput = InferGenerationOutputFromReturn<TResult, TTransformed>
107
+ const hookId = useId()
108
+ const clientId = options.id || hookId
109
+
110
+ const [result, setResult] = useState<TOutput | null>(null)
111
+ const [isLoading, setIsLoading] = useState(false)
112
+ const [error, setError] = useState<Error | undefined>(undefined)
113
+ const [status, setStatus] = useState<GenerationClientState>('idle')
114
+
115
+ const optionsRef = useRef(options)
116
+ optionsRef.current = options
117
+
118
+ const client = useMemo(() => {
119
+ const opts = optionsRef.current
120
+
121
+ // Conditional spread for `body` (strict-optional in target;
122
+ // local source is `Record<string, any> | undefined`). Callbacks
123
+ // wrap optional ones in non-returning bodies so `?.()`'s
124
+ // implicit `undefined` doesn't pollute the function return type.
125
+ const clientOptions: GenerationClientOptions<TInput, TResult, TOutput> = {
126
+ id: clientId,
127
+ body: opts.body,
128
+ devtoolsBridgeFactory: createGenerationDevtoolsBridge,
129
+ devtools: {
130
+ hookName: 'useGeneration',
131
+ // OCTANE DIVERGENCE: identify this binding as Octane (not 'react') in TanStack AI Devtools.
132
+ framework: 'octane',
133
+ ...opts.devtools,
134
+ },
135
+ // The transform's raw return type (`TTransformed`) and the stored output
136
+ // (`TOutput`, with null/void/undefined stripped) are identical at runtime;
137
+ // the cast bridges the relationship that the conditional type hides.
138
+ onResult: ((r: TResult) => optionsRef.current.onResult?.(r)) as (
139
+ result: TResult,
140
+ ) => TOutput | null | void,
141
+ onError: (e: Error) => {
142
+ optionsRef.current.onError?.(e)
143
+ },
144
+ onProgress: (p: number, m?: string) => {
145
+ optionsRef.current.onProgress?.(p, m)
146
+ },
147
+ onChunk: (c: StreamChunk) => {
148
+ optionsRef.current.onChunk?.(c)
149
+ },
150
+ onResultChange: setResult,
151
+ onLoadingChange: setIsLoading,
152
+ onErrorChange: setError,
153
+ onStatusChange: setStatus,
154
+ }
155
+
156
+ if (opts.connection) {
157
+ return new GenerationClient<TInput, TResult, TOutput>({
158
+ ...clientOptions,
159
+ connection: opts.connection,
160
+ })
161
+ }
162
+
163
+ if (opts.fetcher) {
164
+ return new GenerationClient<TInput, TResult, TOutput>({
165
+ ...clientOptions,
166
+ fetcher: opts.fetcher,
167
+ })
168
+ }
169
+
170
+ throw new Error(
171
+ 'useGeneration requires either a connection or fetcher option',
172
+ )
173
+ }, [clientId])
174
+
175
+ // Sync body changes without recreating client
176
+ useEffect(() => {
177
+ // Conditional spread: target uses strict-optional `body?: T`.
178
+ client.updateOptions({
179
+ ...(options.body !== undefined && { body: options.body }),
180
+ })
181
+ }, [client, options.body])
182
+
183
+ // Cleanup on unmount
184
+ useEffect(() => {
185
+ client.mountDevtools()
186
+
187
+ return () => {
188
+ client.dispose()
189
+ }
190
+ }, [client])
191
+
192
+ const generate = useCallback(
193
+ async (input: TInput) => {
194
+ await client.generate(input)
195
+ },
196
+ [client],
197
+ )
198
+
199
+ const stop = useCallback(() => {
200
+ client.stop()
201
+ }, [client])
202
+
203
+ const reset = useCallback(() => {
204
+ client.reset()
205
+ }, [client])
206
+
207
+ return {
208
+ generate: generate as (input: Record<string, any>) => Promise<void>,
209
+ result,
210
+ isLoading,
211
+ error,
212
+ status,
213
+ stop,
214
+ reset,
215
+ }
216
+ }
@@ -0,0 +1,81 @@
1
+ // Declaration companion generated from use-generation.tsrx.
2
+ import type { StreamChunk } from '@tanstack/ai';
3
+ import type { AIDevtoolsDisplayOptions, ConnectConnectionAdapter, GenerationClientState, GenerationFetcher, InferGenerationOutputFromReturn } from '@tanstack/ai-client';
4
+ /**
5
+ * Options for the useGeneration hook.
6
+ *
7
+ * Accepts either a `connection` (streaming transport) or a `fetcher` (direct async call).
8
+ *
9
+ * @template TInput - The input type for the generation request
10
+ * @template TResult - The result type returned by the generation
11
+ * @template TOutput - The output type after optional transform (defaults to TResult)
12
+ */
13
+ export interface UseGenerationOptions<TInput, TResult, TOutput = TResult> {
14
+ /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */
15
+ connection?: ConnectConnectionAdapter;
16
+ /** Direct async function for one-shot generation (no streaming protocol needed) */
17
+ fetcher?: GenerationFetcher<TInput, TResult>;
18
+ /** Unique identifier for this generation instance */
19
+ id?: string;
20
+ /** Additional body parameters to send with connect-based adapter requests */
21
+ body?: Record<string, any>;
22
+ /** Display options for TanStack AI Devtools. */
23
+ devtools?: AIDevtoolsDisplayOptions;
24
+ /**
25
+ * Callback when a result is received. Can optionally return a transformed value.
26
+ *
27
+ * - Return a non-null value to transform and store it as the result
28
+ * - Return `null` to keep the previous result unchanged
29
+ * - Return nothing (`void`) to store the raw result as-is
30
+ */
31
+ onResult?: (result: TResult) => TOutput | null | void;
32
+ /** Callback when an error occurs */
33
+ onError?: (error: Error) => void;
34
+ /** Callback when progress is reported (0-100) */
35
+ onProgress?: (progress: number, message?: string) => void;
36
+ /** Callback for each stream chunk (connect-based adapter mode only) */
37
+ onChunk?: (chunk: StreamChunk) => void;
38
+ }
39
+ /**
40
+ * Return type for the useGeneration hook.
41
+ *
42
+ * @template TOutput - The output type (after optional transform)
43
+ */
44
+ export interface UseGenerationReturn<TOutput> {
45
+ /** Trigger a generation request */
46
+ generate: (input: Record<string, any>) => Promise<void>;
47
+ /** The generation result, or null if not yet generated */
48
+ result: TOutput | null;
49
+ /** Whether a generation is currently in progress */
50
+ isLoading: boolean;
51
+ /** Current error, if any */
52
+ error: Error | undefined;
53
+ /** Current state of the generation client */
54
+ status: GenerationClientState;
55
+ /** Abort the current generation */
56
+ stop: () => void;
57
+ /** Clear result, error, and return to idle */
58
+ reset: () => void;
59
+ }
60
+ /**
61
+ * Generic Octane hook for one-shot generation tasks.
62
+ *
63
+ * This is the base hook used by `useGenerateImage`, `useGenerateSpeech`,
64
+ * `useTranscription`, and `useSummarize`. You can also use it directly
65
+ * for custom generation types.
66
+ *
67
+ * @template TInput - The input type for the generation request
68
+ * @template TResult - The result type returned by the generation
69
+ *
70
+ * @example
71
+ * ```tsx
72
+ * const { generate, result, isLoading } = useGeneration<MyInput, MyResult>({
73
+ * connection: fetchServerSentEvents('/api/generate/custom'),
74
+ * })
75
+ *
76
+ * await generate({ prompt: 'Hello' })
77
+ * ```
78
+ */
79
+ export declare function useGeneration<TInput extends Record<string, any>, TResult, TTransformed = void>(options: Omit<UseGenerationOptions<TInput, TResult>, 'onResult'> & {
80
+ onResult?: (result: TResult) => TTransformed;
81
+ }): UseGenerationReturn<InferGenerationOutputFromReturn<TResult, TTransformed>>;
@@ -0,0 +1,60 @@
1
+ import { useMemo, useRef } from 'octane'
2
+ import { createMcpAppBridge } from '@tanstack/ai-client'
3
+ import type {
4
+ CreateMcpAppBridgeOptions,
5
+ McpAppBridge,
6
+ } from '@tanstack/ai-client'
7
+
8
+ export type UseMcpAppBridgeOptions = CreateMcpAppBridgeOptions
9
+
10
+ /**
11
+ * Octane wrapper around `createMcpAppBridge` that returns a **stable** bridge for
12
+ * the given `threadId`/`callEndpoint`, while always invoking the latest
13
+ * `chat.sendMessage` and `onLink` (kept in refs). This avoids both recreating
14
+ * the bridge on every render and the stale-closure / `exhaustive-deps` dance
15
+ * you'd otherwise write by hand:
16
+ *
17
+ * ```tsx
18
+ * const { sendMessage } = useChat({ threadId, connection })
19
+ * const bridge = useMcpAppBridge({
20
+ * threadId,
21
+ * callEndpoint: '/api/mcp-apps-call',
22
+ * chat: { sendMessage: async (content) => void sendMessage(content) },
23
+ * onLink: (url) => window.open(url, '_blank', 'noopener,noreferrer'),
24
+ * })
25
+ * // pass `bridge` to <MCPAppResource bridge={bridge} … />
26
+ * ```
27
+ *
28
+ * The bridge is recreated only when `threadId`, `callEndpoint`, `fetchImpl`, or
29
+ * the *presence* of `onLink` changes — passing a new inline `onLink`/`sendMessage`
30
+ * each render does not churn it.
31
+ */
32
+ export function useMcpAppBridge(options: UseMcpAppBridgeOptions): McpAppBridge {
33
+ const { threadId, callEndpoint, chat, fetchImpl, onLink } = options
34
+
35
+ // Latest-value refs so the bridge identity stays stable but its callbacks are
36
+ // never stale (the bridge calls `.current` at invocation time, not creation).
37
+ const chatRef = useRef(chat)
38
+ chatRef.current = chat
39
+ const onLinkRef = useRef(onLink)
40
+ onLinkRef.current = onLink
41
+
42
+ // Whether a link handler was supplied governs the bridge's link behavior
43
+ // (forward vs. display-only warn), so it's part of the bridge's identity.
44
+ const hasOnLink = onLink != null
45
+
46
+ return useMemo(
47
+ () =>
48
+ createMcpAppBridge({
49
+ threadId,
50
+ callEndpoint,
51
+ fetchImpl,
52
+ chat: {
53
+ sendMessage: (content, body) =>
54
+ chatRef.current.sendMessage(content, body),
55
+ },
56
+ onLink: hasOnLink ? (url) => onLinkRef.current?.(url) : undefined,
57
+ }),
58
+ [threadId, callEndpoint, fetchImpl, hasOnLink],
59
+ )
60
+ }