@umituz/react-native-ai-generation-content 1.48.2 → 1.49.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.48.2",
3
+ "version": "1.49.0",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -35,6 +35,9 @@ export interface Creation {
35
35
  // Extended fields for job-based creations
36
36
  readonly status?: CreationStatus;
37
37
  readonly output?: CreationOutput;
38
+ // Background job tracking - FAL queue requestId and model
39
+ readonly requestId?: string;
40
+ readonly model?: string;
38
41
  // Soft delete - if set, the creation is considered deleted
39
42
  readonly deletedAt?: Date;
40
43
  }
@@ -58,6 +61,9 @@ export interface CreationDocument {
58
61
  readonly createdAt: FirebaseTimestamp | Date;
59
62
  readonly completedAt?: FirebaseTimestamp | Date;
60
63
  readonly deletedAt?: FirebaseTimestamp | Date;
64
+ // Background job tracking - FAL queue requestId and model
65
+ readonly requestId?: string;
66
+ readonly model?: string;
61
67
  }
62
68
 
63
69
  interface FirebaseTimestamp {
@@ -113,6 +119,8 @@ export function mapDocumentToCreation(
113
119
  ratedAt: ratedAtDate,
114
120
  status: data.status as CreationStatus | undefined,
115
121
  output: data.output ?? undefined,
122
+ requestId: data.requestId,
123
+ model: data.model,
116
124
  deletedAt: deletedAtDate,
117
125
  };
118
126
  }
@@ -125,6 +125,11 @@ export type {
125
125
  BaseProcessingStartData,
126
126
  BaseProcessingResult,
127
127
  } from "./presentation/hooks/useCreationPersistence";
128
+ export { useProcessingJobsPoller } from "./presentation/hooks/useProcessingJobsPoller";
129
+ export type {
130
+ UseProcessingJobsPollerConfig,
131
+ UseProcessingJobsPollerReturn,
132
+ } from "./presentation/hooks/useProcessingJobsPoller";
128
133
 
129
134
  // =============================================================================
130
135
  // PRESENTATION LAYER - Components
@@ -8,6 +8,7 @@ declare const __DEV__: boolean;
8
8
  const UPDATABLE_FIELDS = [
9
9
  "metadata", "isShared", "uri", "type", "prompt", "status",
10
10
  "output", "rating", "ratedAt", "isFavorite", "deletedAt",
11
+ "requestId", "model",
11
12
  ] as const;
12
13
 
13
14
  /**
@@ -34,6 +35,8 @@ export class CreationsWriter {
34
35
  ...(creation.status !== undefined && { status: creation.status }),
35
36
  ...(creation.output !== undefined && { output: creation.output }),
36
37
  ...(creation.prompt !== undefined && { prompt: creation.prompt }),
38
+ ...(creation.requestId !== undefined && { requestId: creation.requestId }),
39
+ ...(creation.model !== undefined && { model: creation.model }),
37
40
  };
38
41
 
39
42
  try {
@@ -0,0 +1,163 @@
1
+ /**
2
+ * useProcessingJobsPoller Hook
3
+ * Polls FAL queue status for "processing" creations and updates Firestore when complete
4
+ * Enables true background generation - works even after wizard is dismissed
5
+ * Uses provider registry internally - no need to pass FAL functions
6
+ */
7
+
8
+ import { useEffect, useRef, useCallback } from "react";
9
+ import { providerRegistry } from "../../../../infrastructure/services/provider-registry.service";
10
+ import type { Creation } from "../../domain/entities/Creation";
11
+ import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
12
+
13
+ declare const __DEV__: boolean;
14
+
15
+ const POLL_INTERVAL_MS = 5000; // Poll every 5 seconds
16
+
17
+ export interface UseProcessingJobsPollerConfig {
18
+ readonly userId?: string | null;
19
+ readonly creations: Creation[];
20
+ readonly repository: ICreationsRepository;
21
+ readonly enabled?: boolean;
22
+ }
23
+
24
+ export interface UseProcessingJobsPollerReturn {
25
+ readonly processingCount: number;
26
+ }
27
+
28
+ interface FalResult {
29
+ video?: { url?: string };
30
+ output?: string;
31
+ images?: Array<{ url?: string }>;
32
+ image?: { url?: string };
33
+ }
34
+
35
+ function extractResultUrl(result: FalResult): { imageUrl?: string; videoUrl?: string } {
36
+ // Video result
37
+ if (result.video?.url) {
38
+ return { videoUrl: result.video.url };
39
+ }
40
+ // Output URL (some models)
41
+ if (typeof result.output === "string" && result.output.startsWith("http")) {
42
+ if (result.output.includes(".mp4") || result.output.includes("video")) {
43
+ return { videoUrl: result.output };
44
+ }
45
+ return { imageUrl: result.output };
46
+ }
47
+ // Images array
48
+ if (result.images?.[0]?.url) {
49
+ return { imageUrl: result.images[0].url };
50
+ }
51
+ // Single image
52
+ if (result.image?.url) {
53
+ return { imageUrl: result.image.url };
54
+ }
55
+ return {};
56
+ }
57
+
58
+ export function useProcessingJobsPoller(
59
+ config: UseProcessingJobsPollerConfig,
60
+ ): UseProcessingJobsPollerReturn {
61
+ const {
62
+ userId,
63
+ creations,
64
+ repository,
65
+ enabled = true,
66
+ } = config;
67
+
68
+ const pollingRef = useRef<Set<string>>(new Set());
69
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
70
+
71
+ // Find creations that need polling
72
+ const processingJobs = creations.filter(
73
+ (c) => c.status === "processing" && c.requestId && c.model,
74
+ );
75
+
76
+ const pollJob = useCallback(
77
+ async (creation: Creation) => {
78
+ if (!userId || !creation.requestId || !creation.model) return;
79
+ if (pollingRef.current.has(creation.id)) return;
80
+
81
+ const provider = providerRegistry.getActiveProvider();
82
+ if (!provider || !provider.isInitialized()) return;
83
+
84
+ pollingRef.current.add(creation.id);
85
+
86
+ try {
87
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
88
+ console.log("[ProcessingJobsPoller] Checking status:", creation.id);
89
+ }
90
+
91
+ const status = await provider.getJobStatus(creation.model, creation.requestId);
92
+
93
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
94
+ console.log("[ProcessingJobsPoller] Status:", creation.id, status.status);
95
+ }
96
+
97
+ if (status.status === "COMPLETED") {
98
+ // Fetch the result
99
+ const result = await provider.getJobResult<FalResult>(creation.model, creation.requestId);
100
+ const urls = extractResultUrl(result);
101
+
102
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
103
+ console.log("[ProcessingJobsPoller] Completed:", creation.id, urls);
104
+ }
105
+
106
+ // Update Firestore
107
+ const uri = urls.videoUrl || urls.imageUrl || "";
108
+ await repository.update(userId, creation.id, {
109
+ status: "completed",
110
+ uri,
111
+ output: urls,
112
+ });
113
+ } else if (status.status === "FAILED") {
114
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
115
+ console.log("[ProcessingJobsPoller] Failed:", creation.id);
116
+ }
117
+
118
+ await repository.update(userId, creation.id, {
119
+ status: "failed",
120
+ metadata: { error: "Generation failed" },
121
+ });
122
+ }
123
+ // If still IN_PROGRESS or IN_QUEUE, we'll check again next interval
124
+ } catch (error) {
125
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
126
+ console.error("[ProcessingJobsPoller] Poll error:", creation.id, error);
127
+ }
128
+ } finally {
129
+ pollingRef.current.delete(creation.id);
130
+ }
131
+ },
132
+ [userId, repository],
133
+ );
134
+
135
+ useEffect(() => {
136
+ if (!enabled || !userId || processingJobs.length === 0) {
137
+ if (intervalRef.current) {
138
+ clearInterval(intervalRef.current);
139
+ intervalRef.current = null;
140
+ }
141
+ return;
142
+ }
143
+
144
+ // Initial poll
145
+ processingJobs.forEach((job) => void pollJob(job));
146
+
147
+ // Set up interval polling
148
+ intervalRef.current = setInterval(() => {
149
+ processingJobs.forEach((job) => void pollJob(job));
150
+ }, POLL_INTERVAL_MS);
151
+
152
+ return () => {
153
+ if (intervalRef.current) {
154
+ clearInterval(intervalRef.current);
155
+ intervalRef.current = null;
156
+ }
157
+ };
158
+ }, [enabled, userId, processingJobs, pollJob]);
159
+
160
+ return {
161
+ processingCount: processingJobs.length,
162
+ };
163
+ }
@@ -8,6 +8,7 @@ import {
8
8
  } from "@umituz/react-native-design-system";
9
9
  import { useCreations } from "../hooks/useCreations";
10
10
  import { useDeleteCreation } from "../hooks/useDeleteCreation";
11
+ import { useProcessingJobsPoller } from "../hooks/useProcessingJobsPoller";
11
12
  import { useGalleryFilters } from "../hooks/useGalleryFilters";
12
13
  import { useGalleryCallbacks } from "../hooks/useGalleryCallbacks";
13
14
  import { GalleryHeader, CreationCard, GalleryEmptyStates } from "../components";
@@ -38,6 +39,14 @@ export function CreationsGalleryScreen({
38
39
  const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
39
40
  const deleteMutation = useDeleteCreation({ userId, repository });
40
41
 
42
+ // Poll FAL queue for "processing" creations (enables true background generation)
43
+ useProcessingJobsPoller({
44
+ userId,
45
+ creations: creations ?? [],
46
+ repository,
47
+ enabled: !!userId && (creations?.length ?? 0) > 0,
48
+ });
49
+
41
50
  useEffect(() => {
42
51
  if (initialCreationId && creations && creations.length > 0 && !hasAutoSelectedRef.current) {
43
52
  const creation = creations.find((c) => c.id === initialCreationId);
@@ -3,7 +3,10 @@
3
3
  * Handles video-specific generation logic (execution only)
4
4
  */
5
5
 
6
- import { executeVideoFeature } from "../../../../../infrastructure/services/video-feature-executor.service";
6
+ import {
7
+ executeVideoFeature,
8
+ submitVideoFeatureToQueue,
9
+ } from "../../../../../infrastructure/services/video-feature-executor.service";
7
10
  import { buildUnifiedPrompt } from "./shared/unified-prompt-builder";
8
11
  import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
9
12
  import type { WizardStrategy } from "./wizard-strategy.types";
@@ -95,6 +98,28 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
95
98
  return { videoUrl: result.videoUrl };
96
99
  },
97
100
 
101
+ submitToQueue: async (input: unknown) => {
102
+ const videoInput = input as WizardVideoInput;
103
+
104
+ const result = await submitVideoFeatureToQueue(videoFeatureType, {
105
+ sourceImageBase64: videoInput.sourceImageBase64,
106
+ targetImageBase64: videoInput.targetImageBase64,
107
+ prompt: videoInput.prompt,
108
+ options: {
109
+ duration: videoInput.duration,
110
+ aspect_ratio: videoInput.aspectRatio,
111
+ resolution: videoInput.resolution,
112
+ },
113
+ });
114
+
115
+ return {
116
+ success: result.success,
117
+ requestId: result.requestId,
118
+ model: result.model,
119
+ error: result.error,
120
+ };
121
+ },
122
+
98
123
  getCreditCost: () => creditCost,
99
124
  };
100
125
  }
@@ -3,9 +3,18 @@
3
3
  * Shared types for all wizard strategies
4
4
  */
5
5
 
6
+ export interface QueueSubmissionResult {
7
+ readonly success: boolean;
8
+ readonly requestId?: string;
9
+ readonly model?: string;
10
+ readonly error?: string;
11
+ }
12
+
6
13
  export interface WizardStrategy {
7
- /** Execute the generation - returns result with URLs */
14
+ /** Execute the generation - returns result with URLs (blocking) */
8
15
  execute: (input: unknown) => Promise<{ imageUrl?: string; videoUrl?: string }>;
16
+ /** Submit to queue for background processing - returns immediately with requestId */
17
+ submitToQueue?: (input: unknown) => Promise<QueueSubmissionResult>;
9
18
  /** Get credit cost for this generation */
10
19
  getCreditCost: () => number;
11
20
  }
@@ -16,6 +16,9 @@ export interface ProcessingCreationData {
16
16
  readonly scenarioId: string;
17
17
  readonly scenarioTitle: string;
18
18
  readonly prompt: string;
19
+ // Background job tracking - FAL queue
20
+ readonly requestId?: string;
21
+ readonly model?: string;
19
22
  }
20
23
 
21
24
  export interface CompletedCreationData {
@@ -51,6 +54,8 @@ export function createCreationPersistence(config: CreationPersistenceConfig = {}
51
54
  createdAt: new Date(),
52
55
  isShared: false,
53
56
  isFavorite: false,
57
+ requestId: data.requestId,
58
+ model: data.model,
54
59
  metadata: {
55
60
  scenarioId: data.scenarioId,
56
61
  scenarioTitle: data.scenarioTitle,
@@ -58,7 +63,11 @@ export function createCreationPersistence(config: CreationPersistenceConfig = {}
58
63
  });
59
64
 
60
65
  if (typeof __DEV__ !== "undefined" && __DEV__) {
61
- console.log("[CreationPersistence] Saved as processing", { creationId });
66
+ console.log("[CreationPersistence] Saved as processing", {
67
+ creationId,
68
+ requestId: data.requestId,
69
+ model: data.model,
70
+ });
62
71
  }
63
72
 
64
73
  return creationId;
@@ -104,6 +113,26 @@ export function createCreationPersistence(config: CreationPersistenceConfig = {}
104
113
  console.log("[CreationPersistence] Updated to failed", { creationId, error });
105
114
  }
106
115
  },
116
+
117
+ /**
118
+ * Update creation with FAL queue requestId and model after job submission
119
+ * Call this after fal.queue.submit() returns
120
+ */
121
+ updateRequestId: async (
122
+ userId: string,
123
+ creationId: string,
124
+ requestId: string,
125
+ model: string,
126
+ ): Promise<void> => {
127
+ await repository.update(userId, creationId, {
128
+ requestId,
129
+ model,
130
+ });
131
+
132
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
133
+ console.log("[CreationPersistence] Updated requestId", { creationId, requestId, model });
134
+ }
135
+ },
107
136
  };
108
137
  }
109
138
 
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * useWizardGeneration Hook
3
- * Wizard generation with Firestore persistence
4
- * - Saves status="processing" at start
5
- * - Updates to status="completed" on success
6
- * - Updates to status="failed" on error
3
+ * Wizard generation with Firestore persistence and background support
4
+ * - Uses queue submission for videos (enables true background generation)
5
+ * - Uses blocking execution for images
6
+ * - Saves requestId/model to Firestore for background polling
7
7
  */
8
8
 
9
- import { useEffect, useRef, useMemo, useCallback } from "react";
9
+ import { useEffect, useRef, useMemo, useCallback, useState } from "react";
10
10
  import { useGenerationOrchestrator } from "../../../../../presentation/hooks/generation";
11
+ import { providerRegistry } from "../../../../../infrastructure/services/provider-registry.service";
11
12
  import { createWizardStrategy, buildWizardInput } from "../../infrastructure/strategies";
12
13
  import { createCreationPersistence } from "../../infrastructure/utils/creation-persistence.util";
13
14
  import type {
@@ -17,6 +18,8 @@ import type {
17
18
 
18
19
  declare const __DEV__: boolean;
19
20
 
21
+ const POLL_INTERVAL_MS = 3000;
22
+
20
23
  export type {
21
24
  WizardOutputType,
22
25
  WizardScenarioData,
@@ -24,6 +27,32 @@ export type {
24
27
  UseWizardGenerationReturn,
25
28
  } from "./wizard-generation.types";
26
29
 
30
+ interface FalResult {
31
+ video?: { url?: string };
32
+ output?: string;
33
+ images?: Array<{ url?: string }>;
34
+ image?: { url?: string };
35
+ }
36
+
37
+ function extractResultUrl(result: FalResult): { imageUrl?: string; videoUrl?: string } {
38
+ if (result.video?.url) {
39
+ return { videoUrl: result.video.url };
40
+ }
41
+ if (typeof result.output === "string" && result.output.startsWith("http")) {
42
+ if (result.output.includes(".mp4") || result.output.includes("video")) {
43
+ return { videoUrl: result.output };
44
+ }
45
+ return { imageUrl: result.output };
46
+ }
47
+ if (result.images?.[0]?.url) {
48
+ return { imageUrl: result.images[0].url };
49
+ }
50
+ if (result.image?.url) {
51
+ return { imageUrl: result.image.url };
52
+ }
53
+ return {};
54
+ }
55
+
27
56
  export const useWizardGeneration = (
28
57
  props: UseWizardGenerationProps,
29
58
  ): UseWizardGenerationReturn => {
@@ -41,23 +70,119 @@ export const useWizardGeneration = (
41
70
 
42
71
  const hasStarted = useRef(false);
43
72
  const creationIdRef = useRef<string | null>(null);
44
- const inputRef = useRef<{ prompt: string } | null>(null);
73
+ const requestIdRef = useRef<string | null>(null);
74
+ const modelRef = useRef<string | null>(null);
75
+ const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
76
+ const [isGenerating, setIsGenerating] = useState(false);
45
77
 
46
- // Persistence utility - separate from strategy
47
78
  const persistence = useMemo(() => createCreationPersistence(), []);
48
-
49
- // Strategy - only handles execution, creditCost is passed from app
50
79
  const strategy = useMemo(
51
80
  () => createWizardStrategy({ scenario, creditCost }),
52
81
  [scenario, creditCost],
53
82
  );
54
83
 
55
- const handleSuccess = useCallback(
84
+ // Cleanup polling on unmount
85
+ useEffect(() => {
86
+ return () => {
87
+ if (pollingRef.current) {
88
+ clearInterval(pollingRef.current);
89
+ pollingRef.current = null;
90
+ }
91
+ };
92
+ }, []);
93
+
94
+ const handleQueueComplete = useCallback(
95
+ async (urls: { imageUrl?: string; videoUrl?: string }) => {
96
+ const creationId = creationIdRef.current;
97
+
98
+ if (creationId && userId) {
99
+ try {
100
+ await persistence.updateToCompleted(userId, creationId, {
101
+ uri: urls.videoUrl || urls.imageUrl || "",
102
+ imageUrl: urls.imageUrl,
103
+ videoUrl: urls.videoUrl,
104
+ });
105
+ } catch (err) {
106
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
107
+ console.error("[useWizardGeneration] updateToCompleted error:", err);
108
+ }
109
+ }
110
+ }
111
+
112
+ creationIdRef.current = null;
113
+ requestIdRef.current = null;
114
+ modelRef.current = null;
115
+ setIsGenerating(false);
116
+ onSuccess?.(urls);
117
+ },
118
+ [userId, persistence, onSuccess],
119
+ );
120
+
121
+ const handleQueueError = useCallback(
122
+ async (errorMsg: string) => {
123
+ const creationId = creationIdRef.current;
124
+
125
+ if (creationId && userId) {
126
+ try {
127
+ await persistence.updateToFailed(userId, creationId, errorMsg);
128
+ } catch (err) {
129
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
130
+ console.error("[useWizardGeneration] updateToFailed error:", err);
131
+ }
132
+ }
133
+ }
134
+
135
+ creationIdRef.current = null;
136
+ requestIdRef.current = null;
137
+ modelRef.current = null;
138
+ setIsGenerating(false);
139
+ onError?.(errorMsg);
140
+ },
141
+ [userId, persistence, onError],
142
+ );
143
+
144
+ const pollQueueStatus = useCallback(async () => {
145
+ const requestId = requestIdRef.current;
146
+ const model = modelRef.current;
147
+ const provider = providerRegistry.getActiveProvider();
148
+
149
+ if (!requestId || !model || !provider) return;
150
+
151
+ try {
152
+ const status = await provider.getJobStatus(model, requestId);
153
+
154
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
155
+ console.log("[useWizardGeneration] Poll status:", status.status);
156
+ }
157
+
158
+ if (status.status === "COMPLETED") {
159
+ if (pollingRef.current) {
160
+ clearInterval(pollingRef.current);
161
+ pollingRef.current = null;
162
+ }
163
+ const result = await provider.getJobResult<FalResult>(model, requestId);
164
+ const urls = extractResultUrl(result);
165
+ await handleQueueComplete(urls);
166
+ } else if (status.status === "FAILED") {
167
+ if (pollingRef.current) {
168
+ clearInterval(pollingRef.current);
169
+ pollingRef.current = null;
170
+ }
171
+ await handleQueueError("Generation failed");
172
+ }
173
+ } catch (err) {
174
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
175
+ console.error("[useWizardGeneration] Poll error:", err);
176
+ }
177
+ }
178
+ }, [handleQueueComplete, handleQueueError]);
179
+
180
+ // For images: use blocking execution with orchestrator
181
+ const handleBlockingSuccess = useCallback(
56
182
  async (result: unknown) => {
57
183
  const typedResult = result as { imageUrl?: string; videoUrl?: string };
58
184
  const creationId = creationIdRef.current;
59
185
 
60
- // Update to completed in Firestore
61
186
  if (creationId && userId) {
62
187
  try {
63
188
  await persistence.updateToCompleted(userId, creationId, {
@@ -73,17 +198,15 @@ export const useWizardGeneration = (
73
198
  }
74
199
 
75
200
  creationIdRef.current = null;
76
- inputRef.current = null;
77
201
  onSuccess?.(result);
78
202
  },
79
203
  [userId, persistence, onSuccess],
80
204
  );
81
205
 
82
- const handleError = useCallback(
206
+ const handleBlockingError = useCallback(
83
207
  async (err: { message: string }) => {
84
208
  const creationId = creationIdRef.current;
85
209
 
86
- // Update to failed in Firestore
87
210
  if (creationId && userId) {
88
211
  try {
89
212
  await persistence.updateToFailed(userId, creationId, err.message);
@@ -95,22 +218,21 @@ export const useWizardGeneration = (
95
218
  }
96
219
 
97
220
  creationIdRef.current = null;
98
- inputRef.current = null;
99
221
  onError?.(err.message);
100
222
  },
101
223
  [userId, persistence, onError],
102
224
  );
103
225
 
104
- const { generate, isGenerating } = useGenerationOrchestrator(strategy, {
226
+ const { generate, isGenerating: isBlockingGenerating } = useGenerationOrchestrator(strategy, {
105
227
  userId,
106
228
  alertMessages,
107
229
  onCreditsExhausted,
108
- onSuccess: handleSuccess,
109
- onError: handleError,
230
+ onSuccess: handleBlockingSuccess,
231
+ onError: handleBlockingError,
110
232
  });
111
233
 
112
234
  useEffect(() => {
113
- if (isGeneratingStep && !hasStarted.current && !isGenerating) {
235
+ if (isGeneratingStep && !hasStarted.current && !isGenerating && !isBlockingGenerating) {
114
236
  hasStarted.current = true;
115
237
 
116
238
  buildWizardInput(wizardData, scenario)
@@ -121,30 +243,85 @@ export const useWizardGeneration = (
121
243
  return;
122
244
  }
123
245
 
124
- inputRef.current = input as { prompt: string };
125
-
126
- // Save to Firestore with status="processing"
127
246
  const typedInput = input as { prompt?: string };
128
- if (userId && typedInput.prompt) {
129
- try {
130
- const creationId = await persistence.saveAsProcessing(userId, {
131
- scenarioId: scenario.id,
132
- scenarioTitle: scenario.title || scenario.id,
133
- prompt: typedInput.prompt,
134
- });
135
- creationIdRef.current = creationId;
136
-
137
- if (typeof __DEV__ !== "undefined" && __DEV__) {
138
- console.log("[useWizardGeneration] Saved as processing:", creationId);
247
+ const useQueueMode = scenario.outputType === "video" && !!strategy.submitToQueue;
248
+
249
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
250
+ console.log("[useWizardGeneration] Mode:", useQueueMode ? "QUEUE" : "BLOCKING");
251
+ }
252
+
253
+ if (useQueueMode && strategy.submitToQueue) {
254
+ // Queue mode for videos
255
+ setIsGenerating(true);
256
+
257
+ // Submit to queue first to get requestId
258
+ const queueResult = await strategy.submitToQueue(input);
259
+
260
+ if (!queueResult.success || !queueResult.requestId || !queueResult.model) {
261
+ hasStarted.current = false;
262
+ setIsGenerating(false);
263
+ onError?.(queueResult.error || "Queue submission failed");
264
+ return;
265
+ }
266
+
267
+ requestIdRef.current = queueResult.requestId;
268
+ modelRef.current = queueResult.model;
269
+
270
+ // Save to Firestore with requestId and model
271
+ if (userId && typedInput.prompt) {
272
+ try {
273
+ const creationId = await persistence.saveAsProcessing(userId, {
274
+ scenarioId: scenario.id,
275
+ scenarioTitle: scenario.title || scenario.id,
276
+ prompt: typedInput.prompt,
277
+ requestId: queueResult.requestId,
278
+ model: queueResult.model,
279
+ });
280
+ creationIdRef.current = creationId;
281
+
282
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
283
+ console.log("[useWizardGeneration] Saved with requestId:", {
284
+ creationId,
285
+ requestId: queueResult.requestId,
286
+ });
287
+ }
288
+ } catch (err) {
289
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
290
+ console.error("[useWizardGeneration] saveAsProcessing error:", err);
291
+ }
139
292
  }
140
- } catch (err) {
141
- if (typeof __DEV__ !== "undefined" && __DEV__) {
142
- console.error("[useWizardGeneration] saveAsProcessing error:", err);
293
+ }
294
+
295
+ // Start polling for completion
296
+ pollingRef.current = setInterval(() => {
297
+ void pollQueueStatus();
298
+ }, POLL_INTERVAL_MS);
299
+
300
+ // Initial poll
301
+ void pollQueueStatus();
302
+ } else {
303
+ // Blocking mode for images
304
+ if (userId && typedInput.prompt) {
305
+ try {
306
+ const creationId = await persistence.saveAsProcessing(userId, {
307
+ scenarioId: scenario.id,
308
+ scenarioTitle: scenario.title || scenario.id,
309
+ prompt: typedInput.prompt,
310
+ });
311
+ creationIdRef.current = creationId;
312
+
313
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
314
+ console.log("[useWizardGeneration] Saved as processing:", creationId);
315
+ }
316
+ } catch (err) {
317
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
318
+ console.error("[useWizardGeneration] saveAsProcessing error:", err);
319
+ }
143
320
  }
144
321
  }
145
- }
146
322
 
147
- generate(input);
323
+ generate(input);
324
+ }
148
325
  })
149
326
  .catch((error) => {
150
327
  hasStarted.current = false;
@@ -155,7 +332,19 @@ export const useWizardGeneration = (
155
332
  if (!isGeneratingStep && hasStarted.current) {
156
333
  hasStarted.current = false;
157
334
  }
158
- }, [isGeneratingStep, scenario, wizardData, isGenerating, generate, onError, userId, persistence]);
335
+ }, [
336
+ isGeneratingStep,
337
+ scenario,
338
+ wizardData,
339
+ isGenerating,
340
+ isBlockingGenerating,
341
+ generate,
342
+ onError,
343
+ userId,
344
+ persistence,
345
+ strategy,
346
+ pollQueueStatus,
347
+ ]);
159
348
 
160
- return { isGenerating };
349
+ return { isGenerating: isGenerating || isBlockingGenerating };
161
350
  };
package/src/index.ts CHANGED
@@ -47,6 +47,7 @@ export {
47
47
  export {
48
48
  providerRegistry, generationOrchestrator, pollJob, createJobPoller,
49
49
  executeImageFeature, hasImageFeatureSupport, executeVideoFeature, hasVideoFeatureSupport,
50
+ submitVideoFeatureToQueue,
50
51
  } from "./infrastructure/services";
51
52
 
52
53
  export type {
@@ -20,6 +20,7 @@ export type { ImageResultExtractor } from "../utils/url-extractor";
20
20
  export {
21
21
  executeVideoFeature,
22
22
  hasVideoFeatureSupport,
23
+ submitVideoFeatureToQueue,
23
24
  } from "./video-feature-executor.service";
24
25
  export type {
25
26
  ExecuteVideoFeatureOptions,
@@ -160,6 +160,67 @@ export function hasVideoFeatureSupport(): boolean {
160
160
  return provider !== null && provider.isInitialized();
161
161
  }
162
162
 
163
+ /**
164
+ * Submit a video feature to the queue for background processing
165
+ * Returns immediately with requestId and model for later status polling
166
+ */
167
+ export async function submitVideoFeatureToQueue(
168
+ featureType: VideoFeatureType,
169
+ request: VideoFeatureRequest,
170
+ ): Promise<{ success: boolean; requestId?: string; model?: string; error?: string }> {
171
+ if (__DEV__) {
172
+ console.log(`[VideoExecutor:${featureType}] QUEUE SUBMIT`, {
173
+ hasSource: !!request.sourceImageBase64,
174
+ hasTarget: !!request.targetImageBase64,
175
+ promptLength: request.prompt?.length ?? 0,
176
+ });
177
+ }
178
+
179
+ const provider = providerRegistry.getActiveProvider();
180
+
181
+ if (!provider) {
182
+ return { success: false, error: "No AI provider configured" };
183
+ }
184
+
185
+ if (!provider.isInitialized()) {
186
+ return { success: false, error: "AI provider not initialized" };
187
+ }
188
+
189
+ const model = provider.getVideoFeatureModel(featureType);
190
+
191
+ try {
192
+ const inputData: VideoFeatureInputData = {
193
+ sourceImageBase64: cleanBase64(request.sourceImageBase64),
194
+ targetImageBase64: cleanBase64(request.targetImageBase64),
195
+ prompt: request.prompt,
196
+ options: request.options,
197
+ };
198
+
199
+ const input = provider.buildVideoFeatureInput(featureType, inputData);
200
+
201
+ const submission = await provider.submitJob(model, input);
202
+
203
+ if (__DEV__) {
204
+ console.log(`[VideoExecutor:${featureType}] QUEUE SUBMITTED`, {
205
+ requestId: submission.requestId,
206
+ model,
207
+ });
208
+ }
209
+
210
+ return {
211
+ success: true,
212
+ requestId: submission.requestId,
213
+ model,
214
+ };
215
+ } catch (error) {
216
+ const message = extractErrorMessage(error, "Queue submission failed", `Video:${featureType}`);
217
+ if (__DEV__) {
218
+ console.error(`[VideoExecutor:${featureType}] QUEUE EXCEPTION`, { error: message });
219
+ }
220
+ return { success: false, error: message };
221
+ }
222
+ }
223
+
163
224
  export type {
164
225
  ExecuteVideoFeatureOptions,
165
226
  VideoFeatureResult,