@umituz/react-native-ai-generation-content 1.72.4 → 1.72.6
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 +1 -1
- package/src/domains/access-control/hooks/useAIFeatureGate.ts +49 -4
- package/src/domains/background/infrastructure/services/job-poller.service.ts +20 -0
- package/src/domains/background/presentation/hooks/use-pending-jobs.ts +6 -3
- package/src/domains/creations/presentation/hooks/useCreationPersistence.ts +10 -1
- package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +20 -6
- package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts +8 -5
- package/src/domains/generation/wizard/infrastructure/strategies/shared/photo-extraction.utils.ts +20 -2
- package/src/domains/generation/wizard/presentation/hooks/generationExecutor.ts +6 -2
- package/src/domains/generation/wizard/presentation/hooks/usePhotoUploadState.ts +21 -7
- package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +22 -4
- package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +9 -1
- package/src/domains/generation/wizard/presentation/hooks/videoQueuePoller.ts +6 -3
- package/src/domains/image-to-video/presentation/hooks/imageToVideoStrategy.ts +7 -1
- package/src/domains/result-preview/presentation/hooks/useResultActions.ts +6 -1
- package/src/domains/scenarios/presentation/screens/HierarchicalScenarioListScreen.tsx +2 -2
- package/src/domains/text-to-video/presentation/hooks/textToVideoStrategy.ts +7 -1
- package/src/infrastructure/services/generation-orchestrator.service.ts +5 -0
- package/src/infrastructure/services/image-feature-executor.service.ts +6 -1
- package/src/presentation/hooks/generation/orchestrator.ts +102 -12
- package/src/presentation/hooks/generation-flow-navigation.ts +13 -2
- package/src/presentation/hooks/use-generation.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.72.
|
|
3
|
+
"version": "1.72.6",
|
|
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",
|
|
@@ -17,16 +17,34 @@ import type {
|
|
|
17
17
|
AIFeatureGateReturn,
|
|
18
18
|
} from "../types/access-control.types";
|
|
19
19
|
|
|
20
|
+
declare const __DEV__: boolean;
|
|
21
|
+
|
|
20
22
|
const handlePromiseResult = (
|
|
21
23
|
result: void | Promise<void>,
|
|
22
24
|
onSuccess?: () => void,
|
|
23
25
|
onError?: (error: Error) => void,
|
|
24
26
|
): void => {
|
|
27
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
28
|
+
console.log("[AIFeatureGate] handlePromiseResult - isPromise:", result instanceof Promise);
|
|
29
|
+
}
|
|
25
30
|
if (result instanceof Promise) {
|
|
26
31
|
result
|
|
27
|
-
.then(() =>
|
|
28
|
-
|
|
32
|
+
.then(() => {
|
|
33
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
34
|
+
console.log("[AIFeatureGate] Promise resolved, calling onSuccess");
|
|
35
|
+
}
|
|
36
|
+
onSuccess?.();
|
|
37
|
+
})
|
|
38
|
+
.catch((err) => {
|
|
39
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
40
|
+
console.log("[AIFeatureGate] Promise rejected:", err);
|
|
41
|
+
}
|
|
42
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
43
|
+
});
|
|
29
44
|
} else {
|
|
45
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
46
|
+
console.log("[AIFeatureGate] Sync result, calling onSuccess");
|
|
47
|
+
}
|
|
30
48
|
onSuccess?.();
|
|
31
49
|
}
|
|
32
50
|
};
|
|
@@ -62,18 +80,45 @@ export function useAIFeatureGate(options: AIFeatureGateOptions): AIFeatureGateRe
|
|
|
62
80
|
|
|
63
81
|
const requireFeature = useCallback(
|
|
64
82
|
(action: () => void | Promise<void>): void => {
|
|
83
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
84
|
+
console.log("[AIFeatureGate] requireFeature called:", {
|
|
85
|
+
isOffline,
|
|
86
|
+
isAuthenticated,
|
|
87
|
+
isCreditsLoaded,
|
|
88
|
+
isPremium,
|
|
89
|
+
creditBalance,
|
|
90
|
+
creditCost,
|
|
91
|
+
hasCredits,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
65
95
|
if (isOffline) {
|
|
96
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
97
|
+
console.log("[AIFeatureGate] BLOCKED: User is offline");
|
|
98
|
+
}
|
|
66
99
|
onNetworkError?.();
|
|
67
100
|
return;
|
|
68
101
|
}
|
|
69
102
|
|
|
70
|
-
if (isAuthenticated && !isCreditsLoaded)
|
|
103
|
+
if (isAuthenticated && !isCreditsLoaded) {
|
|
104
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
105
|
+
console.log("[AIFeatureGate] BLOCKED: User authenticated but credits not loaded yet");
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
111
|
+
console.log("[AIFeatureGate] Calling requireFeatureFromPackage");
|
|
112
|
+
}
|
|
71
113
|
|
|
72
114
|
requireFeatureFromPackage(() => {
|
|
115
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
116
|
+
console.log("[AIFeatureGate] Inside requireFeatureFromPackage callback - executing action");
|
|
117
|
+
}
|
|
73
118
|
handlePromiseResult(action(), onSuccess, onError);
|
|
74
119
|
});
|
|
75
120
|
},
|
|
76
|
-
[isOffline, isAuthenticated, isCreditsLoaded, onNetworkError, requireFeatureFromPackage, onSuccess, onError],
|
|
121
|
+
[isOffline, isAuthenticated, isCreditsLoaded, isPremium, creditBalance, creditCost, hasCredits, onNetworkError, requireFeatureFromPackage, onSuccess, onError],
|
|
77
122
|
);
|
|
78
123
|
|
|
79
124
|
return {
|
|
@@ -77,6 +77,26 @@ export async function pollJob<T = unknown>(
|
|
|
77
77
|
signal,
|
|
78
78
|
} = options;
|
|
79
79
|
|
|
80
|
+
// Validate requestId early
|
|
81
|
+
if (!requestId || typeof requestId !== "string" || requestId.trim() === "") {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
error: new Error("Invalid requestId provided"),
|
|
85
|
+
attempts: 0,
|
|
86
|
+
elapsedMs: 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Validate model early
|
|
91
|
+
if (!model || typeof model !== "string" || model.trim() === "") {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
error: new Error("Invalid model provided"),
|
|
95
|
+
attempts: 0,
|
|
96
|
+
elapsedMs: 0,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
const pollingConfig = { ...DEFAULT_POLLING_CONFIG, ...config };
|
|
81
101
|
const { maxAttempts, maxTotalTimeMs, maxConsecutiveErrors } = pollingConfig;
|
|
82
102
|
|
|
@@ -85,7 +85,8 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
|
|
|
85
85
|
onSuccess: (id: string) => {
|
|
86
86
|
queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
|
|
87
87
|
queryKey,
|
|
88
|
-
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
88
|
+
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
89
|
+
old?.filter((job: BackgroundJob<TInput, TResult>) => job && job.id !== id) ?? [],
|
|
89
90
|
);
|
|
90
91
|
},
|
|
91
92
|
});
|
|
@@ -95,7 +96,8 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
|
|
|
95
96
|
onSuccess: () => {
|
|
96
97
|
queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
|
|
97
98
|
queryKey,
|
|
98
|
-
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
99
|
+
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
100
|
+
old?.filter((job: BackgroundJob<TInput, TResult>) => job && job.status !== "completed") ?? [],
|
|
99
101
|
);
|
|
100
102
|
},
|
|
101
103
|
});
|
|
@@ -105,7 +107,8 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
|
|
|
105
107
|
onSuccess: () => {
|
|
106
108
|
queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
|
|
107
109
|
queryKey,
|
|
108
|
-
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
110
|
+
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
111
|
+
old?.filter((job: BackgroundJob<TInput, TResult>) => job && job.status !== "failed") ?? [],
|
|
109
112
|
);
|
|
110
113
|
},
|
|
111
114
|
});
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useCallback, useMemo } from "react";
|
|
6
|
+
|
|
7
|
+
declare const __DEV__: boolean;
|
|
6
8
|
import { useAuth } from "@umituz/react-native-auth";
|
|
7
9
|
import { createCreationsRepository } from "../../infrastructure/adapters";
|
|
8
10
|
import type { Creation } from "../../domain/entities/Creation";
|
|
@@ -68,7 +70,14 @@ export function useCreationPersistence(
|
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
if (creditCost && onCreditDeduct) {
|
|
71
|
-
onCreditDeduct(creditCost).catch(() => {
|
|
73
|
+
onCreditDeduct(creditCost).catch((error) => {
|
|
74
|
+
// Log credit deduction errors for debugging
|
|
75
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
76
|
+
console.error("[CreationPersistence] Credit deduction failed:", error);
|
|
77
|
+
}
|
|
78
|
+
// Don't throw - credit deduction failure shouldn't block completion
|
|
79
|
+
// But we should notify user or retry in production
|
|
80
|
+
});
|
|
72
81
|
}
|
|
73
82
|
},
|
|
74
83
|
[userId, repository, creditCost, onCreditDeduct]
|
|
@@ -42,6 +42,14 @@ export function useProcessingJobsPoller(
|
|
|
42
42
|
const pollingRef = useRef<Set<string>>(new Set());
|
|
43
43
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
44
44
|
|
|
45
|
+
// Convert to IDs to prevent re-creating array on every render
|
|
46
|
+
const processingJobIds = useMemo(
|
|
47
|
+
() => creations
|
|
48
|
+
.filter((c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model)
|
|
49
|
+
.map((c) => c.id),
|
|
50
|
+
[creations],
|
|
51
|
+
);
|
|
52
|
+
|
|
45
53
|
const processingJobs = useMemo(
|
|
46
54
|
() => creations.filter(
|
|
47
55
|
(c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model,
|
|
@@ -104,7 +112,7 @@ export function useProcessingJobsPoller(
|
|
|
104
112
|
};
|
|
105
113
|
|
|
106
114
|
useEffect(() => {
|
|
107
|
-
if (!enabled || !userId ||
|
|
115
|
+
if (!enabled || !userId || processingJobIds.length === 0) {
|
|
108
116
|
if (intervalRef.current) {
|
|
109
117
|
clearInterval(intervalRef.current);
|
|
110
118
|
intervalRef.current = null;
|
|
@@ -112,13 +120,19 @@ export function useProcessingJobsPoller(
|
|
|
112
120
|
return;
|
|
113
121
|
}
|
|
114
122
|
|
|
123
|
+
// Get current jobs at poll time to avoid stale closures
|
|
124
|
+
const pollCurrentJobs = () => {
|
|
125
|
+
const currentJobs = creations.filter(
|
|
126
|
+
(c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model,
|
|
127
|
+
);
|
|
128
|
+
currentJobs.forEach((job) => pollJobRef.current?.(job));
|
|
129
|
+
};
|
|
130
|
+
|
|
115
131
|
// Initial poll
|
|
116
|
-
|
|
132
|
+
pollCurrentJobs();
|
|
117
133
|
|
|
118
134
|
// Set up interval polling
|
|
119
|
-
intervalRef.current = setInterval(
|
|
120
|
-
processingJobs.forEach((job) => pollJobRef.current?.(job));
|
|
121
|
-
}, DEFAULT_POLL_INTERVAL_MS);
|
|
135
|
+
intervalRef.current = setInterval(pollCurrentJobs, DEFAULT_POLL_INTERVAL_MS);
|
|
122
136
|
|
|
123
137
|
return () => {
|
|
124
138
|
// Clear polling set first to prevent new operations
|
|
@@ -130,7 +144,7 @@ export function useProcessingJobsPoller(
|
|
|
130
144
|
intervalRef.current = null;
|
|
131
145
|
}
|
|
132
146
|
};
|
|
133
|
-
}, [enabled, userId,
|
|
147
|
+
}, [enabled, userId, processingJobIds, creations]);
|
|
134
148
|
|
|
135
149
|
return {
|
|
136
150
|
processingCount: processingJobs.length,
|
package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts
CHANGED
|
@@ -83,14 +83,17 @@ function applyStyleEnhancements(prompt: string, wizardData: Record<string, unkno
|
|
|
83
83
|
export function createImageStrategy(options: CreateImageStrategyOptions): WizardStrategy {
|
|
84
84
|
const { scenario, creditCost } = options;
|
|
85
85
|
|
|
86
|
+
// Validate model early - fail fast
|
|
87
|
+
if (!scenario.model) {
|
|
88
|
+
throw new Error("Model is required for image generation");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const model = scenario.model;
|
|
92
|
+
|
|
86
93
|
return {
|
|
87
94
|
execute: async (input: unknown) => {
|
|
88
95
|
const imageInput = input as WizardImageInput;
|
|
89
|
-
|
|
90
|
-
throw new Error("Model is required for image generation");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const result = await executeImageGeneration(imageInput, scenario.model);
|
|
96
|
+
const result = await executeImageGeneration(imageInput, model);
|
|
94
97
|
|
|
95
98
|
if (!result.success || !result.imageUrl) {
|
|
96
99
|
throw new Error(result.error || "Image generation failed");
|
package/src/domains/generation/wizard/infrastructure/strategies/shared/photo-extraction.utils.ts
CHANGED
|
@@ -55,13 +55,31 @@ export async function extractPhotosAsBase64(
|
|
|
55
55
|
return [];
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
const
|
|
58
|
+
// Use Promise.allSettled to handle individual failures gracefully
|
|
59
|
+
const results = await Promise.allSettled(
|
|
60
|
+
photoUris.map(async (uri, index) => {
|
|
61
|
+
try {
|
|
62
|
+
return await readFileAsBase64(uri);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
65
|
+
console.error(`[PhotoExtraction] Failed to read photo ${index}:`, error);
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Extract successful results only
|
|
73
|
+
const validPhotos = results
|
|
74
|
+
.map((result) => (result.status === "fulfilled" ? result.value : null))
|
|
75
|
+
.filter((photo): photo is string => typeof photo === "string" && photo.length > 0);
|
|
60
76
|
|
|
61
77
|
if (enableDebugLogs && typeof __DEV__ !== "undefined" && __DEV__) {
|
|
78
|
+
const failedCount = results.filter((r) => r.status === "rejected").length;
|
|
62
79
|
console.log("[PhotoExtraction] Converted photos", {
|
|
63
80
|
total: photoUris.length,
|
|
64
81
|
valid: validPhotos.length,
|
|
82
|
+
failed: failedCount,
|
|
65
83
|
sizes: validPhotos.map((p) => `${(p.length / 1024).toFixed(1)}KB`),
|
|
66
84
|
});
|
|
67
85
|
}
|
|
@@ -44,9 +44,13 @@ export const executeWizardGeneration = async (params: ExecuteGenerationParams):
|
|
|
44
44
|
console.log("[WizardGeneration] GENERATING -", isVideoMode ? "VIDEO" : "PHOTO");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
// Safely extract prompt with type guard
|
|
48
|
+
const prompt = typeof input === "object" && input !== null && "prompt" in input && typeof input.prompt === "string"
|
|
49
|
+
? input.prompt
|
|
50
|
+
: "";
|
|
51
|
+
|
|
48
52
|
const generationFn = isVideoMode ? videoGenerationFn : photoGenerationFn;
|
|
49
|
-
await generationFn(input,
|
|
53
|
+
await generationFn(input, prompt);
|
|
50
54
|
|
|
51
55
|
if (isMountedRef.current) {
|
|
52
56
|
dispatch({ type: "COMPLETE" });
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useState, useCallback, useEffect, useRef } from "react";
|
|
8
|
+
|
|
9
|
+
declare const __DEV__: boolean;
|
|
8
10
|
import { useMedia, MediaQuality, MediaValidationError, MEDIA_CONSTANTS } from "@umituz/react-native-design-system";
|
|
9
11
|
import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
|
|
10
12
|
|
|
@@ -51,6 +53,15 @@ export const usePhotoUploadState = ({
|
|
|
51
53
|
const { pickImage, isLoading } = useMedia();
|
|
52
54
|
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
53
55
|
|
|
56
|
+
// Use refs to avoid effect re-runs on callback changes
|
|
57
|
+
const onErrorRef = useRef(onError);
|
|
58
|
+
const translationsRef = useRef(translations);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
onErrorRef.current = onError;
|
|
62
|
+
translationsRef.current = translations;
|
|
63
|
+
}, [onError, translations]);
|
|
64
|
+
|
|
54
65
|
const maxFileSizeMB = config?.maxFileSizeMB ?? MEDIA_CONSTANTS.MAX_IMAGE_SIZE_MB;
|
|
55
66
|
|
|
56
67
|
useEffect(() => {
|
|
@@ -61,28 +72,31 @@ export const usePhotoUploadState = ({
|
|
|
61
72
|
}, [stepId, initialImage]);
|
|
62
73
|
|
|
63
74
|
useEffect(() => {
|
|
75
|
+
// Clear any existing timeout first
|
|
76
|
+
if (timeoutRef.current) {
|
|
77
|
+
clearTimeout(timeoutRef.current);
|
|
78
|
+
timeoutRef.current = undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
if (isLoading) {
|
|
65
82
|
timeoutRef.current = setTimeout(() => {
|
|
66
83
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
67
84
|
console.warn("[usePhotoUploadState] Image picker timeout - possible stuck state");
|
|
68
85
|
}
|
|
69
|
-
|
|
70
|
-
title:
|
|
86
|
+
onErrorRef.current?.({
|
|
87
|
+
title: translationsRef.current.error,
|
|
71
88
|
message: "Image selection is taking too long. Please try again.",
|
|
72
89
|
});
|
|
73
90
|
}, 30000);
|
|
74
|
-
} else {
|
|
75
|
-
if (timeoutRef.current) {
|
|
76
|
-
clearTimeout(timeoutRef.current);
|
|
77
|
-
}
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
return () => {
|
|
81
94
|
if (timeoutRef.current) {
|
|
82
95
|
clearTimeout(timeoutRef.current);
|
|
96
|
+
timeoutRef.current = undefined;
|
|
83
97
|
}
|
|
84
98
|
};
|
|
85
|
-
}, [isLoading
|
|
99
|
+
}, [isLoading]);
|
|
86
100
|
|
|
87
101
|
const clearImage = useCallback(() => {
|
|
88
102
|
setImage(null);
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useEffect, useRef, useCallback, useState } from "react";
|
|
6
|
+
|
|
7
|
+
declare const __DEV__: boolean;
|
|
6
8
|
import { pollQueueStatus } from "./videoQueuePoller";
|
|
7
9
|
import { DEFAULT_POLL_INTERVAL_MS } from "../../../../../infrastructure/constants/polling.constants";
|
|
8
10
|
import type { GenerationUrls } from "./generation-result.utils";
|
|
@@ -50,7 +52,11 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
50
52
|
imageUrl: urls.imageUrl,
|
|
51
53
|
videoUrl: urls.videoUrl,
|
|
52
54
|
});
|
|
53
|
-
} catch {
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
57
|
+
console.error("[VideoQueue] Failed to update completion status:", error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
resetRefs();
|
|
56
62
|
onSuccess?.(urls);
|
|
@@ -64,7 +70,11 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
64
70
|
if (creationId && userId) {
|
|
65
71
|
try {
|
|
66
72
|
await persistence.updateToFailed(userId, creationId, errorMsg);
|
|
67
|
-
} catch {
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
75
|
+
console.error("[VideoQueue] Failed to update error status:", error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
68
78
|
}
|
|
69
79
|
resetRefs();
|
|
70
80
|
onError?.(errorMsg);
|
|
@@ -107,7 +117,11 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
107
117
|
prompt,
|
|
108
118
|
});
|
|
109
119
|
creationIdRef.current = creationId;
|
|
110
|
-
} catch {
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
122
|
+
console.error("[VideoQueue] Failed to save processing creation:", error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
const queueResult = await strategy.submitToQueue(input);
|
|
@@ -126,7 +140,11 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
126
140
|
if (creationId && userId && queueResult.requestId && queueResult.model) {
|
|
127
141
|
try {
|
|
128
142
|
await persistence.updateRequestId(userId, creationId, queueResult.requestId, queueResult.model);
|
|
129
|
-
} catch {
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
145
|
+
console.error("[VideoQueue] Failed to update request ID:", error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
pollingRef.current = setInterval(() => void pollStatus(), DEFAULT_POLL_INTERVAL_MS);
|
|
@@ -72,7 +72,8 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
|
|
|
72
72
|
if (isGeneratingStep && state.status === "IDLE" && !isAlreadyGenerating) {
|
|
73
73
|
dispatch({ type: "START_PREPARATION" });
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
// Execute generation and handle errors properly
|
|
76
|
+
void executeWizardGeneration({
|
|
76
77
|
wizardData,
|
|
77
78
|
scenario,
|
|
78
79
|
isVideoMode,
|
|
@@ -81,6 +82,13 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
|
|
|
81
82
|
onError,
|
|
82
83
|
videoGenerationFn: videoGeneration.startGeneration,
|
|
83
84
|
photoGenerationFn: photoGeneration.startGeneration,
|
|
85
|
+
}).catch((error) => {
|
|
86
|
+
// Catch any unhandled errors from executeWizardGeneration
|
|
87
|
+
if (isMountedRef.current) {
|
|
88
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
89
|
+
dispatch({ type: "ERROR", error: errorMsg });
|
|
90
|
+
onError?.(errorMsg);
|
|
91
|
+
}
|
|
84
92
|
});
|
|
85
93
|
}
|
|
86
94
|
|
|
@@ -16,12 +16,15 @@ interface PollParams {
|
|
|
16
16
|
export const pollQueueStatus = async (params: PollParams): Promise<void> => {
|
|
17
17
|
const { requestId, model, isPollingRef, pollingRef, onComplete, onError } = params;
|
|
18
18
|
|
|
19
|
+
// Atomic check-and-set to prevent race condition
|
|
19
20
|
if (isPollingRef.current) return;
|
|
21
|
+
isPollingRef.current = true;
|
|
20
22
|
|
|
21
23
|
const provider = providerRegistry.getActiveProvider();
|
|
22
|
-
if (!provider)
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
if (!provider) {
|
|
25
|
+
isPollingRef.current = false;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
25
28
|
try {
|
|
26
29
|
const status = await provider.getJobStatus(model, requestId);
|
|
27
30
|
if (__DEV__) console.log("[VideoQueueGeneration] Poll:", status.status);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { executeImageToVideo } from "../../infrastructure/services";
|
|
2
|
+
|
|
3
|
+
declare const __DEV__: boolean;
|
|
2
4
|
import type { GenerationStrategy } from "../../../../presentation/hooks/generation";
|
|
3
5
|
import type {
|
|
4
6
|
ImageToVideoFeatureConfig,
|
|
@@ -41,7 +43,11 @@ export const createImageToVideoStrategy = (
|
|
|
41
43
|
type: "image-to-video",
|
|
42
44
|
imageUri: input.imageUrl,
|
|
43
45
|
metadata: input.options as Record<string, unknown> | undefined,
|
|
44
|
-
}).catch(() => {
|
|
46
|
+
}).catch((error) => {
|
|
47
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
48
|
+
console.error("[ImageToVideo] onGenerationStart callback failed:", error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
45
51
|
|
|
46
52
|
const result = await executeImageToVideo(
|
|
47
53
|
{ imageUri: input.imageUrl, userId, motionPrompt: input.prompt, options: input.options },
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useState, useCallback } from "react";
|
|
7
|
+
|
|
8
|
+
declare const __DEV__: boolean;
|
|
7
9
|
import * as MediaLibrary from "expo-media-library";
|
|
8
10
|
import * as Sharing from "expo-sharing";
|
|
9
11
|
import {
|
|
@@ -88,7 +90,10 @@ export const useResultActions = (options: UseResultActionsOptions = {}): UseResu
|
|
|
88
90
|
onShareEnd?.(true);
|
|
89
91
|
}
|
|
90
92
|
} catch (error: unknown) {
|
|
91
|
-
if (__DEV__
|
|
93
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
94
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
95
|
+
console.log("[ResultActions] Share cancelled or failed:", errorMsg);
|
|
96
|
+
}
|
|
92
97
|
onShareEnd?.(true);
|
|
93
98
|
} finally {
|
|
94
99
|
setIsSharing(false);
|
|
@@ -39,7 +39,7 @@ export const HierarchicalScenarioListScreen: React.FC<HierarchicalScenarioListSc
|
|
|
39
39
|
onSelectScenario,
|
|
40
40
|
onBack,
|
|
41
41
|
t,
|
|
42
|
-
numColumns =
|
|
42
|
+
numColumns = 2,
|
|
43
43
|
isLoading = false,
|
|
44
44
|
}) => {
|
|
45
45
|
const tokens = useAppDesignTokens();
|
|
@@ -133,7 +133,7 @@ export const HierarchicalScenarioListScreen: React.FC<HierarchicalScenarioListSc
|
|
|
133
133
|
data={filteredScenarios}
|
|
134
134
|
numColumns={numColumns}
|
|
135
135
|
showsVerticalScrollIndicator={false}
|
|
136
|
-
columnWrapperStyle={
|
|
136
|
+
columnWrapperStyle={styles.row}
|
|
137
137
|
renderItem={renderItem}
|
|
138
138
|
keyExtractor={(item) => item.id}
|
|
139
139
|
ListEmptyComponent={isLoading ? LoadingComponent : (filteredScenarios.length === 0 ? ListEmptyComponent : null)}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { executeTextToVideo } from "../../infrastructure/services";
|
|
2
|
+
|
|
3
|
+
declare const __DEV__: boolean;
|
|
2
4
|
import type { GenerationStrategy } from "../../../../presentation/hooks/generation";
|
|
3
5
|
import type {
|
|
4
6
|
TextToVideoConfig,
|
|
@@ -40,7 +42,11 @@ export const createTextToVideoStrategy = (
|
|
|
40
42
|
type: "text-to-video",
|
|
41
43
|
prompt: input.prompt,
|
|
42
44
|
metadata: input.options as Record<string, unknown> | undefined,
|
|
43
|
-
}).catch(() => {
|
|
45
|
+
}).catch((error) => {
|
|
46
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
47
|
+
console.error("[TextToVideo] onGenerationStart callback failed:", error);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
44
50
|
|
|
45
51
|
const result = await executeTextToVideo(
|
|
46
52
|
{ prompt: input.prompt, userId, options: input.options },
|
|
@@ -86,6 +86,11 @@ class GenerationOrchestratorService {
|
|
|
86
86
|
throw pollResult.error ?? new Error("Polling failed");
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Validate result exists before type assertion
|
|
90
|
+
if (!pollResult.data) {
|
|
91
|
+
throw new Error("Polling succeeded but no data returned");
|
|
92
|
+
}
|
|
93
|
+
|
|
89
94
|
const result = pollResult.data as T;
|
|
90
95
|
const duration = Date.now() - startTime;
|
|
91
96
|
|
|
@@ -72,10 +72,15 @@ export async function executeImageFeature(
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Safely extract requestId with type guard
|
|
76
|
+
const requestId = typeof result === "object" && result !== null && "requestId" in result && typeof result.requestId === "string"
|
|
77
|
+
? result.requestId
|
|
78
|
+
: undefined;
|
|
79
|
+
|
|
75
80
|
return {
|
|
76
81
|
success: true,
|
|
77
82
|
imageUrl,
|
|
78
|
-
requestId
|
|
83
|
+
requestId,
|
|
79
84
|
};
|
|
80
85
|
} catch (error) {
|
|
81
86
|
const message = extractErrorMessage(error, "Processing failed", `Image:${featureType}`);
|
|
@@ -34,6 +34,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
34
34
|
const isGeneratingRef = useRef(false);
|
|
35
35
|
const isMountedRef = useRef(true);
|
|
36
36
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
37
|
+
const cleanupTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
37
38
|
|
|
38
39
|
const offlineStore = useOfflineStore();
|
|
39
40
|
const { showError, showSuccess } = useAlert();
|
|
@@ -44,43 +45,92 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
44
45
|
return () => {
|
|
45
46
|
isMountedRef.current = false;
|
|
46
47
|
abortControllerRef.current?.abort();
|
|
48
|
+
// Clear any pending lifecycle complete timeout
|
|
49
|
+
if (cleanupTimeoutRef.current) {
|
|
50
|
+
clearTimeout(cleanupTimeoutRef.current);
|
|
51
|
+
cleanupTimeoutRef.current = null;
|
|
52
|
+
}
|
|
47
53
|
};
|
|
48
54
|
}, []);
|
|
49
55
|
|
|
50
56
|
const handleLifecycleComplete = useCallback(
|
|
51
57
|
(status: "success" | "error", result?: TResult, error?: GenerationError) => {
|
|
52
58
|
if (!lifecycle?.onComplete) return;
|
|
59
|
+
|
|
60
|
+
// Clear any existing timeout first
|
|
61
|
+
if (cleanupTimeoutRef.current) {
|
|
62
|
+
clearTimeout(cleanupTimeoutRef.current);
|
|
63
|
+
cleanupTimeoutRef.current = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
const delay = lifecycle.completeDelay ?? 500;
|
|
54
|
-
|
|
55
|
-
if (isMountedRef.current)
|
|
67
|
+
cleanupTimeoutRef.current = setTimeout(() => {
|
|
68
|
+
if (isMountedRef.current) {
|
|
69
|
+
lifecycle.onComplete?.(status, result, error);
|
|
70
|
+
}
|
|
71
|
+
cleanupTimeoutRef.current = null;
|
|
56
72
|
}, delay);
|
|
57
|
-
// Store timeout ID for cleanup (if component unmounts during delay)
|
|
58
|
-
return () => clearTimeout(timeoutId);
|
|
59
73
|
},
|
|
60
74
|
[lifecycle],
|
|
61
75
|
);
|
|
62
76
|
|
|
63
77
|
const executeGeneration = useCallback(
|
|
64
78
|
async (input: TInput) => {
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
80
|
+
console.log("[Orchestrator] ----------------------------------------");
|
|
81
|
+
console.log("[Orchestrator] executeGeneration() called");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const creditCost = strategy.getCreditCost();
|
|
85
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
86
|
+
console.log("[Orchestrator] Deducting credits:", creditCost);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const creditDeducted = await deductCredit(creditCost);
|
|
90
|
+
if (!creditDeducted) {
|
|
91
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
92
|
+
console.log("[Orchestrator] ERROR: Credit deduction failed");
|
|
93
|
+
}
|
|
94
|
+
throw createGenerationError("credits", alertMessages.creditFailed);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
98
|
+
console.log("[Orchestrator] Credits deducted successfully");
|
|
99
|
+
}
|
|
67
100
|
|
|
68
101
|
setState((prev) => ({ ...prev, status: "generating" }));
|
|
69
|
-
if (typeof __DEV__ !== "undefined" && __DEV__)
|
|
102
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
103
|
+
console.log("[Orchestrator] State: generating - calling strategy.execute()");
|
|
104
|
+
}
|
|
70
105
|
|
|
71
106
|
const result = await strategy.execute(input);
|
|
107
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
108
|
+
console.log("[Orchestrator] strategy.execute() completed");
|
|
109
|
+
}
|
|
72
110
|
|
|
73
111
|
if (strategy.save && userId) {
|
|
74
112
|
if (isMountedRef.current) setState((prev) => ({ ...prev, status: "saving" }));
|
|
113
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
114
|
+
console.log("[Orchestrator] Saving result to Firestore");
|
|
115
|
+
}
|
|
75
116
|
try {
|
|
76
117
|
await strategy.save(result, userId);
|
|
118
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
119
|
+
console.log("[Orchestrator] Result saved successfully");
|
|
120
|
+
}
|
|
77
121
|
} catch (saveErr) {
|
|
122
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
123
|
+
console.log("[Orchestrator] ERROR: Save failed:", saveErr);
|
|
124
|
+
}
|
|
78
125
|
throw createGenerationError("save", alertMessages.saveFailed, saveErr instanceof Error ? saveErr : undefined);
|
|
79
126
|
}
|
|
80
127
|
}
|
|
81
128
|
|
|
82
129
|
if (isMountedRef.current) setState({ status: "success", isGenerating: false, result, error: null });
|
|
83
|
-
if (typeof __DEV__ !== "undefined" && __DEV__)
|
|
130
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
131
|
+
console.log("[Orchestrator] ✅ Generation SUCCESS");
|
|
132
|
+
console.log("[Orchestrator] ========================================");
|
|
133
|
+
}
|
|
84
134
|
|
|
85
135
|
if (alertMessages.success) showSuccess("Success", alertMessages.success);
|
|
86
136
|
onSuccess?.(result);
|
|
@@ -92,41 +142,81 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
92
142
|
|
|
93
143
|
const generate = useCallback(
|
|
94
144
|
async (input: TInput) => {
|
|
95
|
-
if (typeof __DEV__ !== "undefined" && __DEV__)
|
|
96
|
-
|
|
145
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
146
|
+
console.log("[Orchestrator] ========================================");
|
|
147
|
+
console.log("[Orchestrator] generate() called with input:", JSON.stringify(input).substring(0, 200));
|
|
148
|
+
console.log("[Orchestrator] isGenerating:", isGeneratingRef.current);
|
|
149
|
+
console.log("[Orchestrator] userId:", userId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (isGeneratingRef.current) {
|
|
153
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
154
|
+
console.log("[Orchestrator] BLOCKED: Already generating");
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
97
158
|
|
|
98
159
|
// Create new AbortController for this generation
|
|
99
160
|
abortControllerRef.current = new AbortController();
|
|
100
161
|
isGeneratingRef.current = true;
|
|
101
162
|
setState({ ...INITIAL_STATE, status: "checking", isGenerating: true });
|
|
102
163
|
|
|
164
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
165
|
+
console.log("[Orchestrator] State set to 'checking', isGenerating: true");
|
|
166
|
+
}
|
|
167
|
+
|
|
103
168
|
try {
|
|
104
169
|
// Check online status inside the try block to avoid dependency on offlineStore.isOnline
|
|
105
170
|
if (!offlineStore.isOnline) {
|
|
171
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
172
|
+
console.log("[Orchestrator] ERROR: User is offline");
|
|
173
|
+
}
|
|
106
174
|
throw createGenerationError("network", alertMessages.networkError);
|
|
107
175
|
}
|
|
108
176
|
|
|
177
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
178
|
+
console.log("[Orchestrator] Online check passed");
|
|
179
|
+
}
|
|
180
|
+
|
|
109
181
|
// Check if aborted
|
|
110
182
|
if (abortControllerRef.current.signal.aborted) {
|
|
183
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
184
|
+
console.log("[Orchestrator] ERROR: Generation aborted (1)");
|
|
185
|
+
}
|
|
111
186
|
throw new Error("Generation aborted");
|
|
112
187
|
}
|
|
113
188
|
|
|
114
189
|
// Pre-validate credits before generation to catch concurrent consumption
|
|
115
190
|
const creditCost = strategy.getCreditCost();
|
|
191
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
192
|
+
console.log("[Orchestrator] Checking credits - cost:", creditCost);
|
|
193
|
+
}
|
|
194
|
+
|
|
116
195
|
const hasEnoughCredits = await checkCredits(creditCost);
|
|
117
196
|
if (!hasEnoughCredits) {
|
|
118
197
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
119
|
-
console.log("[Orchestrator] Pre-validation
|
|
198
|
+
console.log("[Orchestrator] ERROR: Pre-validation failed - insufficient credits");
|
|
120
199
|
}
|
|
121
200
|
onCreditsExhausted?.();
|
|
122
201
|
throw createGenerationError("credits", alertMessages.creditFailed);
|
|
123
202
|
}
|
|
124
203
|
|
|
204
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
205
|
+
console.log("[Orchestrator] Credit check passed");
|
|
206
|
+
}
|
|
207
|
+
|
|
125
208
|
// Check if aborted before moderation
|
|
126
209
|
if (abortControllerRef.current.signal.aborted) {
|
|
210
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
211
|
+
console.log("[Orchestrator] ERROR: Generation aborted (2)");
|
|
212
|
+
}
|
|
127
213
|
throw new Error("Generation aborted");
|
|
128
214
|
}
|
|
129
215
|
|
|
216
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
217
|
+
console.log("[Orchestrator] Starting moderation check");
|
|
218
|
+
}
|
|
219
|
+
|
|
130
220
|
return await handleModeration({
|
|
131
221
|
input,
|
|
132
222
|
moderation,
|
|
@@ -161,7 +251,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
161
251
|
abortControllerRef.current = null;
|
|
162
252
|
}
|
|
163
253
|
},
|
|
164
|
-
[offlineStore
|
|
254
|
+
[offlineStore, moderation, alertMessages, strategy, checkCredits, onCreditsExhausted, executeGeneration, showError, onError, handleLifecycleComplete],
|
|
165
255
|
);
|
|
166
256
|
|
|
167
257
|
const reset = useCallback(() => {
|
|
@@ -24,7 +24,11 @@ export function createGoNextHandler(
|
|
|
24
24
|
onComplete?.(newState);
|
|
25
25
|
} else {
|
|
26
26
|
setState({ ...state, currentStepIndex: nextIndex });
|
|
27
|
-
|
|
27
|
+
// Bounds check before accessing array
|
|
28
|
+
const nextStep = config.photoSteps[nextIndex];
|
|
29
|
+
if (nextStep) {
|
|
30
|
+
onStepChange?.(nextIndex, nextStep);
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
};
|
|
30
34
|
}
|
|
@@ -40,8 +44,15 @@ export function createGoBackHandler(
|
|
|
40
44
|
if (!canGoBack) return;
|
|
41
45
|
|
|
42
46
|
const prevIndex = currentStepIndex - 1;
|
|
47
|
+
// Validate index is within bounds
|
|
48
|
+
if (prevIndex < 0) return;
|
|
49
|
+
|
|
43
50
|
setState((prev) => ({ ...prev, currentStepIndex: prevIndex, isComplete: false }));
|
|
44
|
-
|
|
51
|
+
// Bounds check before accessing array
|
|
52
|
+
const prevStep = config.photoSteps[prevIndex];
|
|
53
|
+
if (prevStep) {
|
|
54
|
+
onStepChange?.(prevIndex, prevStep);
|
|
55
|
+
}
|
|
45
56
|
};
|
|
46
57
|
}
|
|
47
58
|
|
|
@@ -88,7 +88,7 @@ export function useGeneration<T = unknown>(
|
|
|
88
88
|
|
|
89
89
|
const genResult = await generationOrchestrator.generate<T>(request);
|
|
90
90
|
|
|
91
|
-
if (abortRef.current || abortControllerRef.current
|
|
91
|
+
if (abortRef.current || abortControllerRef.current?.signal.aborted) return;
|
|
92
92
|
|
|
93
93
|
setResult(genResult);
|
|
94
94
|
|