@tanstack/ai-remix 0.0.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/LICENSE +21 -0
- package/README.md +11 -0
- package/package.json +71 -0
- package/src/chat-ui/chat-input.tsx +125 -0
- package/src/chat-ui/chat-message.tsx +197 -0
- package/src/chat-ui/chat-messages.tsx +67 -0
- package/src/chat-ui/chat.tsx +48 -0
- package/src/chat-ui/create-chat-hook.ts +66 -0
- package/src/chat-ui/create-ui.tsx +516 -0
- package/src/chat-ui/text-part.tsx +29 -0
- package/src/chat-ui/thinking-part.tsx +44 -0
- package/src/chat-ui/tool-approval.tsx +94 -0
- package/src/create-audio-recorder.ts +106 -0
- package/src/create-byok.ts +29 -0
- package/src/create-chat.ts +421 -0
- package/src/create-generate-audio.ts +93 -0
- package/src/create-generate-image.ts +94 -0
- package/src/create-generate-speech.ts +93 -0
- package/src/create-generate-video.ts +347 -0
- package/src/create-generation.ts +333 -0
- package/src/create-mcp-app-bridge.ts +36 -0
- package/src/create-realtime-chat.ts +242 -0
- package/src/create-summarize.ts +96 -0
- package/src/create-transcription.ts +105 -0
- package/src/index.ts +92 -0
- package/src/realtime-types.ts +150 -0
- package/src/types.ts +313 -0
- package/src/ui.ts +41 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createGeneration } from './create-generation.ts'
|
|
2
|
+
import { reconstructImageResult } from '@tanstack/ai-client'
|
|
3
|
+
import type { Handle } from 'remix/ui'
|
|
4
|
+
import type { ImageGenerationResult } from '@tanstack/ai'
|
|
5
|
+
import type {
|
|
6
|
+
GenerationPersistenceOptions,
|
|
7
|
+
ImageGenerateInput,
|
|
8
|
+
} from '@tanstack/ai-client'
|
|
9
|
+
import type {
|
|
10
|
+
CreateGenerationOptions,
|
|
11
|
+
CreateGenerationReturn,
|
|
12
|
+
} from './create-generation.ts'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Options for the createGenerateImage helper.
|
|
16
|
+
*
|
|
17
|
+
* Handle is the first argument of the helper. It is not part of this type.
|
|
18
|
+
*
|
|
19
|
+
* @template TOutput - The output type after optional transform (defaults to ImageGenerationResult)
|
|
20
|
+
*/
|
|
21
|
+
export type CreateGenerateImageOptions<TOutput = ImageGenerationResult> = Omit<
|
|
22
|
+
CreateGenerationOptions<ImageGenerateInput, ImageGenerationResult, TOutput>,
|
|
23
|
+
'onResult' | 'reconstructResult'
|
|
24
|
+
> & {
|
|
25
|
+
onResult?: (result: ImageGenerationResult) => TOutput | null | void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return type for the createGenerateImage helper.
|
|
30
|
+
*
|
|
31
|
+
* @template TOutput - The output type (after optional transform)
|
|
32
|
+
*/
|
|
33
|
+
export type CreateGenerateImageReturn<TOutput = ImageGenerationResult> =
|
|
34
|
+
CreateGenerationReturn<TOutput, ImageGenerateInput>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates an image generation helper for Remix setup.
|
|
38
|
+
*
|
|
39
|
+
* Supports two transport modes:
|
|
40
|
+
* - **ConnectConnectionAdapter** — Streaming transport (SSE, HTTP stream, custom)
|
|
41
|
+
* - **Fetcher** — Direct async function call
|
|
42
|
+
*
|
|
43
|
+
* Call this in a Remix component setup function. Pass the component Handle as
|
|
44
|
+
* the first argument.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* import { createGenerateImage } from '@tanstack/ai-remix'
|
|
49
|
+
* import { fetchServerSentEvents } from '@tanstack/ai-client'
|
|
50
|
+
* import type { Handle } from 'remix/ui'
|
|
51
|
+
*
|
|
52
|
+
* function ImageGenerator(handle: Handle) {
|
|
53
|
+
* const image = createGenerateImage(handle, {
|
|
54
|
+
* connection: fetchServerSentEvents('/api/generate/image'),
|
|
55
|
+
* })
|
|
56
|
+
*
|
|
57
|
+
* return () => (
|
|
58
|
+
* <div>
|
|
59
|
+
* <button onClick={() => image.generate({ prompt: 'A sunset over mountains' })}>
|
|
60
|
+
* Generate
|
|
61
|
+
* </button>
|
|
62
|
+
* {image.isLoading ? <p>Generating...</p> : null}
|
|
63
|
+
* {image.result?.images.map((img) => (
|
|
64
|
+
* <img src={img.url || `data:image/png;base64,${img.b64Json}`} />
|
|
65
|
+
* ))}
|
|
66
|
+
* </div>
|
|
67
|
+
* )
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function createGenerateImage<TTransformed = void>(
|
|
72
|
+
handle: Pick<Handle, 'id' | 'update' | 'signal'>,
|
|
73
|
+
options: Omit<
|
|
74
|
+
CreateGenerateImageOptions,
|
|
75
|
+
'onResult' | 'persistence' | 'threadId'
|
|
76
|
+
> & {
|
|
77
|
+
onResult?: (result: ImageGenerationResult) => TTransformed
|
|
78
|
+
} & GenerationPersistenceOptions,
|
|
79
|
+
) {
|
|
80
|
+
const devtools = {
|
|
81
|
+
...options.devtools,
|
|
82
|
+
hookName: 'createGenerateImage',
|
|
83
|
+
outputKind: 'image' as const,
|
|
84
|
+
}
|
|
85
|
+
return createGeneration<
|
|
86
|
+
ImageGenerateInput,
|
|
87
|
+
ImageGenerationResult,
|
|
88
|
+
TTransformed
|
|
89
|
+
>(handle, {
|
|
90
|
+
...options,
|
|
91
|
+
devtools,
|
|
92
|
+
reconstructResult: reconstructImageResult,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createGeneration } from './create-generation.ts'
|
|
2
|
+
import { reconstructSpeechResult } from '@tanstack/ai-client'
|
|
3
|
+
import type { Handle } from 'remix/ui'
|
|
4
|
+
import type { TTSResult } from '@tanstack/ai'
|
|
5
|
+
import type {
|
|
6
|
+
GenerationPersistenceOptions,
|
|
7
|
+
SpeechGenerateInput,
|
|
8
|
+
} from '@tanstack/ai-client'
|
|
9
|
+
import type {
|
|
10
|
+
CreateGenerationOptions,
|
|
11
|
+
CreateGenerationReturn,
|
|
12
|
+
} from './create-generation.ts'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Options for the createGenerateSpeech helper.
|
|
16
|
+
*
|
|
17
|
+
* Handle is the first argument of the helper. It is not part of this type.
|
|
18
|
+
*
|
|
19
|
+
* @template TOutput - The output type after optional transform (defaults to TTSResult)
|
|
20
|
+
*/
|
|
21
|
+
export type CreateGenerateSpeechOptions<TOutput = TTSResult> = Omit<
|
|
22
|
+
CreateGenerationOptions<SpeechGenerateInput, TTSResult, TOutput>,
|
|
23
|
+
'onResult' | 'reconstructResult'
|
|
24
|
+
> & {
|
|
25
|
+
onResult?: (result: TTSResult) => TOutput | null | void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return type for the createGenerateSpeech helper.
|
|
30
|
+
*
|
|
31
|
+
* @template TOutput - The output type (after optional transform)
|
|
32
|
+
*/
|
|
33
|
+
export type CreateGenerateSpeechReturn<TOutput = TTSResult> =
|
|
34
|
+
CreateGenerationReturn<TOutput, SpeechGenerateInput>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a speech generation (text-to-speech) helper for Remix setup.
|
|
38
|
+
*
|
|
39
|
+
* Call this in a Remix component setup function. Pass the component Handle as
|
|
40
|
+
* the first argument.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* import { createGenerateSpeech } from '@tanstack/ai-remix'
|
|
45
|
+
* import { fetchServerSentEvents } from '@tanstack/ai-client'
|
|
46
|
+
* import type { Handle } from 'remix/ui'
|
|
47
|
+
*
|
|
48
|
+
* function SpeechGenerator(handle: Handle) {
|
|
49
|
+
* const speech = createGenerateSpeech(handle, {
|
|
50
|
+
* connection: fetchServerSentEvents('/api/generate/speech'),
|
|
51
|
+
* })
|
|
52
|
+
*
|
|
53
|
+
* return () => (
|
|
54
|
+
* <div>
|
|
55
|
+
* <button
|
|
56
|
+
* onClick={() => speech.generate({ text: 'Hello world', voice: 'alloy' })}
|
|
57
|
+
* >
|
|
58
|
+
* Generate Speech
|
|
59
|
+
* </button>
|
|
60
|
+
* {speech.result ? (
|
|
61
|
+
* <audio
|
|
62
|
+
* src={`data:audio/${speech.result.format};base64,${speech.result.audio}`}
|
|
63
|
+
* controls
|
|
64
|
+
* />
|
|
65
|
+
* ) : null}
|
|
66
|
+
* </div>
|
|
67
|
+
* )
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function createGenerateSpeech<TTransformed = void>(
|
|
72
|
+
handle: Pick<Handle, 'id' | 'update' | 'signal'>,
|
|
73
|
+
options: Omit<
|
|
74
|
+
CreateGenerateSpeechOptions,
|
|
75
|
+
'onResult' | 'persistence' | 'threadId'
|
|
76
|
+
> & {
|
|
77
|
+
onResult?: (result: TTSResult) => TTransformed
|
|
78
|
+
} & GenerationPersistenceOptions,
|
|
79
|
+
) {
|
|
80
|
+
const devtools = {
|
|
81
|
+
...options.devtools,
|
|
82
|
+
hookName: 'createGenerateSpeech',
|
|
83
|
+
outputKind: 'audio' as const,
|
|
84
|
+
}
|
|
85
|
+
return createGeneration<SpeechGenerateInput, TTSResult, TTransformed>(
|
|
86
|
+
handle,
|
|
87
|
+
{
|
|
88
|
+
...options,
|
|
89
|
+
devtools,
|
|
90
|
+
reconstructResult: reconstructSpeechResult,
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { VideoGenerationClient } from '@tanstack/ai-client'
|
|
2
|
+
import { createVideoDevtoolsBridge } from '@tanstack/ai-client/devtools'
|
|
3
|
+
import type { Handle } from 'remix/ui'
|
|
4
|
+
import type { StreamChunk } from '@tanstack/ai'
|
|
5
|
+
import type {
|
|
6
|
+
AIDevtoolsDisplayOptions,
|
|
7
|
+
ConnectConnectionAdapter,
|
|
8
|
+
GenerationClientState,
|
|
9
|
+
GenerationFetcher,
|
|
10
|
+
GenerationPersistenceOptions,
|
|
11
|
+
InferGenerationOutputFromReturn,
|
|
12
|
+
VideoGenerateInput,
|
|
13
|
+
VideoGenerateResult,
|
|
14
|
+
VideoGenerationClientOptions,
|
|
15
|
+
VideoStatusInfo,
|
|
16
|
+
} from '@tanstack/ai-client'
|
|
17
|
+
import type { ByokClient } from '@tanstack/ai-client/byok'
|
|
18
|
+
import type { ProviderId } from '@tanstack/ai/byok'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options for the createGenerateVideo helper.
|
|
22
|
+
*
|
|
23
|
+
* Handle is the first argument of the helper. It is not part of this type.
|
|
24
|
+
*
|
|
25
|
+
* @template TOutput - The output type after optional transform (defaults to VideoGenerateResult)
|
|
26
|
+
*/
|
|
27
|
+
export interface CreateGenerateVideoOptions<TOutput = VideoGenerateResult> {
|
|
28
|
+
/** Connect-based adapter for streaming transport (server handles polling) */
|
|
29
|
+
connection?: ConnectConnectionAdapter
|
|
30
|
+
/** Direct async function that returns a completed video result */
|
|
31
|
+
fetcher?: GenerationFetcher<VideoGenerateInput, VideoGenerateResult>
|
|
32
|
+
/** Additional body parameters to send with connect-based adapter requests */
|
|
33
|
+
body?: Record<string, any>
|
|
34
|
+
/** Optional BYOK keyring. Keys go in `x-byok-*` headers, never the body. */
|
|
35
|
+
byok?: ByokClient
|
|
36
|
+
/** Optional provider id. If it returns a slug, only that key is sent. If no slug resolves (`byokProvider`, then `body.provider`), generate throws. */
|
|
37
|
+
byokProvider?: () => ProviderId | undefined
|
|
38
|
+
/** Display options for TanStack AI Devtools. */
|
|
39
|
+
devtools?: AIDevtoolsDisplayOptions
|
|
40
|
+
/**
|
|
41
|
+
* How this generation persists across reloads.
|
|
42
|
+
* - Omit / `false`: ephemeral, in-memory only.
|
|
43
|
+
* - `true`: server-driven — on mount the client hydrates the last generation
|
|
44
|
+
* for its `threadId` from the server (needs a connection with a
|
|
45
|
+
* `hydrateGeneration` handler) and repaints it; it never auto-starts a run.
|
|
46
|
+
*/
|
|
47
|
+
persistence?: boolean
|
|
48
|
+
/**
|
|
49
|
+
* The **scope** this generation belongs to: a stable, app-chosen name for the
|
|
50
|
+
* slot successive runs fill — not a link to a chat conversation.
|
|
51
|
+
*
|
|
52
|
+
* The helper starts empty and produces many runs over its life; each gets its
|
|
53
|
+
* own `runId`, but all belong to one scope. Persistence keys on this, so
|
|
54
|
+
* derive it from your own domain and keep it identical across reloads (e.g.
|
|
55
|
+
* `` `video-${videoId}-start-frame` ``). It is also sent as the AG-UI thread
|
|
56
|
+
* id on the wire, which the protocol requires.
|
|
57
|
+
*
|
|
58
|
+
* **Required whenever `persistence` is set** — an app that cannot name the
|
|
59
|
+
* scope has nothing to restore to. Optional for ephemeral generations. If
|
|
60
|
+
* omitted, the helper uses `handle.id`.
|
|
61
|
+
*/
|
|
62
|
+
threadId?: string
|
|
63
|
+
/**
|
|
64
|
+
* Server-driven hydration handler for `persistence: true` when the
|
|
65
|
+
* connection doesn't carry one (e.g. alongside `fetcher`, or a `stream()` /
|
|
66
|
+
* `rpcStream()` adapter built without handlers) — typically a one-line
|
|
67
|
+
* server-function call. The connection's own handler takes precedence.
|
|
68
|
+
*/
|
|
69
|
+
hydrateGeneration?: ConnectConnectionAdapter['hydrateGeneration']
|
|
70
|
+
/**
|
|
71
|
+
* Re-attach handler that replays a run still generating to completion on
|
|
72
|
+
* mount, when the connection doesn't carry one. Without it, a restored
|
|
73
|
+
* `running` snapshot surfaces as an (interrupted) error. The connection's
|
|
74
|
+
* own handler takes precedence.
|
|
75
|
+
*/
|
|
76
|
+
joinRun?: ConnectConnectionAdapter['joinRun']
|
|
77
|
+
/**
|
|
78
|
+
* Callback when video generation completes. Can optionally return a transformed value.
|
|
79
|
+
*
|
|
80
|
+
* - Return a non-null value to transform and store it as the result
|
|
81
|
+
* - Return `null` to keep the previous result unchanged
|
|
82
|
+
* - Return nothing (`void`) to store the raw result as-is
|
|
83
|
+
*/
|
|
84
|
+
onResult?: (result: VideoGenerateResult) => TOutput | null | void
|
|
85
|
+
/** Callback when an error occurs */
|
|
86
|
+
onError?: (error: Error) => void
|
|
87
|
+
/** Callback when progress is reported (0-100) */
|
|
88
|
+
onProgress?: (progress: number, message?: string) => void
|
|
89
|
+
/** Callback when a video job is created */
|
|
90
|
+
onJobCreated?: (jobId: string) => void
|
|
91
|
+
/** Callback on each status update */
|
|
92
|
+
onStatusUpdate?: (status: VideoStatusInfo) => void
|
|
93
|
+
/** Callback for each stream chunk (connect-based adapter mode only) */
|
|
94
|
+
onChunk?: (chunk: StreamChunk) => void
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Return type for the createGenerateVideo helper.
|
|
99
|
+
*
|
|
100
|
+
* Fields are getters over local lets. Remix re-renders read the latest values
|
|
101
|
+
* after `handle.update()`.
|
|
102
|
+
*
|
|
103
|
+
* @template TOutput - The output type (after optional transform)
|
|
104
|
+
*/
|
|
105
|
+
export interface CreateGenerateVideoReturn<TOutput = VideoGenerateResult> {
|
|
106
|
+
/** The final video result (with URL), or null */
|
|
107
|
+
readonly result: TOutput | null
|
|
108
|
+
/** The current job ID, or null */
|
|
109
|
+
readonly jobId: string | null
|
|
110
|
+
/** Current video generation status info, or null */
|
|
111
|
+
readonly videoStatus: VideoStatusInfo | null
|
|
112
|
+
/** Whether generation/polling is in progress */
|
|
113
|
+
readonly isLoading: boolean
|
|
114
|
+
/** Current error, if any */
|
|
115
|
+
readonly error: Error | undefined
|
|
116
|
+
/** Current state of the generation */
|
|
117
|
+
readonly status: GenerationClientState
|
|
118
|
+
/** Trigger video generation */
|
|
119
|
+
generate: (input: VideoGenerateInput) => Promise<void>
|
|
120
|
+
/** Abort the current generation/polling */
|
|
121
|
+
stop: () => void
|
|
122
|
+
/** Clear all state and return to idle */
|
|
123
|
+
reset: () => void
|
|
124
|
+
/**
|
|
125
|
+
* The id of the generation job currently running, or `null` when nothing is in
|
|
126
|
+
* flight. Each call to `generate` is one job with its own id. Pass it to your
|
|
127
|
+
* own endpoint to cancel or poll the provider job — `stop()` only aborts the
|
|
128
|
+
* local stream, it does not stop work already running on the provider.
|
|
129
|
+
*/
|
|
130
|
+
readonly runId: string | null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Creates a video generation helper for Remix setup.
|
|
135
|
+
*
|
|
136
|
+
* Video generation is asynchronous: a job is created, then polled for status
|
|
137
|
+
* until completion. This helper handles the full lifecycle.
|
|
138
|
+
*
|
|
139
|
+
* Call this in a Remix component setup function. Pass the component Handle as
|
|
140
|
+
* the first argument.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```tsx
|
|
144
|
+
* import { createGenerateVideo } from '@tanstack/ai-remix'
|
|
145
|
+
* import { fetchServerSentEvents } from '@tanstack/ai-client'
|
|
146
|
+
* import type { Handle } from 'remix/ui'
|
|
147
|
+
*
|
|
148
|
+
* function VideoGenerator(handle: Handle) {
|
|
149
|
+
* const video = createGenerateVideo(handle, {
|
|
150
|
+
* connection: fetchServerSentEvents('/api/generate/video'),
|
|
151
|
+
* onStatusUpdate: (status) => console.log(`Progress: ${status.progress}%`),
|
|
152
|
+
* })
|
|
153
|
+
*
|
|
154
|
+
* return () => (
|
|
155
|
+
* <div>
|
|
156
|
+
* <button onClick={() => video.generate({ prompt: 'A flying car over a city' })}>
|
|
157
|
+
* Generate Video
|
|
158
|
+
* </button>
|
|
159
|
+
* {video.isLoading && video.videoStatus ? (
|
|
160
|
+
* <p>
|
|
161
|
+
* Status: {video.videoStatus.status} ({video.videoStatus.progress}%)
|
|
162
|
+
* </p>
|
|
163
|
+
* ) : null}
|
|
164
|
+
* {video.result ? <video src={video.result.url} controls /> : null}
|
|
165
|
+
* </div>
|
|
166
|
+
* )
|
|
167
|
+
* }
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
// `TTransformed` infers from the `onResult` return position so the callback
|
|
171
|
+
// parameter is typed as `VideoGenerateResult` and `result` narrows to the
|
|
172
|
+
// transform's return. See issue #848.
|
|
173
|
+
export function createGenerateVideo<TTransformed = void>(
|
|
174
|
+
handle: Pick<Handle, 'id' | 'update' | 'signal'>,
|
|
175
|
+
options: Omit<
|
|
176
|
+
CreateGenerateVideoOptions,
|
|
177
|
+
'onResult' | 'persistence' | 'threadId'
|
|
178
|
+
> & {
|
|
179
|
+
onResult?: (result: VideoGenerateResult) => TTransformed
|
|
180
|
+
} & GenerationPersistenceOptions,
|
|
181
|
+
) {
|
|
182
|
+
type TOutput = InferGenerationOutputFromReturn<
|
|
183
|
+
VideoGenerateResult,
|
|
184
|
+
TTransformed
|
|
185
|
+
>
|
|
186
|
+
|
|
187
|
+
let result: TOutput | null = null
|
|
188
|
+
let jobId: string | null = null
|
|
189
|
+
let videoStatus: VideoStatusInfo | null = null
|
|
190
|
+
let isLoading = false
|
|
191
|
+
let error: Error | undefined = undefined
|
|
192
|
+
let status: GenerationClientState = 'idle'
|
|
193
|
+
let runId: string | null = null
|
|
194
|
+
let disposed = false
|
|
195
|
+
|
|
196
|
+
const notify = () => {
|
|
197
|
+
if (disposed) return
|
|
198
|
+
void handle.update()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const threadId = options.threadId ?? handle.id
|
|
202
|
+
|
|
203
|
+
const baseOptions: Omit<
|
|
204
|
+
VideoGenerationClientOptions<TOutput>,
|
|
205
|
+
'persistence' | 'threadId'
|
|
206
|
+
> = {
|
|
207
|
+
...(options.body !== undefined && { body: options.body }),
|
|
208
|
+
...(options.hydrateGeneration !== undefined && {
|
|
209
|
+
hydrateGeneration: options.hydrateGeneration,
|
|
210
|
+
}),
|
|
211
|
+
...(options.joinRun !== undefined && { joinRun: options.joinRun }),
|
|
212
|
+
...(options.byok !== undefined && { byok: options.byok }),
|
|
213
|
+
byokProvider: () => options.byokProvider?.(),
|
|
214
|
+
devtoolsBridgeFactory: createVideoDevtoolsBridge,
|
|
215
|
+
devtools: {
|
|
216
|
+
hookName: 'createGenerateVideo',
|
|
217
|
+
...options.devtools,
|
|
218
|
+
framework: 'remix',
|
|
219
|
+
outputKind: 'video' as const,
|
|
220
|
+
},
|
|
221
|
+
// The transform's raw return type (`TTransformed`) and the stored output
|
|
222
|
+
// (`TOutput`, with null/void/undefined stripped) are identical at runtime;
|
|
223
|
+
// the cast bridges the relationship that the conditional type hides.
|
|
224
|
+
onResult: ((r: VideoGenerateResult) => options.onResult?.(r)) as (
|
|
225
|
+
result: VideoGenerateResult,
|
|
226
|
+
) => TOutput | null | void,
|
|
227
|
+
onError: (e: Error) => {
|
|
228
|
+
if (!disposed) options.onError?.(e)
|
|
229
|
+
},
|
|
230
|
+
onProgress: (p: number, m?: string) => {
|
|
231
|
+
if (!disposed) options.onProgress?.(p, m)
|
|
232
|
+
},
|
|
233
|
+
onChunk: (c: StreamChunk) => {
|
|
234
|
+
if (!disposed) options.onChunk?.(c)
|
|
235
|
+
},
|
|
236
|
+
onJobCreated: (id: string) => {
|
|
237
|
+
if (!disposed) options.onJobCreated?.(id)
|
|
238
|
+
},
|
|
239
|
+
onStatusUpdate: (s: VideoStatusInfo) => {
|
|
240
|
+
if (!disposed) options.onStatusUpdate?.(s)
|
|
241
|
+
},
|
|
242
|
+
onResultChange: (r: TOutput | null) => {
|
|
243
|
+
if (disposed) return
|
|
244
|
+
result = r
|
|
245
|
+
notify()
|
|
246
|
+
},
|
|
247
|
+
onLoadingChange: (l: boolean) => {
|
|
248
|
+
if (disposed) return
|
|
249
|
+
isLoading = l
|
|
250
|
+
notify()
|
|
251
|
+
},
|
|
252
|
+
onErrorChange: (e: Error | undefined) => {
|
|
253
|
+
if (disposed) return
|
|
254
|
+
error = e
|
|
255
|
+
notify()
|
|
256
|
+
},
|
|
257
|
+
onStatusChange: (s: GenerationClientState) => {
|
|
258
|
+
if (disposed) return
|
|
259
|
+
status = s
|
|
260
|
+
notify()
|
|
261
|
+
},
|
|
262
|
+
onJobIdChange: (id: string | null) => {
|
|
263
|
+
if (disposed) return
|
|
264
|
+
jobId = id
|
|
265
|
+
notify()
|
|
266
|
+
},
|
|
267
|
+
onVideoStatusChange: (s: VideoStatusInfo | null) => {
|
|
268
|
+
if (disposed) return
|
|
269
|
+
videoStatus = s
|
|
270
|
+
notify()
|
|
271
|
+
},
|
|
272
|
+
onResumeStateChange: (rs: { runId: string } | null) => {
|
|
273
|
+
if (disposed) return
|
|
274
|
+
runId = rs?.runId ?? null
|
|
275
|
+
notify()
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const persistenceProps =
|
|
280
|
+
typeof options.threadId === 'string' && options.persistence
|
|
281
|
+
? {
|
|
282
|
+
persistence: options.persistence,
|
|
283
|
+
threadId: options.threadId,
|
|
284
|
+
}
|
|
285
|
+
: {
|
|
286
|
+
threadId,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let client: VideoGenerationClient<TOutput>
|
|
290
|
+
if (options.connection) {
|
|
291
|
+
client = new VideoGenerationClient<TOutput>({
|
|
292
|
+
...baseOptions,
|
|
293
|
+
...persistenceProps,
|
|
294
|
+
connection: options.connection,
|
|
295
|
+
})
|
|
296
|
+
} else if (options.fetcher) {
|
|
297
|
+
client = new VideoGenerationClient<TOutput>({
|
|
298
|
+
...baseOptions,
|
|
299
|
+
...persistenceProps,
|
|
300
|
+
fetcher: options.fetcher,
|
|
301
|
+
})
|
|
302
|
+
} else {
|
|
303
|
+
throw new Error(
|
|
304
|
+
'createGenerateVideo requires either a connection or fetcher option',
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const dispose = () => {
|
|
309
|
+
if (disposed) return
|
|
310
|
+
disposed = true
|
|
311
|
+
client.dispose()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (handle.signal.aborted) {
|
|
315
|
+
dispose()
|
|
316
|
+
} else {
|
|
317
|
+
client.mountDevtools()
|
|
318
|
+
handle.signal.addEventListener('abort', dispose, { once: true })
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
get result() {
|
|
323
|
+
return result
|
|
324
|
+
},
|
|
325
|
+
get jobId() {
|
|
326
|
+
return jobId
|
|
327
|
+
},
|
|
328
|
+
get videoStatus() {
|
|
329
|
+
return videoStatus
|
|
330
|
+
},
|
|
331
|
+
get isLoading() {
|
|
332
|
+
return isLoading
|
|
333
|
+
},
|
|
334
|
+
get error() {
|
|
335
|
+
return error
|
|
336
|
+
},
|
|
337
|
+
get status() {
|
|
338
|
+
return status
|
|
339
|
+
},
|
|
340
|
+
generate: (input: VideoGenerateInput) => client.generate(input),
|
|
341
|
+
stop: () => client.stop(),
|
|
342
|
+
reset: () => client.reset(),
|
|
343
|
+
get runId() {
|
|
344
|
+
return runId
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
}
|