@umituz/react-native-ai-generation-content 1.65.7 → 1.65.9

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 (33) hide show
  1. package/package.json +1 -1
  2. package/src/domains/access-control/hooks/useAIFeatureGate.ts +1 -1
  3. package/src/domains/background/infrastructure/services/job-poller.service.ts +81 -3
  4. package/src/domains/background/infrastructure/utils/polling-interval.util.ts +1 -4
  5. package/src/domains/background/presentation/hooks/use-background-generation.ts +13 -3
  6. package/src/domains/creations/infrastructure/repositories/CreationsFetcher.ts +1 -1
  7. package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +0 -1
  8. package/src/domains/creations/infrastructure/repositories/creation-delete.operations.ts +65 -6
  9. package/src/domains/creations/presentation/hooks/creation-validators.ts +0 -1
  10. package/src/domains/generation/infrastructure/flow/use-flow-store.types.ts +0 -1
  11. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +5 -3
  12. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.types.ts +84 -0
  13. package/src/domains/generation/wizard/presentation/hooks/generationExecutor.ts +6 -7
  14. package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +2 -2
  15. package/src/domains/image-to-video/domain/types/config.types.ts +3 -0
  16. package/src/domains/image-to-video/infrastructure/services/image-to-video-executor.ts +6 -3
  17. package/src/domains/image-to-video/presentation/hooks/imageToVideoStrategy.ts +10 -11
  18. package/src/domains/image-to-video/presentation/hooks/useFormState.ts +2 -2
  19. package/src/domains/image-to-video/presentation/hooks/useGeneration.ts +39 -6
  20. package/src/domains/image-to-video/presentation/hooks/useImageToVideoFeature.ts +19 -14
  21. package/src/domains/prompts/infrastructure/services/ImagePromptBuilder.ts +7 -1
  22. package/src/domains/text-to-image/presentation/hooks/useFormState.ts +1 -2
  23. package/src/domains/text-to-image/presentation/hooks/useGeneration.ts +2 -2
  24. package/src/domains/text-to-image/presentation/screens/TextToImageWizardFlow.tsx +0 -1
  25. package/src/domains/text-to-video/infrastructure/services/text-to-video-executor.ts +2 -2
  26. package/src/infrastructure/executors/base-executor.ts +2 -2
  27. package/src/infrastructure/executors/index.ts +1 -1
  28. package/src/infrastructure/http/http-fetch-handler.ts +1 -1
  29. package/src/infrastructure/http/http-request-executor.ts +1 -1
  30. package/src/infrastructure/utils/domain-guards.ts +2 -1
  31. package/src/infrastructure/utils/index.ts +5 -1
  32. package/src/presentation/hooks/generation/orchestrator.ts +29 -2
  33. package/src/presentation/hooks/generation/useImageGeneration.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.65.7",
3
+ "version": "1.65.9",
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",
@@ -52,7 +52,7 @@ export function useAIFeatureGate(options: AIFeatureGateOptions): AIFeatureGateRe
52
52
  hasSubscription: isPremium,
53
53
  creditBalance,
54
54
  requiredCredits: creditCost,
55
- onShowPaywall: openPaywall,
55
+ onShowPaywall: () => openPaywall(),
56
56
  isCreditsLoaded,
57
57
  });
58
58
 
@@ -10,6 +10,56 @@ import { checkStatusForErrors, isJobComplete } from "../utils/status-checker.uti
10
10
  import { validateResult } from "../utils/result-validator.util";
11
11
  import type { PollJobOptions, PollJobResult } from "./job-poller.types";
12
12
 
13
+ declare const __DEV__: boolean;
14
+
15
+ /**
16
+ * Wraps a promise with abort signal support
17
+ * Rejects if signal is aborted before promise resolves
18
+ */
19
+ function withAbortSignal<T>(
20
+ promise: Promise<T>,
21
+ signal: AbortSignal | undefined,
22
+ timeoutMs?: number,
23
+ ): Promise<T> {
24
+ if (!signal && !timeoutMs) {
25
+ return promise;
26
+ }
27
+
28
+ return new Promise<T>((resolve, reject) => {
29
+ // Handle abort signal
30
+ if (signal?.aborted) {
31
+ reject(new Error("Operation aborted"));
32
+ return;
33
+ }
34
+
35
+ const abortHandler = () => {
36
+ reject(new Error("Operation aborted"));
37
+ };
38
+
39
+ signal?.addEventListener("abort", abortHandler, { once: true });
40
+
41
+ // Handle timeout
42
+ let timeoutId: NodeJS.Timeout | undefined;
43
+ if (timeoutMs) {
44
+ timeoutId = setTimeout(() => {
45
+ reject(new Error(`Operation timeout after ${timeoutMs}ms`));
46
+ }, timeoutMs);
47
+ }
48
+
49
+ promise
50
+ .then((result) => {
51
+ signal?.removeEventListener("abort", abortHandler);
52
+ if (timeoutId) clearTimeout(timeoutId);
53
+ resolve(result);
54
+ })
55
+ .catch((error) => {
56
+ signal?.removeEventListener("abort", abortHandler);
57
+ if (timeoutId) clearTimeout(timeoutId);
58
+ reject(error);
59
+ });
60
+ });
61
+ }
62
+
13
63
  /**
14
64
  * Poll job until completion with exponential backoff
15
65
  * Only reports 100% on actual completion
@@ -60,7 +110,12 @@ export async function pollJob<T = unknown>(
60
110
  }
61
111
 
62
112
  try {
63
- const status = await provider.getJobStatus(model, requestId);
113
+ // Wrap provider calls with abort signal support and timeout (30s default)
114
+ const status = await withAbortSignal(
115
+ provider.getJobStatus(model, requestId),
116
+ signal,
117
+ 30000,
118
+ );
64
119
  onStatusChange?.(status);
65
120
 
66
121
  const statusCheck = checkStatusForErrors(status);
@@ -77,7 +132,12 @@ export async function pollJob<T = unknown>(
77
132
  consecutiveTransientErrors = 0;
78
133
 
79
134
  if (isJobComplete(status)) {
80
- const result = await provider.getJobResult<T>(model, requestId);
135
+ // Wrap result retrieval with abort signal support and timeout (60s for larger results)
136
+ const result = await withAbortSignal(
137
+ provider.getJobResult<T>(model, requestId),
138
+ signal,
139
+ 60000,
140
+ );
81
141
 
82
142
  const validation = validateResult(result);
83
143
  if (!validation.isValid) {
@@ -98,11 +158,29 @@ export async function pollJob<T = unknown>(
98
158
  elapsedMs: Date.now() - startTime,
99
159
  };
100
160
  }
101
- } catch {
161
+ } catch (error) {
102
162
  consecutiveTransientErrors++;
103
163
 
164
+ if (__DEV__) {
165
+ console.warn("[JobPoller] Transient error during polling", {
166
+ attempt: attempt + 1,
167
+ requestId,
168
+ model,
169
+ consecutiveErrors: consecutiveTransientErrors,
170
+ error: error instanceof Error ? error.message : String(error),
171
+ code: (error as { code?: string })?.code,
172
+ });
173
+ }
174
+
104
175
  // Check if we've hit max consecutive transient errors
105
176
  if (maxConsecutiveErrors && consecutiveTransientErrors >= maxConsecutiveErrors) {
177
+ if (__DEV__) {
178
+ console.error("[JobPoller] Max consecutive errors reached", {
179
+ maxConsecutiveErrors,
180
+ requestId,
181
+ model,
182
+ });
183
+ }
106
184
  return {
107
185
  success: false,
108
186
  error: new Error(`Too many consecutive errors (${consecutiveTransientErrors})`),
@@ -2,10 +2,7 @@
2
2
  * Polling Interval Calculator
3
3
  */
4
4
 
5
- import {
6
- DEFAULT_POLLING_CONFIG,
7
- type PollingConfig,
8
- } from "../../../../domain/entities/polling.types";
5
+ import type { PollingConfig } from "../../../../domain/entities/polling.types";
9
6
 
10
7
  export interface IntervalOptions {
11
8
  attempt: number;
@@ -1,4 +1,4 @@
1
- import { useCallback, useRef, useState } from "react";
1
+ import { useCallback, useRef, useState, useMemo } from "react";
2
2
  import { usePendingJobs } from "./use-pending-jobs";
3
3
  import { executeDirectGeneration, executeQueuedJob } from "../../infrastructure/executors/backgroundJobExecutor";
4
4
  import { DEFAULT_QUEUE_CONFIG } from "../../domain/entities/job.types";
@@ -85,13 +85,23 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
85
85
  [removeJob],
86
86
  );
87
87
 
88
+ // Calculate active jobs from TanStack Query state (not ref) for reactivity
89
+ // Active jobs are those currently processing or queued
90
+ const activeJobs = useMemo(
91
+ () => jobs.filter((job) => job.status === "processing" || job.status === "queued"),
92
+ [jobs]
93
+ );
94
+
95
+ const activeJobCount = activeJobs.length;
96
+ const hasActiveJobs = activeJobCount > 0;
97
+
88
98
  return {
89
99
  startJob,
90
100
  executeDirectly,
91
101
  cancelJob,
92
102
  pendingJobs: jobs,
93
- activeJobCount: activeJobsRef.current.size,
94
- hasActiveJobs: activeJobsRef.current.size > 0,
103
+ activeJobCount,
104
+ hasActiveJobs,
95
105
  isProcessing,
96
106
  progress,
97
107
  };
@@ -144,7 +144,7 @@ export class CreationsFetcher {
144
144
  if (__DEV__) {
145
145
  console.error("[CreationsFetcher] Realtime subscription error:", {
146
146
  error: error.message,
147
- code: (error as any).code,
147
+ code: (error as { code?: string }).code,
148
148
  userId,
149
149
  });
150
150
  }
@@ -1,4 +1,3 @@
1
- declare const __DEV__: boolean;
2
1
 
3
2
  import { BaseRepository, FirestorePathResolver } from "@umituz/react-native-firebase";
4
3
  import type {
@@ -5,6 +5,8 @@
5
5
  import { updateDoc, deleteDoc } from "firebase/firestore";
6
6
  import { type FirestorePathResolver } from "@umituz/react-native-firebase";
7
7
 
8
+ declare const __DEV__: boolean;
9
+
8
10
  /**
9
11
  * Soft deletes a creation
10
12
  */
@@ -14,12 +16,31 @@ export async function deleteCreation(
14
16
  creationId: string
15
17
  ): Promise<boolean> {
16
18
  const docRef = pathResolver.getDocRef(userId, creationId);
17
- if (!docRef) return false;
19
+ if (!docRef) {
20
+ if (__DEV__) {
21
+ console.error("[CreationDelete] Cannot delete: Invalid document reference", {
22
+ userId,
23
+ creationId,
24
+ });
25
+ }
26
+ return false;
27
+ }
18
28
 
19
29
  try {
20
30
  await updateDoc(docRef, { deletedAt: new Date() });
31
+ if (__DEV__) {
32
+ console.log("[CreationDelete] Soft deleted successfully", { userId, creationId });
33
+ }
21
34
  return true;
22
- } catch {
35
+ } catch (error) {
36
+ if (__DEV__) {
37
+ console.error("[CreationDelete] Soft delete failed", {
38
+ userId,
39
+ creationId,
40
+ error: error instanceof Error ? error.message : String(error),
41
+ code: (error as { code?: string })?.code,
42
+ });
43
+ }
23
44
  return false;
24
45
  }
25
46
  }
@@ -33,12 +54,31 @@ export async function hardDeleteCreation(
33
54
  creationId: string
34
55
  ): Promise<boolean> {
35
56
  const docRef = pathResolver.getDocRef(userId, creationId);
36
- if (!docRef) return false;
57
+ if (!docRef) {
58
+ if (__DEV__) {
59
+ console.error("[CreationDelete] Cannot hard delete: Invalid document reference", {
60
+ userId,
61
+ creationId,
62
+ });
63
+ }
64
+ return false;
65
+ }
37
66
 
38
67
  try {
39
68
  await deleteDoc(docRef);
69
+ if (__DEV__) {
70
+ console.log("[CreationDelete] Hard deleted successfully", { userId, creationId });
71
+ }
40
72
  return true;
41
- } catch {
73
+ } catch (error) {
74
+ if (__DEV__) {
75
+ console.error("[CreationDelete] Hard delete failed", {
76
+ userId,
77
+ creationId,
78
+ error: error instanceof Error ? error.message : String(error),
79
+ code: (error as { code?: string })?.code,
80
+ });
81
+ }
42
82
  return false;
43
83
  }
44
84
  }
@@ -52,12 +92,31 @@ export async function restoreCreation(
52
92
  creationId: string
53
93
  ): Promise<boolean> {
54
94
  const docRef = pathResolver.getDocRef(userId, creationId);
55
- if (!docRef) return false;
95
+ if (!docRef) {
96
+ if (__DEV__) {
97
+ console.error("[CreationDelete] Cannot restore: Invalid document reference", {
98
+ userId,
99
+ creationId,
100
+ });
101
+ }
102
+ return false;
103
+ }
56
104
 
57
105
  try {
58
106
  await updateDoc(docRef, { deletedAt: null });
107
+ if (__DEV__) {
108
+ console.log("[CreationDelete] Restored successfully", { userId, creationId });
109
+ }
59
110
  return true;
60
- } catch {
111
+ } catch (error) {
112
+ if (__DEV__) {
113
+ console.error("[CreationDelete] Restore failed", {
114
+ userId,
115
+ creationId,
116
+ error: error instanceof Error ? error.message : String(error),
117
+ code: (error as { code?: string })?.code,
118
+ });
119
+ }
61
120
  return false;
62
121
  }
63
122
  }
@@ -14,7 +14,6 @@ import {
14
14
  } from "../../domain/constants";
15
15
  import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
16
16
 
17
- declare const __DEV__: boolean;
18
17
 
19
18
  export interface ValidationResult {
20
19
  isValid: boolean;
@@ -4,7 +4,6 @@
4
4
 
5
5
  import type {
6
6
  FlowState,
7
- FlowActions,
8
7
  StepDefinition,
9
8
  } from "../../../../domain/entities/flow-config.types";
10
9
 
@@ -15,7 +15,7 @@ import { extractPrompt, extractDuration, extractAspectRatio, extractResolution }
15
15
  import { extractPhotosAsBase64 } from "./shared/photo-extraction.utils";
16
16
  import { getVideoFeatureType } from "./video-generation.utils";
17
17
  import type { WizardVideoInput, CreateVideoStrategyOptions } from "./video-generation.types";
18
- import { validatePhotoCount } from "./video-generation.types";
18
+ import { validatePhotoCount, validateWizardVideoInput } from "./video-generation.types";
19
19
 
20
20
  declare const __DEV__: boolean;
21
21
 
@@ -76,7 +76,8 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
76
76
 
77
77
  return {
78
78
  execute: async (input: unknown) => {
79
- const videoInput = input as WizardVideoInput;
79
+ // Runtime validation with descriptive errors
80
+ const videoInput = validateWizardVideoInput(input);
80
81
 
81
82
  const result = await executeVideoFeature(videoFeatureType, {
82
83
  sourceImageBase64: videoInput.sourceImageBase64,
@@ -97,7 +98,8 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
97
98
  },
98
99
 
99
100
  submitToQueue: async (input: unknown) => {
100
- const videoInput = input as WizardVideoInput;
101
+ // Runtime validation with descriptive errors
102
+ const videoInput = validateWizardVideoInput(input);
101
103
 
102
104
  const result = await submitVideoFeatureToQueue(videoFeatureType, {
103
105
  sourceImageBase64: videoInput.sourceImageBase64,
@@ -59,3 +59,87 @@ export function validatePhotoCount(
59
59
 
60
60
  return { isValid: true };
61
61
  }
62
+
63
+ /**
64
+ * Type guard for WizardVideoInput
65
+ * Validates runtime input and provides type safety
66
+ */
67
+ export function isWizardVideoInput(input: unknown): input is WizardVideoInput {
68
+ if (!input || typeof input !== "object") {
69
+ return false;
70
+ }
71
+
72
+ const obj = input as Record<string, unknown>;
73
+
74
+ // prompt is required
75
+ if (typeof obj.prompt !== "string" || obj.prompt.length === 0) {
76
+ return false;
77
+ }
78
+
79
+ // Optional fields validation
80
+ if (obj.sourceImageBase64 !== undefined && typeof obj.sourceImageBase64 !== "string") {
81
+ return false;
82
+ }
83
+
84
+ if (obj.targetImageBase64 !== undefined && typeof obj.targetImageBase64 !== "string") {
85
+ return false;
86
+ }
87
+
88
+ if (obj.duration !== undefined && typeof obj.duration !== "number") {
89
+ return false;
90
+ }
91
+
92
+ if (obj.aspectRatio !== undefined && typeof obj.aspectRatio !== "string") {
93
+ return false;
94
+ }
95
+
96
+ if (obj.resolution !== undefined && typeof obj.resolution !== "string") {
97
+ return false;
98
+ }
99
+
100
+ return true;
101
+ }
102
+
103
+ /**
104
+ * Validates and casts input to WizardVideoInput
105
+ * Throws descriptive error if validation fails
106
+ */
107
+ export function validateWizardVideoInput(input: unknown): WizardVideoInput {
108
+ if (!isWizardVideoInput(input)) {
109
+ const errors: string[] = [];
110
+
111
+ if (!input || typeof input !== "object") {
112
+ throw new Error("Invalid input: expected object");
113
+ }
114
+
115
+ const obj = input as Record<string, unknown>;
116
+
117
+ if (typeof obj.prompt !== "string" || obj.prompt.length === 0) {
118
+ errors.push("prompt (string, required)");
119
+ }
120
+
121
+ if (obj.sourceImageBase64 !== undefined && typeof obj.sourceImageBase64 !== "string") {
122
+ errors.push("sourceImageBase64 (string, optional)");
123
+ }
124
+
125
+ if (obj.targetImageBase64 !== undefined && typeof obj.targetImageBase64 !== "string") {
126
+ errors.push("targetImageBase64 (string, optional)");
127
+ }
128
+
129
+ if (obj.duration !== undefined && typeof obj.duration !== "number") {
130
+ errors.push("duration (number, optional)");
131
+ }
132
+
133
+ if (obj.aspectRatio !== undefined && typeof obj.aspectRatio !== "string") {
134
+ errors.push("aspectRatio (string, optional)");
135
+ }
136
+
137
+ if (obj.resolution !== undefined && typeof obj.resolution !== "string") {
138
+ errors.push("resolution (string, optional)");
139
+ }
140
+
141
+ throw new Error(`Invalid WizardVideoInput: ${errors.join(", ")}`);
142
+ }
143
+
144
+ return input;
145
+ }
@@ -1,19 +1,18 @@
1
1
  import { buildWizardInput } from "../../infrastructure/strategies";
2
2
  import type { WizardScenarioData } from "./wizard-generation.types";
3
- import type { Scenario } from "../../../../scenarios/domain/scenario.types";
4
3
  import type { GenerationAction } from "./generationStateMachine";
5
4
 
6
5
  declare const __DEV__: boolean;
7
6
 
8
7
  interface ExecuteGenerationParams {
9
- wizardData: WizardScenarioData;
10
- scenario: Scenario;
8
+ wizardData: Record<string, unknown>;
9
+ scenario: WizardScenarioData;
11
10
  isVideoMode: boolean;
12
11
  isMountedRef: React.MutableRefObject<boolean>;
13
12
  dispatch: React.Dispatch<GenerationAction>;
14
13
  onError?: (error: string) => void;
15
- videoGenerationFn: (input: any, prompt: string) => Promise<void>;
16
- photoGenerationFn: (input: any, prompt: string) => Promise<void>;
14
+ videoGenerationFn: (input: unknown, prompt: string) => Promise<void>;
15
+ photoGenerationFn: (input: unknown, prompt: string) => Promise<void>;
17
16
  }
18
17
 
19
18
  export const executeWizardGeneration = async (params: ExecuteGenerationParams): Promise<void> => {
@@ -52,10 +51,10 @@ export const executeWizardGeneration = async (params: ExecuteGenerationParams):
52
51
  if (isMountedRef.current) {
53
52
  dispatch({ type: "COMPLETE" });
54
53
  }
55
- } catch (error: any) {
54
+ } catch (error: unknown) {
56
55
  if (!isMountedRef.current) return;
57
56
 
58
- const errorMsg = error?.message || "error.generation.unknown";
57
+ const errorMsg = (error instanceof Error ? error.message : String(error)) || "error.generation.unknown";
59
58
  if (__DEV__) {
60
59
  console.error("[WizardGeneration] Error:", errorMsg, error);
61
60
  }
@@ -46,7 +46,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
46
46
  if (creationId && userId && (urls.videoUrl || urls.imageUrl)) {
47
47
  try {
48
48
  await persistence.updateToCompleted(userId, creationId, {
49
- uri: urls.videoUrl || urls.imageUrl,
49
+ uri: (urls.videoUrl || urls.imageUrl) ?? "",
50
50
  imageUrl: urls.imageUrl,
51
51
  videoUrl: urls.videoUrl,
52
52
  });
@@ -123,7 +123,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
123
123
  requestIdRef.current = queueResult.requestId;
124
124
  modelRef.current = queueResult.model;
125
125
 
126
- if (creationId && userId) {
126
+ if (creationId && userId && queueResult.requestId && queueResult.model) {
127
127
  try {
128
128
  await persistence.updateRequestId(userId, creationId, queueResult.requestId, queueResult.model);
129
129
  } catch {}
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { ImageToVideoResult } from "./image-to-video.types";
7
7
  import type { ImageToVideoFormState } from "./form.types";
8
+ import type { ImageToVideoGenerationStartData, ImageToVideoCreationData } from "./image-to-video-callbacks.types";
8
9
 
9
10
  export interface ImageToVideoCallbacks {
10
11
  onGenerate: (formState: ImageToVideoFormState) => Promise<void>;
@@ -13,6 +14,8 @@ export interface ImageToVideoCallbacks {
13
14
  onShowPaywall?: (cost: number) => void;
14
15
  onSuccess?: (result: ImageToVideoResult) => void;
15
16
  onError?: (error: string) => void;
17
+ onGenerationStart?: (data: ImageToVideoGenerationStartData) => Promise<void>;
18
+ onCreationSave?: (data: ImageToVideoCreationData) => Promise<void>;
16
19
  }
17
20
 
18
21
  export interface ImageToVideoFormConfig {
@@ -14,6 +14,9 @@ import type { ImageToVideoRequest, ImageToVideoResult } from "../../domain/types
14
14
  import { env } from "../../../../infrastructure/config/env.config";
15
15
  import type { ExecuteImageToVideoOptions } from "./image-to-video-executor.types";
16
16
 
17
+ // Export types
18
+ export type { ExecuteImageToVideoOptions };
19
+
17
20
  const STATUS_PROGRESS: Record<string, number> = {
18
21
  queued: 10,
19
22
  in_queue: 15,
@@ -41,17 +44,17 @@ class ImageToVideoExecutor extends BaseExecutor<
41
44
  input: Record<string, unknown>,
42
45
  onProgress?: (progress: number) => void,
43
46
  ): Promise<unknown> {
44
- this.logInfo("Starting provider.subscribe()...");
47
+ this.log("info", "Starting provider.subscribe()...");
45
48
 
46
49
  const result = await provider.subscribe(model, input, {
47
50
  onQueueUpdate: (status: { status: string; queuePosition?: number }) => {
48
- this.logInfo(`Queue: ${status.status}, pos: ${status.queuePosition}`);
51
+ this.log("info", `Queue: ${status.status}, pos: ${status.queuePosition}`);
49
52
  onProgress?.(STATUS_PROGRESS[status.status.toLowerCase()] ?? 30);
50
53
  },
51
54
  timeoutMs: env.generationVideoTimeoutMs,
52
55
  });
53
56
 
54
- this.logInfo(`Complete, keys: ${result ? Object.keys(result as object) : "null"}`);
57
+ this.log("info", `Complete, keys: ${result ? Object.keys(result as object) : "null"}`);
55
58
  checkFalApiError(result);
56
59
  return result;
57
60
  }
@@ -1,7 +1,7 @@
1
1
  import { executeImageToVideo } from "../../infrastructure/services";
2
2
  import type { GenerationStrategy } from "../../../../presentation/hooks/generation";
3
3
  import type {
4
- ImageToVideoConfig,
4
+ ImageToVideoFeatureConfig,
5
5
  ImageToVideoCallbacks,
6
6
  ImageToVideoResult,
7
7
  ImageToVideoOptions,
@@ -17,8 +17,8 @@ interface VideoGenerationInput {
17
17
  }
18
18
 
19
19
  interface CreateStrategyParams {
20
- config: ImageToVideoConfig;
21
- callbacks: ImageToVideoCallbacks;
20
+ config: ImageToVideoFeatureConfig;
21
+ callbacks?: ImageToVideoCallbacks;
22
22
  buildInput: ImageToVideoInputBuilder;
23
23
  extractResult?: ImageToVideoResultExtractor;
24
24
  userId: string;
@@ -30,22 +30,21 @@ interface CreateStrategyParams {
30
30
  export const createImageToVideoStrategy = (
31
31
  params: CreateStrategyParams,
32
32
  ): GenerationStrategy<VideoGenerationInput, ImageToVideoResult> => {
33
- const { config, callbacks, buildInput, extractResult, userId, currentPrompt, creationIdRef, updateState } = params;
33
+ const { config, callbacks, buildInput, extractResult, userId, creationIdRef, updateState } = params;
34
34
 
35
35
  return {
36
36
  execute: async (input) => {
37
37
  creationIdRef.current = input.creationId;
38
38
 
39
- callbacks.onGenerationStart?.({
39
+ callbacks?.onGenerationStart?.({
40
40
  creationId: input.creationId,
41
41
  type: "image-to-video",
42
- imageUrl: input.imageUrl,
43
- prompt: input.prompt,
42
+ imageUri: input.imageUrl,
44
43
  metadata: input.options as Record<string, unknown> | undefined,
45
44
  }).catch(() => {});
46
45
 
47
46
  const result = await executeImageToVideo(
48
- { imageUrl: input.imageUrl, prompt: input.prompt, userId, options: input.options },
47
+ { imageUri: input.imageUrl, userId, motionPrompt: input.prompt, options: input.options },
49
48
  { model: config.model, buildInput, extractResult },
50
49
  );
51
50
 
@@ -61,15 +60,15 @@ export const createImageToVideoStrategy = (
61
60
  thumbnailUrl: result.thumbnailUrl,
62
61
  };
63
62
  },
64
- getCreditCost: () => config.creditCost,
63
+ getCreditCost: () => config.creditCost ?? 0,
65
64
  save: async (result) => {
66
65
  if (result.success && result.videoUrl && creationIdRef.current) {
67
- await callbacks.onCreationSave?.({
66
+ await callbacks?.onCreationSave?.({
68
67
  creationId: creationIdRef.current,
69
68
  type: "image-to-video",
70
69
  videoUrl: result.videoUrl,
71
70
  thumbnailUrl: result.thumbnailUrl,
72
- prompt: currentPrompt,
71
+ imageUri: "",
73
72
  });
74
73
  }
75
74
  },
@@ -24,8 +24,8 @@ export interface UseFormStateReturn {
24
24
  function createInitialState(defaults: ImageToVideoFormDefaults): ImageToVideoFormState {
25
25
  return {
26
26
  selectedImages: [],
27
- animationStyle: defaults.animationStyle,
28
- duration: defaults.duration,
27
+ animationStyle: defaults.animationStyle ?? "none",
28
+ duration: defaults.duration ?? 3,
29
29
  motionPrompt: "",
30
30
  };
31
31
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Generation Hook for Image-to-Video
3
- * Manages generation state and execution
3
+ * Manages generation state and execution with abort support
4
4
  */
5
5
 
6
- import { useState, useCallback, useMemo } from "react";
6
+ import { useState, useCallback, useMemo, useRef, useEffect } from "react";
7
7
  import type {
8
8
  ImageToVideoFormState,
9
9
  ImageToVideoGenerationState,
@@ -36,20 +36,46 @@ export function useGeneration(options: UseGenerationOptions): UseGenerationRetur
36
36
  INITIAL_GENERATION_STATE
37
37
  );
38
38
 
39
+ const abortControllerRef = useRef<AbortController | null>(null);
40
+ const isMountedRef = useRef(true);
41
+
42
+ // Cleanup on unmount
43
+ useEffect(() => {
44
+ isMountedRef.current = true;
45
+ return () => {
46
+ isMountedRef.current = false;
47
+ abortControllerRef.current?.abort();
48
+ };
49
+ }, []);
50
+
51
+ // Stabilize callbacks to prevent unnecessary re-renders
52
+ const onErrorRef = useRef(callbacks.onError);
53
+ const onGenerateRef = useRef(callbacks.onGenerate);
54
+
55
+ useEffect(() => {
56
+ onErrorRef.current = callbacks.onError;
57
+ onGenerateRef.current = callbacks.onGenerate;
58
+ }, [callbacks.onError, callbacks.onGenerate]);
59
+
39
60
  const setProgress = useCallback((progress: number) => {
61
+ if (!isMountedRef.current) return;
40
62
  setGenerationState((prev) => ({ ...prev, progress }));
41
63
  }, []);
42
64
 
43
65
  const setError = useCallback((error: string | null) => {
66
+ if (!isMountedRef.current) return;
44
67
  setGenerationState((prev) => ({ ...prev, error, isGenerating: false }));
45
68
  }, []);
46
69
 
47
70
  const handleGenerate = useCallback(async () => {
48
71
  if (formState.selectedImages.length === 0) {
49
- callbacks.onError?.("No images selected");
72
+ onErrorRef.current?.("No images selected");
50
73
  return;
51
74
  }
52
75
 
76
+ // Create new AbortController for this generation
77
+ abortControllerRef.current = new AbortController();
78
+
53
79
  setGenerationState({
54
80
  isGenerating: true,
55
81
  progress: 0,
@@ -57,18 +83,25 @@ export function useGeneration(options: UseGenerationOptions): UseGenerationRetur
57
83
  });
58
84
 
59
85
  try {
60
- await callbacks.onGenerate(formState);
86
+ await onGenerateRef.current(formState);
87
+
88
+ if (!isMountedRef.current || abortControllerRef.current.signal.aborted) return;
89
+
61
90
  setGenerationState((prev) => ({ ...prev, isGenerating: false, progress: 100 }));
62
91
  } catch (error) {
92
+ if (!isMountedRef.current || abortControllerRef.current.signal.aborted) return;
93
+
63
94
  const errorMessage = error instanceof Error ? error.message : String(error);
64
95
  setGenerationState({
65
96
  isGenerating: false,
66
97
  progress: 0,
67
98
  error: errorMessage,
68
99
  });
69
- callbacks.onError?.(errorMessage);
100
+ onErrorRef.current?.(errorMessage);
101
+ } finally {
102
+ abortControllerRef.current = null;
70
103
  }
71
- }, [formState, callbacks]);
104
+ }, [formState]);
72
105
 
73
106
  const isReady = useMemo(
74
107
  () => formState.selectedImages.length > 0 && !generationState.isGenerating,
@@ -1,11 +1,12 @@
1
1
  import { useState, useCallback, useMemo, useRef } from "react";
2
2
  import { useGenerationOrchestrator } from "../../../../presentation/hooks/generation";
3
3
  import { createImageToVideoStrategy } from "./imageToVideoStrategy";
4
- import type {
5
- UseImageToVideoFeatureProps,
6
- UseImageToVideoFeatureReturn,
4
+ import type { ImageToVideoCallbacks } from "../../domain/types";
5
+ import {
7
6
  INITIAL_STATE,
8
7
  DEFAULT_ALERT_MESSAGES,
8
+ type UseImageToVideoFeatureProps,
9
+ type UseImageToVideoFeatureReturn,
9
10
  } from "./image-to-video-feature.types";
10
11
 
11
12
  export type {
@@ -26,7 +27,7 @@ export function useImageToVideoFeature(props: UseImageToVideoFeatureProps): UseI
26
27
  () =>
27
28
  createImageToVideoStrategy({
28
29
  config,
29
- callbacks,
30
+ callbacks: callbacks as ImageToVideoCallbacks | undefined,
30
31
  buildInput: config.buildInput,
31
32
  extractResult: config.extractResult,
32
33
  userId,
@@ -40,14 +41,17 @@ export function useImageToVideoFeature(props: UseImageToVideoFeatureProps): UseI
40
41
  const orchestrator = useGenerationOrchestrator(strategy, {
41
42
  userId,
42
43
  alertMessages: DEFAULT_ALERT_MESSAGES,
43
- onCreditsExhausted: () => callbacks.onShowPaywall?.(config.creditCost ?? 0),
44
- onSuccess: (result) => {
45
- config.onProcessingComplete?.();
46
- callbacks.onGenerate?.(result);
44
+ onCreditsExhausted: () => callbacks?.onShowPaywall?.(config.creditCost ?? 0),
45
+ onSuccess: (result: unknown) => {
46
+ const typedResult = result as { success: boolean; videoUrl?: string; thumbnailUrl?: string };
47
+ if (typedResult.success && typedResult.videoUrl) {
48
+ config.onProcessingComplete?.(typedResult);
49
+ callbacks?.onGenerate?.(typedResult);
50
+ }
47
51
  },
48
52
  onError: (err) => {
49
- config.onProcessingError?.(err.message);
50
- callbacks.onError?.(err.message);
53
+ config.onError?.(err.message);
54
+ callbacks?.onError?.(err.message);
51
55
  },
52
56
  });
53
57
 
@@ -60,8 +64,8 @@ export function useImageToVideoFeature(props: UseImageToVideoFeatureProps): UseI
60
64
  }, []);
61
65
 
62
66
  const generate = useCallback(
63
- async (params?: any) => {
64
- const imageUri = params?.imageUri || state.imageUri;
67
+ async (params?: unknown): Promise<{ success: boolean; videoUrl?: string; thumbnailUrl?: string; error?: string }> => {
68
+ const imageUri = (params && typeof params === "object" && "imageUri" in params ? (params as { imageUri?: string }).imageUri : undefined) || state.imageUri;
65
69
  if (!imageUri) {
66
70
  const error = "Image is required";
67
71
  setState((prev) => ({ ...prev, error }));
@@ -74,11 +78,12 @@ export function useImageToVideoFeature(props: UseImageToVideoFeatureProps): UseI
74
78
  const result = await orchestrator.generate({
75
79
  imageUrl: imageUri,
76
80
  prompt: state.motionPrompt || "",
77
- options: params,
81
+ options: params && typeof params === "object" ? params : undefined,
78
82
  creationId: `image-to-video-${Date.now()}`,
79
83
  });
80
84
  setState((prev) => ({ ...prev, isProcessing: false }));
81
- return result;
85
+ const typedResult = result as { success: boolean; videoUrl?: string; thumbnailUrl?: string; error?: string };
86
+ return typedResult;
82
87
  } catch (error) {
83
88
  const message = error instanceof Error ? error.message : "Generation failed";
84
89
  setState((prev) => ({ ...prev, isProcessing: false, error: message }));
@@ -11,7 +11,10 @@ import {
11
11
  ANTI_REALISM_SEGMENTS,
12
12
  ANATOMY_NEGATIVE_SEGMENTS,
13
13
  } from "../../domain/entities/image-prompt-segments";
14
- import type { ImagePromptResult, ImagePromptBuilderOptions } from "./image-prompt-builder.types";
14
+ import type { ImagePromptResult, ImagePromptBuilderOptions, AnimeSelfiePromptResult } from "./image-prompt-builder.types";
15
+
16
+ // Export types
17
+ export type { ImagePromptResult, ImagePromptBuilderOptions, AnimeSelfiePromptResult };
15
18
 
16
19
  export class ImagePromptBuilder {
17
20
  private positiveSegments: string[] = [];
@@ -96,3 +99,6 @@ export class ImagePromptBuilder {
96
99
  };
97
100
  }
98
101
  }
102
+
103
+ // Export utility functions
104
+ export { createAnimeSelfiePrompt, createStyleTransferPrompt } from "../utils/prompt-creators.util";
@@ -13,10 +13,9 @@ import type {
13
13
  TextToImageFormActions,
14
14
  TextToImageFormDefaults,
15
15
  } from "../../domain/types/form.types";
16
- import { DEFAULT_FORM_VALUES } from "../../domain/constants/options.constants";
17
16
 
18
17
  export interface UseFormStateOptions {
19
- defaults: Required<TextToImageFormDefaults>;
18
+ defaults: TextToImageFormDefaults;
20
19
  }
21
20
 
22
21
  export interface UseFormStateReturn {
@@ -72,7 +72,7 @@ export function useGeneration(options: UseGenerationOptions): UseGenerationRetur
72
72
 
73
73
  // Use orchestrator
74
74
  const { generate, isGenerating, error } = useGenerationOrchestrator(strategy, {
75
- userId,
75
+ userId: userId ?? undefined,
76
76
  alertMessages: DEFAULT_ALERT_MESSAGES,
77
77
  onCreditsExhausted: () => callbacks.onCreditsRequired?.(totalCost),
78
78
  onSuccess: (result) => {
@@ -132,7 +132,7 @@ export function useGeneration(options: UseGenerationOptions): UseGenerationRetur
132
132
  return {
133
133
  generationState: {
134
134
  isGenerating,
135
- error: error?.message === undefined ? null : error.message,
135
+ error: error?.message ?? null,
136
136
  },
137
137
  totalCost,
138
138
  handleGenerate,
@@ -10,7 +10,6 @@ import { GenericWizardFlow } from "../../../../domains/generation/wizard/present
10
10
  import { TEXT_TO_IMAGE_WIZARD_CONFIG } from "../../../../domains/generation/wizard/configs";
11
11
  import { useAIFeatureGate } from "../../../../domains/access-control";
12
12
  import {
13
- createDefaultAlerts,
14
13
  createScenarioData,
15
14
  useWizardFlowHandlers,
16
15
  AutoSkipPreview,
@@ -55,7 +55,7 @@ class TextToVideoExecutor extends BaseExecutor<
55
55
  input: Record<string, unknown>,
56
56
  onProgress?: (progress: number) => void,
57
57
  ): Promise<unknown> {
58
- this.logInfo("Starting provider.run()...");
58
+ this.log("info","Starting provider.run()...");
59
59
 
60
60
  // Provider reports real progress via callback
61
61
  const result = await provider.run(model, input, {
@@ -68,7 +68,7 @@ class TextToVideoExecutor extends BaseExecutor<
68
68
  },
69
69
  });
70
70
 
71
- this.logInfo("provider.run() completed");
71
+ this.log("info","provider.run() completed");
72
72
  return result;
73
73
  }
74
74
 
@@ -21,7 +21,7 @@ export abstract class BaseExecutor<TRequest, TResult, TOutput> {
21
21
  options: BaseExecutorOptions<TRequest, TOutput>,
22
22
  ): Promise<Result<TResult, string>> {
23
23
  const providerResult = this.getProvider();
24
- if (providerResult.error) return failure(providerResult.error);
24
+ if (providerResult.error || !providerResult.provider) return failure(providerResult.error);
25
25
 
26
26
  const validationError = this.validateRequest(request);
27
27
  if (validationError) {
@@ -88,7 +88,7 @@ export abstract class BaseExecutor<TRequest, TResult, TOutput> {
88
88
  return success(this.transformResult(extracted as TOutput));
89
89
  }
90
90
 
91
- private log(level: "info" | "error", message: string): void {
91
+ protected log(level: "info" | "error", message: string): void {
92
92
  if (typeof __DEV__ !== "undefined" && __DEV__) {
93
93
  const fn = level === "error" ? console.error : console.log;
94
94
  fn(`[${this.logPrefix}] ${message}`);
@@ -4,4 +4,4 @@
4
4
  */
5
5
 
6
6
  export { BaseExecutor } from "./base-executor";
7
- export type { BaseExecutorOptions } from "./base-executor";
7
+ export type { BaseExecutorOptions } from "./base-executor.types";
@@ -19,7 +19,7 @@ export async function fetchWithTimeout<T>(
19
19
  const { retries = 0 } = options;
20
20
 
21
21
  const operation = async (): Promise<T> => {
22
- const response = await executeRequest<T>(url, options);
22
+ const response = await executeRequest(url, options);
23
23
 
24
24
  if (!isSuccessResponse(response)) {
25
25
  const errorMessage = await extractErrorMessage(response);
@@ -11,7 +11,7 @@ import type { RequestOptions } from "./api-client.types";
11
11
  /**
12
12
  * Executes HTTP request with timeout
13
13
  */
14
- export async function executeRequest<T>(
14
+ export async function executeRequest(
15
15
  url: string,
16
16
  options: RequestOptions = {}
17
17
  ): Promise<Response> {
@@ -2,7 +2,8 @@
2
2
  * Domain-Specific Type Guards
3
3
  */
4
4
 
5
- import { hasProperty, isObject } from "./structure-guards";
5
+ import { hasProperty } from "./structure-guards";
6
+ import { isObject } from "./primitive-guards";
6
7
  import { isNonEmptyString } from "./primitive-guards";
7
8
 
8
9
  /**
@@ -6,7 +6,6 @@ export * from "./error-classifier.util";
6
6
  export * from "./error-message-extractor.util";
7
7
  export * from "./error-handling.util";
8
8
  export * from "./validation.util";
9
- export * from "./type-guards.util";
10
9
  export * from "./api-client.util";
11
10
  export * from "../../domains/background/infrastructure/utils/polling-interval.util";
12
11
  export * from "./progress-calculator.util";
@@ -21,3 +20,8 @@ export * from "./provider-validator.util";
21
20
  export * from "./base64.util";
22
21
  export * from "./video-result-extractor.util";
23
22
  export * from "./id-generator.util";
23
+
24
+ // Export type guards (avoiding duplicate exports)
25
+ export { hasProperty, hasProperties } from "./structure-guards";
26
+ export { isObject, isNonEmptyString, isArray, isNumber, isFunction } from "./primitive-guards";
27
+ export { isCreationWithOutput, isWizardData } from "./domain-guards";
@@ -33,6 +33,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
33
33
  const [state, setState] = useState<GenerationState<TResult>>(INITIAL_STATE);
34
34
  const isGeneratingRef = useRef(false);
35
35
  const isMountedRef = useRef(true);
36
+ const abortControllerRef = useRef<AbortController | null>(null);
36
37
 
37
38
  const offlineStore = useOfflineStore();
38
39
  const { showError, showSuccess } = useAlert();
@@ -40,7 +41,10 @@ export const useGenerationOrchestrator = <TInput, TResult>(
40
41
 
41
42
  useEffect(() => {
42
43
  isMountedRef.current = true;
43
- return () => { isMountedRef.current = false; };
44
+ return () => {
45
+ isMountedRef.current = false;
46
+ abortControllerRef.current?.abort();
47
+ };
44
48
  }, []);
45
49
 
46
50
  const handleLifecycleComplete = useCallback(
@@ -91,6 +95,8 @@ export const useGenerationOrchestrator = <TInput, TResult>(
91
95
  if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[Orchestrator] generate() called");
92
96
  if (isGeneratingRef.current) return;
93
97
 
98
+ // Create new AbortController for this generation
99
+ abortControllerRef.current = new AbortController();
94
100
  isGeneratingRef.current = true;
95
101
  setState({ ...INITIAL_STATE, status: "checking", isGenerating: true });
96
102
 
@@ -100,6 +106,11 @@ export const useGenerationOrchestrator = <TInput, TResult>(
100
106
  throw createGenerationError("network", alertMessages.networkError);
101
107
  }
102
108
 
109
+ // Check if aborted
110
+ if (abortControllerRef.current.signal.aborted) {
111
+ throw new Error("Generation aborted");
112
+ }
113
+
103
114
  // Pre-validate credits before generation to catch concurrent consumption
104
115
  const creditCost = strategy.getCreditCost();
105
116
  const hasEnoughCredits = await checkCredits(creditCost);
@@ -111,6 +122,11 @@ export const useGenerationOrchestrator = <TInput, TResult>(
111
122
  throw createGenerationError("credits", alertMessages.creditFailed);
112
123
  }
113
124
 
125
+ // Check if aborted before moderation
126
+ if (abortControllerRef.current.signal.aborted) {
127
+ throw new Error("Generation aborted");
128
+ }
129
+
114
130
  return await handleModeration({
115
131
  input,
116
132
  moderation,
@@ -125,6 +141,14 @@ export const useGenerationOrchestrator = <TInput, TResult>(
125
141
  handleLifecycleComplete,
126
142
  });
127
143
  } catch (err) {
144
+ // Don't show error if aborted
145
+ if (abortControllerRef.current?.signal.aborted) {
146
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
147
+ console.log("[Orchestrator] Generation aborted");
148
+ }
149
+ return;
150
+ }
151
+
128
152
  const error = parseError(err);
129
153
  if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[Orchestrator] Error:", error);
130
154
  if (isMountedRef.current) setState({ status: "error", isGenerating: false, result: null, error });
@@ -134,12 +158,15 @@ export const useGenerationOrchestrator = <TInput, TResult>(
134
158
  throw error;
135
159
  } finally {
136
160
  isGeneratingRef.current = false;
161
+ abortControllerRef.current = null;
137
162
  }
138
163
  },
139
- [moderation, alertMessages, strategy, checkCredits, onCreditsExhausted, executeGeneration, showError, onError, handleLifecycleComplete],
164
+ [offlineStore.isOnline, moderation, alertMessages, strategy, checkCredits, onCreditsExhausted, executeGeneration, showError, onError, handleLifecycleComplete],
140
165
  );
141
166
 
142
167
  const reset = useCallback(() => {
168
+ abortControllerRef.current?.abort();
169
+ abortControllerRef.current = null;
143
170
  setState(INITIAL_STATE);
144
171
  isGeneratingRef.current = false;
145
172
  }, []);
@@ -15,6 +15,9 @@ import type {
15
15
  DualImageInput,
16
16
  } from "./use-image-generation.types";
17
17
 
18
+ // Export types
19
+ export type { ImageGenerationConfig, ImageGenerationInput, SingleImageInput, DualImageInput } from "./use-image-generation.types";
20
+
18
21
  const isDualImageInput = (input: ImageGenerationInput): input is DualImageInput =>
19
22
  "sourceImageBase64" in input && "targetImageBase64" in input;
20
23