@tanstack/ai-solid 0.15.1 → 0.16.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.
Files changed (38) hide show
  1. package/README.md +17 -0
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.js +2 -2
  4. package/dist/types.d.ts +29 -3
  5. package/dist/use-chat.js +81 -5
  6. package/dist/use-chat.js.map +1 -1
  7. package/dist/use-generate-audio.d.ts +9 -10
  8. package/dist/use-generate-audio.js +4 -11
  9. package/dist/use-generate-audio.js.map +1 -1
  10. package/dist/use-generate-image.d.ts +9 -10
  11. package/dist/use-generate-image.js +4 -11
  12. package/dist/use-generate-image.js.map +1 -1
  13. package/dist/use-generate-speech.d.ts +9 -10
  14. package/dist/use-generate-speech.js +4 -11
  15. package/dist/use-generate-speech.js.map +1 -1
  16. package/dist/use-generate-video.d.ts +50 -4
  17. package/dist/use-generate-video.js +54 -23
  18. package/dist/use-generate-video.js.map +1 -1
  19. package/dist/use-generation.d.ts +59 -6
  20. package/dist/use-generation.js +43 -19
  21. package/dist/use-generation.js.map +1 -1
  22. package/dist/use-summarize.d.ts +9 -10
  23. package/dist/use-summarize.js +4 -11
  24. package/dist/use-summarize.js.map +1 -1
  25. package/dist/use-transcription.d.ts +9 -10
  26. package/dist/use-transcription.js +4 -11
  27. package/dist/use-transcription.js.map +1 -1
  28. package/package.json +6 -6
  29. package/src/index.ts +10 -0
  30. package/src/types.ts +44 -0
  31. package/src/use-chat.ts +123 -6
  32. package/src/use-generate-audio.ts +33 -23
  33. package/src/use-generate-image.ts +33 -23
  34. package/src/use-generate-speech.ts +32 -23
  35. package/src/use-generate-video.ts +120 -25
  36. package/src/use-generation.ts +129 -25
  37. package/src/use-summarize.ts +34 -23
  38. package/src/use-transcription.ts +33 -24
@@ -2,11 +2,11 @@ import { GenerationClient } from '@tanstack/ai-client'
2
2
  import { createGenerationDevtoolsBridge } from '@tanstack/ai-client/devtools'
3
3
  import {
4
4
  createEffect,
5
- createMemo,
6
5
  createSignal,
7
6
  createUniqueId,
8
7
  onCleanup,
9
8
  onMount,
9
+ untrack,
10
10
  } from 'solid-js'
11
11
  import type { StreamChunk } from '@tanstack/ai'
12
12
  import type {
@@ -15,6 +15,8 @@ import type {
15
15
  GenerationClientOptions,
16
16
  GenerationClientState,
17
17
  GenerationFetcher,
18
+ GenerationPersistenceOptions,
19
+ GenerationRestoredResult,
18
20
  InferGenerationOutputFromReturn,
19
21
  } from '@tanstack/ai-client'
20
22
  import type { Accessor } from 'solid-js'
@@ -33,12 +35,51 @@ export interface UseGenerationOptions<TInput, TResult, TOutput = TResult> {
33
35
  connection?: ConnectConnectionAdapter
34
36
  /** Direct async function for one-shot generation (no streaming protocol needed) */
35
37
  fetcher?: GenerationFetcher<TInput, TResult>
36
- /** Unique identifier for this generation instance */
38
+ /**
39
+ * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
40
+ */
37
41
  id?: string
38
42
  /** Additional body parameters to send with connect-based adapter requests */
39
43
  body?: Record<string, any>
40
44
  /** Display options for TanStack AI Devtools. */
41
45
  devtools?: AIDevtoolsDisplayOptions
46
+ /**
47
+ * How this generation persists across reloads.
48
+ * - Omit / `false`: ephemeral, in-memory only.
49
+ * - `true`: server-driven — on mount the client hydrates the last generation
50
+ * for its `threadId` from the server (needs a connection with a
51
+ * `hydrateGeneration` handler) and repaints it; it never auto-starts a run.
52
+ */
53
+ persistence?: boolean
54
+ /**
55
+ * The **scope** this generation belongs to: a stable, app-chosen name for the
56
+ * slot successive runs fill — not a link to a chat conversation.
57
+ *
58
+ * The hook starts empty and produces many runs over its life; each gets its
59
+ * own `runId`, but all belong to one scope. Persistence keys on this, so
60
+ * derive it from your own domain and keep it identical across reloads (e.g.
61
+ * `` `video-${videoId}-start-frame` ``). It is also sent as the AG-UI thread
62
+ * id on the wire, which the protocol requires.
63
+ *
64
+ * **Required whenever `persistence` is set** — an app that cannot name the
65
+ * scope has nothing to restore to. Optional for ephemeral generations, where
66
+ * it falls back to `id` purely to satisfy the wire.
67
+ */
68
+ threadId?: string
69
+ /**
70
+ * Server-driven hydration handler for `persistence: true` when the
71
+ * connection doesn't carry one (e.g. alongside `fetcher`, or a `stream()` /
72
+ * `rpcStream()` adapter built without handlers) — typically a one-line
73
+ * server-function call. The connection's own handler takes precedence.
74
+ */
75
+ hydrateGeneration?: ConnectConnectionAdapter['hydrateGeneration']
76
+ /**
77
+ * Re-attach handler that replays a run still generating to completion on
78
+ * mount, when the connection doesn't carry one. Without it, a restored
79
+ * `running` snapshot surfaces as an (interrupted) error. The connection's
80
+ * own handler takes precedence.
81
+ */
82
+ joinRun?: ConnectConnectionAdapter['joinRun']
42
83
  /**
43
84
  * Callback when a result is received. Can optionally return a transformed value.
44
85
  *
@@ -53,16 +94,26 @@ export interface UseGenerationOptions<TInput, TResult, TOutput = TResult> {
53
94
  onProgress?: (progress: number, message?: string) => void
54
95
  /** Callback for each stream chunk (connect-based adapter mode only) */
55
96
  onChunk?: (chunk: StreamChunk) => void
97
+ /**
98
+ * @internal Rebuild a typed result from a restored snapshot, injected by each
99
+ * specialized hook (image / speech / audio / transcription / summarize).
100
+ * Forwarded to the client so a server-hydrate restore repaints `result`.
101
+ */
102
+ reconstructResult?: (restored: GenerationRestoredResult) => TResult | null
56
103
  }
57
104
 
58
105
  /**
59
106
  * Return type for the useGeneration hook.
60
107
  *
61
108
  * @template TOutput - The output type (possibly transformed from the raw result)
109
+ * @template TInput - The input type accepted by `generate` (defaults to any object)
62
110
  */
63
- export interface UseGenerationReturn<TOutput> {
111
+ export interface UseGenerationReturn<
112
+ TOutput,
113
+ TInput extends Record<string, any> = Record<string, any>,
114
+ > {
64
115
  /** Trigger a generation request */
65
- generate: (input: Record<string, any>) => Promise<void>
116
+ generate: (input: TInput) => Promise<void>
66
117
  /** The generation result, or null if not yet generated */
67
118
  result: Accessor<TOutput | null>
68
119
  /** Whether a generation is currently in progress */
@@ -75,6 +126,13 @@ export interface UseGenerationReturn<TOutput> {
75
126
  stop: () => void
76
127
  /** Clear result, error, and return to idle */
77
128
  reset: () => void
129
+ /**
130
+ * The id of the generation job currently running, or `null` when nothing is in
131
+ * flight. Each call to `generate` is one job with its own id. Pass it to your
132
+ * own endpoint to cancel or poll the provider job — `stop()` only aborts the
133
+ * local stream, it does not stop work already running on the provider.
134
+ */
135
+ runId: Accessor<string | null>
78
136
  }
79
137
 
80
138
  /**
@@ -108,26 +166,51 @@ export function useGeneration<
108
166
  TResult,
109
167
  TTransformed = void,
110
168
  >(
111
- options: Omit<UseGenerationOptions<TInput, TResult>, 'onResult'> & {
169
+ options: Omit<
170
+ UseGenerationOptions<TInput, TResult>,
171
+ 'onResult' | 'persistence' | 'threadId' | 'id'
172
+ > & {
112
173
  onResult?: (result: TResult) => TTransformed
113
- },
114
- ): UseGenerationReturn<InferGenerationOutputFromReturn<TResult, TTransformed>> {
174
+ } & GenerationPersistenceOptions,
175
+ ): UseGenerationReturn<
176
+ InferGenerationOutputFromReturn<TResult, TTransformed>,
177
+ TInput
178
+ > {
115
179
  type TOutput = InferGenerationOutputFromReturn<TResult, TTransformed>
116
180
  const hookId = createUniqueId()
117
- const clientId = options.id || hookId
118
181
 
119
182
  const [result, setResult] = createSignal<TOutput | null>(null)
120
183
  const [isLoading, setIsLoading] = createSignal(false)
121
184
  const [error, setError] = createSignal<Error | undefined>(undefined)
122
185
  const [status, setStatus] = createSignal<GenerationClientState>('idle')
186
+ const [runId, setRunId] = createSignal<string | null>(null)
187
+ let disposed = false
123
188
 
124
- const client = createMemo(() => {
189
+ // Built once. `untrack` keeps the option reads below from subscribing
190
+ // construction to `options.persistence` / `options.devtools` /
191
+ // `options.body`: a re-run would build a second client
192
+ // and orphan the first (only the live one is disposed on cleanup). Later
193
+ // `options.body` changes are pushed through `updateOptions` instead.
194
+ const client = untrack((): GenerationClient<TInput, TResult, TOutput> => {
125
195
  // Conditional spread on `body`: `GenerationClientOptions.body` is a
126
196
  // strict optional (`body?: Record<string, any>`) and EOPT forbids
127
197
  // assigning the source `T | undefined` directly.
128
198
  const clientOptions: GenerationClientOptions<TInput, TResult, TOutput> = {
129
- id: clientId,
130
199
  body: options.body,
200
+ // Identity: pass `threadId` alone when set (never also pass deprecated `id`).
201
+ ...(options.threadId !== undefined
202
+ ? { threadId: options.threadId }
203
+ : { id: options.id ?? hookId }),
204
+ ...(options.persistence !== undefined && {
205
+ persistence: options.persistence,
206
+ }),
207
+ ...(options.hydrateGeneration !== undefined && {
208
+ hydrateGeneration: options.hydrateGeneration,
209
+ }),
210
+ ...(options.joinRun !== undefined && { joinRun: options.joinRun }),
211
+ ...(options.reconstructResult
212
+ ? { reconstructResult: options.reconstructResult }
213
+ : {}),
131
214
  devtoolsBridgeFactory: createGenerationDevtoolsBridge,
132
215
  devtools: {
133
216
  ...options.devtools,
@@ -140,13 +223,30 @@ export function useGeneration<
140
223
  onResult: ((r: TResult) => options.onResult?.(r)) as (
141
224
  result: TResult,
142
225
  ) => TOutput | null | void,
143
- onError: (e: Error) => options.onError?.(e),
144
- onProgress: (p: number, m?: string) => options.onProgress?.(p, m),
145
- onChunk: (c: StreamChunk) => options.onChunk?.(c),
146
- onResultChange: setResult,
147
- onLoadingChange: setIsLoading,
148
- onErrorChange: setError,
149
- onStatusChange: setStatus,
226
+ onError: (e: Error) => {
227
+ if (!disposed) options.onError?.(e)
228
+ },
229
+ onProgress: (p: number, m?: string) => {
230
+ if (!disposed) options.onProgress?.(p, m)
231
+ },
232
+ onChunk: (c: StreamChunk) => {
233
+ if (!disposed) options.onChunk?.(c)
234
+ },
235
+ onResultChange: (r) => {
236
+ if (!disposed) setResult(() => r)
237
+ },
238
+ onLoadingChange: (l) => {
239
+ if (!disposed) setIsLoading(l)
240
+ },
241
+ onErrorChange: (e) => {
242
+ if (!disposed) setError(e)
243
+ },
244
+ onStatusChange: (s) => {
245
+ if (!disposed) setStatus(s)
246
+ },
247
+ onResumeStateChange: (rs) => {
248
+ if (!disposed) setRunId(rs?.runId ?? null)
249
+ },
150
250
  }
151
251
 
152
252
  if (options.connection) {
@@ -166,44 +266,48 @@ export function useGeneration<
166
266
  throw new Error(
167
267
  'useGeneration requires either a connection or fetcher option',
168
268
  )
169
- }, [clientId])
269
+ })
170
270
 
171
271
  // Sync body changes without recreating client
172
272
  createEffect(() => {
173
273
  const currentBody = options.body
174
- client().updateOptions({
274
+ client.updateOptions({
175
275
  ...(currentBody !== undefined && { body: currentBody }),
176
276
  })
177
277
  })
178
278
 
279
+ // Mount devtools only. Generation runs are never auto-started on mount — a
280
+ // persisted snapshot is hydrated for display, never replayed.
179
281
  onMount(() => {
180
- client().mountDevtools()
282
+ client.mountDevtools()
181
283
  })
182
284
 
183
285
  // Cleanup on unmount: stop any in-flight requests and unregister devtools
184
286
  onCleanup(() => {
185
- client().dispose()
287
+ disposed = true
288
+ client.dispose()
186
289
  })
187
290
 
188
291
  const generate = async (input: TInput) => {
189
- await client().generate(input)
292
+ await client.generate(input)
190
293
  }
191
294
 
192
295
  const stop = () => {
193
- client().stop()
296
+ client.stop()
194
297
  }
195
298
 
196
299
  const reset = () => {
197
- client().reset()
300
+ client.reset()
198
301
  }
199
302
 
200
303
  return {
201
- generate: generate as (input: Record<string, any>) => Promise<void>,
304
+ generate,
202
305
  result,
203
306
  isLoading,
204
307
  error,
205
308
  status,
206
309
  stop,
207
310
  reset,
311
+ runId,
208
312
  }
209
313
  }
@@ -1,10 +1,16 @@
1
1
  import { useGeneration } from './use-generation'
2
+ import { reconstructSummarizeResult } from '@tanstack/ai-client'
3
+ import type {
4
+ UseGenerationOptions,
5
+ UseGenerationReturn,
6
+ } from './use-generation'
2
7
  import type { StreamChunk, SummarizationResult } from '@tanstack/ai'
3
8
  import type {
4
9
  AIDevtoolsDisplayOptions,
5
10
  ConnectConnectionAdapter,
6
11
  GenerationClientState,
7
12
  GenerationFetcher,
13
+ GenerationPersistenceOptions,
8
14
  InferGenerationOutputFromReturn,
9
15
  SummarizeGenerateInput,
10
16
  } from '@tanstack/ai-client'
@@ -15,12 +21,19 @@ import type { Accessor } from 'solid-js'
15
21
  *
16
22
  * @template TOutput - The transformed output type (defaults to SummarizationResult)
17
23
  */
18
- export interface UseSummarizeOptions<TOutput = SummarizationResult> {
24
+ export interface UseSummarizeOptions<
25
+ TOutput = SummarizationResult,
26
+ > extends Pick<
27
+ UseGenerationOptions<SummarizeGenerateInput, SummarizationResult, TOutput>,
28
+ 'persistence' | 'threadId' | 'hydrateGeneration' | 'joinRun'
29
+ > {
19
30
  /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */
20
31
  connection?: ConnectConnectionAdapter
21
32
  /** Direct async function for summarization */
22
33
  fetcher?: GenerationFetcher<SummarizeGenerateInput, SummarizationResult>
23
- /** Unique identifier for this generation instance */
34
+ /**
35
+ * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
36
+ */
24
37
  id?: string
25
38
  /** Additional body parameters to send with connect-based adapter requests */
26
39
  body?: Record<string, any>
@@ -47,7 +60,10 @@ export interface UseSummarizeOptions<TOutput = SummarizationResult> {
47
60
  *
48
61
  * @template TOutput - The transformed output type (defaults to SummarizationResult)
49
62
  */
50
- export interface UseSummarizeReturn<TOutput = SummarizationResult> {
63
+ export interface UseSummarizeReturn<TOutput = SummarizationResult> extends Omit<
64
+ UseGenerationReturn<TOutput>,
65
+ 'generate'
66
+ > {
51
67
  /** Trigger summarization */
52
68
  generate: (input: SummarizeGenerateInput) => Promise<void>
53
69
  /** The summarization result, or null */
@@ -58,10 +74,6 @@ export interface UseSummarizeReturn<TOutput = SummarizationResult> {
58
74
  error: Accessor<Error | undefined>
59
75
  /** Current state of the generation */
60
76
  status: Accessor<GenerationClientState>
61
- /** Abort the current summarization */
62
- stop: () => void
63
- /** Clear result, error, and return to idle */
64
- reset: () => void
65
77
  }
66
78
 
67
79
  /**
@@ -94,9 +106,12 @@ export interface UseSummarizeReturn<TOutput = SummarizationResult> {
94
106
  * ```
95
107
  */
96
108
  export function useSummarize<TTransformed = void>(
97
- options: Omit<UseSummarizeOptions, 'onResult'> & {
109
+ options: Omit<
110
+ UseSummarizeOptions,
111
+ 'onResult' | 'persistence' | 'threadId' | 'id'
112
+ > & {
98
113
  onResult?: (result: SummarizationResult) => TTransformed
99
- },
114
+ } & GenerationPersistenceOptions,
100
115
  ): UseSummarizeReturn<
101
116
  InferGenerationOutputFromReturn<SummarizationResult, TTransformed>
102
117
  > {
@@ -106,19 +121,15 @@ export function useSummarize<TTransformed = void>(
106
121
  hookName: 'useSummarize',
107
122
  outputKind: 'text' as const,
108
123
  }
109
- const { generate, result, isLoading, error, status, stop, reset } =
110
- useGeneration<SummarizeGenerateInput, SummarizationResult, TTransformed>({
111
- ...options,
112
- devtools,
113
- })
124
+ const generation = useGeneration<
125
+ SummarizeGenerateInput,
126
+ SummarizationResult,
127
+ TTransformed
128
+ >({
129
+ ...options,
130
+ devtools,
131
+ reconstructResult: reconstructSummarizeResult,
132
+ })
114
133
 
115
- return {
116
- generate: generate as (input: SummarizeGenerateInput) => Promise<void>,
117
- result,
118
- isLoading,
119
- error,
120
- status,
121
- stop,
122
- reset,
123
- }
134
+ return generation
124
135
  }
@@ -1,10 +1,16 @@
1
1
  import { useGeneration } from './use-generation'
2
+ import { reconstructTranscriptionResult } from '@tanstack/ai-client'
3
+ import type {
4
+ UseGenerationOptions,
5
+ UseGenerationReturn,
6
+ } from './use-generation'
2
7
  import type { StreamChunk, TranscriptionResult } from '@tanstack/ai'
3
8
  import type {
4
9
  AIDevtoolsDisplayOptions,
5
10
  ConnectConnectionAdapter,
6
11
  GenerationClientState,
7
12
  GenerationFetcher,
13
+ GenerationPersistenceOptions,
8
14
  InferGenerationOutputFromReturn,
9
15
  TranscriptionGenerateInput,
10
16
  } from '@tanstack/ai-client'
@@ -15,12 +21,23 @@ import type { Accessor } from 'solid-js'
15
21
  *
16
22
  * @template TOutput - The transformed output type (defaults to TranscriptionResult)
17
23
  */
18
- export interface UseTranscriptionOptions<TOutput = TranscriptionResult> {
24
+ export interface UseTranscriptionOptions<
25
+ TOutput = TranscriptionResult,
26
+ > extends Pick<
27
+ UseGenerationOptions<
28
+ TranscriptionGenerateInput,
29
+ TranscriptionResult,
30
+ TOutput
31
+ >,
32
+ 'persistence' | 'threadId' | 'hydrateGeneration' | 'joinRun'
33
+ > {
19
34
  /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */
20
35
  connection?: ConnectConnectionAdapter
21
36
  /** Direct async function for transcription */
22
37
  fetcher?: GenerationFetcher<TranscriptionGenerateInput, TranscriptionResult>
23
- /** Unique identifier for this generation instance */
38
+ /**
39
+ * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
40
+ */
24
41
  id?: string
25
42
  /** Additional body parameters to send with connect-based adapter requests */
26
43
  body?: Record<string, any>
@@ -47,7 +64,9 @@ export interface UseTranscriptionOptions<TOutput = TranscriptionResult> {
47
64
  *
48
65
  * @template TOutput - The transformed output type (defaults to TranscriptionResult)
49
66
  */
50
- export interface UseTranscriptionReturn<TOutput = TranscriptionResult> {
67
+ export interface UseTranscriptionReturn<
68
+ TOutput = TranscriptionResult,
69
+ > extends Omit<UseGenerationReturn<TOutput>, 'generate'> {
51
70
  /** Trigger transcription */
52
71
  generate: (input: TranscriptionGenerateInput) => Promise<void>
53
72
  /** The transcription result, or null */
@@ -58,10 +77,6 @@ export interface UseTranscriptionReturn<TOutput = TranscriptionResult> {
58
77
  error: Accessor<Error | undefined>
59
78
  /** Current state of the generation */
60
79
  status: Accessor<GenerationClientState>
61
- /** Abort the current transcription */
62
- stop: () => void
63
- /** Clear result, error, and return to idle */
64
- reset: () => void
65
80
  }
66
81
 
67
82
  /**
@@ -100,9 +115,12 @@ export interface UseTranscriptionReturn<TOutput = TranscriptionResult> {
100
115
  * ```
101
116
  */
102
117
  export function useTranscription<TTransformed = void>(
103
- options: Omit<UseTranscriptionOptions, 'onResult'> & {
118
+ options: Omit<
119
+ UseTranscriptionOptions,
120
+ 'onResult' | 'persistence' | 'threadId' | 'id'
121
+ > & {
104
122
  onResult?: (result: TranscriptionResult) => TTransformed
105
- },
123
+ } & GenerationPersistenceOptions,
106
124
  ): UseTranscriptionReturn<
107
125
  InferGenerationOutputFromReturn<TranscriptionResult, TTransformed>
108
126
  > {
@@ -112,20 +130,11 @@ export function useTranscription<TTransformed = void>(
112
130
  hookName: 'useTranscription',
113
131
  outputKind: 'text' as const,
114
132
  }
115
- const { generate, result, isLoading, error, status, stop, reset } =
116
- useGeneration<
117
- TranscriptionGenerateInput,
118
- TranscriptionResult,
119
- TTransformed
120
- >({ ...options, devtools })
133
+ const generation = useGeneration<
134
+ TranscriptionGenerateInput,
135
+ TranscriptionResult,
136
+ TTransformed
137
+ >({ ...options, devtools, reconstructResult: reconstructTranscriptionResult })
121
138
 
122
- return {
123
- generate: generate as (input: TranscriptionGenerateInput) => Promise<void>,
124
- result,
125
- isLoading,
126
- error,
127
- status,
128
- stop,
129
- reset,
130
- }
139
+ return generation
131
140
  }